Lesson 5: Player Controller & Movement Systems
Welcome to Core Gameplay Development!
Excellent work completing Phase 1! Now we're diving into Phase 2: Core Development. In this lesson, you'll build the foundation of player interaction - responsive movement, smooth camera controls, and polished character controller systems that feel great in multiplayer.
Learning Objectives
By the end of this lesson, you will:
- ✅ Build responsive player movement with C++ and Blueprint
- ✅ Implement camera controls and input handling
- ✅ Create character controller systems with proper replication
- ✅ Add movement animations and states
- ✅ Implement jump, sprint, and crouch mechanics
- ✅ Test movement in multiplayer environment
- ✅ Optimize movement for network performance
Part 1: Character Controller Setup
Step 1: Enhance BRCharacter Class
1.1 Update Character Header
Open BRCharacter.h and add movement properties:
UCLASS()
class MULTIPLAYERBATTLEROYALE_API ABRCharacter : public ACharacter
{
GENERATED_BODY()
public:
ABRCharacter(const FObjectInitializer& ObjectInitializer);
protected:
virtual void BeginPlay() override;
virtual void SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) override;
virtual void Tick(float DeltaTime) override;
// Movement properties
UPROPERTY(Replicated, BlueprintReadOnly, Category = "Movement")
float MaxWalkSpeed = 600.0f;
UPROPERTY(Replicated, BlueprintReadOnly, Category = "Movement")
float MaxSprintSpeed = 900.0f;
UPROPERTY(Replicated, BlueprintReadOnly, Category = "Movement")
float MaxCrouchSpeed = 300.0f;
UPROPERTY(Replicated, BlueprintReadOnly, Category = "Movement")
bool bIsSprinting = false;
UPROPERTY(Replicated, BlueprintReadOnly, Category = "Movement")
bool bIsCrouching = false;
// Camera properties
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
class USpringArmComponent* SpringArm;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
class UCameraComponent* Camera;
// Input functions
void MoveForward(float Value);
void MoveRight(float Value);
void Turn(float Value);
void LookUp(float Value);
void StartJump();
void StopJump();
void StartSprint();
void StopSprint();
void StartCrouch();
void StopCrouch();
// Server RPCs
UFUNCTION(Server, Reliable, WithValidation)
void ServerStartSprint();
UFUNCTION(Server, Reliable, WithValidation)
void ServerStopSprint();
UFUNCTION(Server, Reliable, WithValidation)
void ServerStartCrouch();
UFUNCTION(Server, Reliable, WithValidation)
void ServerStopCrouch();
// Replication
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
// Movement update
void UpdateMovementSpeed();
};
1.2 Implement Character Class
Open BRCharacter.cpp:
#include "BRCharacter.h"
#include "Camera/CameraComponent.h"
#include "Components/CapsuleComponent.h"
#include "Components/InputComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "GameFramework/SpringArmComponent.h"
#include "Net/UnrealNetwork.h"
ABRCharacter::ABRCharacter(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
PrimaryActorTick.bCanEverTick = true;
// Enable replication
bReplicates = true;
SetReplicatingMovement(true);
// Set default values
MaxHealth = 100.0f;
Health = MaxHealth;
MaxWalkSpeed = 600.0f;
MaxSprintSpeed = 900.0f;
MaxCrouchSpeed = 300.0f;
bIsSprinting = false;
bIsCrouching = false;
// Create spring arm component
SpringArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArm"));
SpringArm->SetupAttachment(RootComponent);
SpringArm->TargetArmLength = 300.0f;
SpringArm->bUsePawnControlRotation = true;
SpringArm->bInheritPitch = true;
SpringArm->bInheritYaw = true;
SpringArm->bInheritRoll = false;
// Create camera component
Camera = CreateDefaultSubobject<UCameraComponent>(TEXT("Camera"));
Camera->SetupAttachment(SpringArm, USpringArmComponent::SocketName);
Camera->bUsePawnControlRotation = false;
// Configure character movement
GetCharacterMovement()->MaxWalkSpeed = MaxWalkSpeed;
GetCharacterMovement()->JumpZVelocity = 600.0f;
GetCharacterMovement()->AirControl = 0.35f;
GetCharacterMovement()->bOrientRotationToMovement = true;
GetCharacterMovement()->RotationRate = FRotator(0.0f, 540.0f, 0.0f);
}
void ABRCharacter::BeginPlay()
{
Super::BeginPlay();
UpdateMovementSpeed();
}
void ABRCharacter::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}
void ABRCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
// Movement bindings
PlayerInputComponent->BindAxis("MoveForward", this, &ABRCharacter::MoveForward);
PlayerInputComponent->BindAxis("MoveRight", this, &ABRCharacter::MoveRight);
PlayerInputComponent->BindAxis("Turn", this, &ABRCharacter::Turn);
PlayerInputComponent->BindAxis("LookUp", this, &ABRCharacter::LookUp);
// Action bindings
PlayerInputComponent->BindAction("Jump", IE_Pressed, this, &ABRCharacter::StartJump);
PlayerInputComponent->BindAction("Jump", IE_Released, this, &ABRCharacter::StopJump);
PlayerInputComponent->BindAction("Sprint", IE_Pressed, this, &ABRCharacter::StartSprint);
PlayerInputComponent->BindAction("Sprint", IE_Released, this, &ABRCharacter::StopSprint);
PlayerInputComponent->BindAction("Crouch", IE_Pressed, this, &ABRCharacter::StartCrouch);
PlayerInputComponent->BindAction("Crouch", IE_Released, this, &ABRCharacter::StopCrouch);
}
void ABRCharacter::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(ABRCharacter, MaxWalkSpeed);
DOREPLIFETIME(ABRCharacter, MaxSprintSpeed);
DOREPLIFETIME(ABRCharacter, MaxCrouchSpeed);
DOREPLIFETIME(ABRCharacter, bIsSprinting);
DOREPLIFETIME(ABRCharacter, bIsCrouching);
}
// Movement functions
void ABRCharacter::MoveForward(float Value)
{
if (Controller != nullptr && Value != 0.0f)
{
const FRotator Rotation = Controller->GetControlRotation();
const FRotator YawRotation(0, Rotation.Yaw, 0);
const FVector Direction = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
AddMovementInput(Direction, Value);
}
}
void ABRCharacter::MoveRight(float Value)
{
if (Controller != nullptr && Value != 0.0f)
{
const FRotator Rotation = Controller->GetControlRotation();
const FRotator YawRotation(0, Rotation.Yaw, 0);
const FVector Direction = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);
AddMovementInput(Direction, Value);
}
}
void ABRCharacter::Turn(float Value)
{
AddControllerYawInput(Value);
}
void ABRCharacter::LookUp(float Value)
{
AddControllerPitchInput(Value);
}
void ABRCharacter::StartJump()
{
bPressedJump = true;
}
void ABRCharacter::StopJump()
{
bPressedJump = false;
}
void ABRCharacter::StartSprint()
{
if (HasAuthority())
{
bIsSprinting = true;
UpdateMovementSpeed();
}
else
{
ServerStartSprint();
}
}
void ABRCharacter::StopSprint()
{
if (HasAuthority())
{
bIsSprinting = false;
UpdateMovementSpeed();
}
else
{
ServerStopSprint();
}
}
void ABRCharacter::StartCrouch()
{
if (HasAuthority())
{
bIsCrouching = true;
Crouch();
UpdateMovementSpeed();
}
else
{
ServerStartCrouch();
}
}
void ABRCharacter::StopCrouch()
{
if (HasAuthority())
{
bIsCrouching = false;
UnCrouch();
UpdateMovementSpeed();
}
else
{
ServerStopCrouch();
}
}
void ABRCharacter::UpdateMovementSpeed()
{
if (GetCharacterMovement())
{
if (bIsCrouching)
{
GetCharacterMovement()->MaxWalkSpeed = MaxCrouchSpeed;
}
else if (bIsSprinting)
{
GetCharacterMovement()->MaxWalkSpeed = MaxSprintSpeed;
}
else
{
GetCharacterMovement()->MaxWalkSpeed = MaxWalkSpeed;
}
}
}
// Server RPCs
void ABRCharacter::ServerStartSprint_Implementation()
{
bIsSprinting = true;
UpdateMovementSpeed();
}
bool ABRCharacter::ServerStartSprint_Validate()
{
return true;
}
void ABRCharacter::ServerStopSprint_Implementation()
{
bIsSprinting = false;
UpdateMovementSpeed();
}
bool ABRCharacter::ServerStopSprint_Validate()
{
return true;
}
void ABRCharacter::ServerStartCrouch_Implementation()
{
bIsCrouching = true;
Crouch();
UpdateMovementSpeed();
}
bool ABRCharacter::ServerStartCrouch_Validate()
{
return true;
}
void ABRCharacter::ServerStopCrouch_Implementation()
{
bIsCrouching = false;
UnCrouch();
UpdateMovementSpeed();
}
bool ABRCharacter::ServerStopCrouch_Validate()
{
return true;
}
Part 2: Input Mapping
Step 2: Configure Input Actions
2.1 Create Input Actions
-
In Unreal Editor, go to Edit → Project Settings
-
Navigate to Engine → Input
-
Create these Action Mappings:
Jump→ Space BarSprint→ Left ShiftCrouch→ Left Control
-
Create these Axis Mappings:
MoveForward→ W (Scale: 1.0), S (Scale: -1.0)MoveRight→ D (Scale: 1.0), A (Scale: -1.0)Turn→ Mouse X (Scale: 1.0)LookUp→ Mouse Y (Scale: -1.0)
2.2 Test Input
- Play in Editor and test all movement inputs
- Verify camera rotation works smoothly
- Test sprint and crouch toggles
Part 3: Movement Animation Setup
Step 3: Create Animation Blueprint
3.1 Create Animation Blueprint
- Right-click in Content Browser → Animation → Animation Blueprint
- Select AnimInstance as parent class
- Select your character's skeleton
- Name it:
ABP_BRCharacter
3.2 Set Up State Machine
Create these states in the Animation Blueprint:
Idle State:
- Default state
- Plays idle animation
- Transitions to Walk when speed > 0
Walk State:
- Plays walk animation
- Transitions to Idle when speed = 0
- Transitions to Sprint when sprinting
Sprint State:
- Plays sprint animation
- Transitions to Walk when not sprinting
Jump State:
- Plays jump animation
- Transitions to Idle when grounded
Crouch State:
- Plays crouch animation
- Transitions to Idle when standing
3.3 Blend Spaces
Create blend spaces for smooth transitions:
- Walk Blend Space: Forward/Backward, Left/Right
- Sprint Blend Space: Forward/Backward, Left/Right
Part 4: Network Optimization
Step 4: Optimize for Multiplayer
4.1 Movement Replication Settings
In BRCharacter.cpp constructor:
// Optimize movement replication
GetCharacterMovement()->SetIsReplicated(true);
GetCharacterMovement()->SetNetUpdateFrequency(30.0f); // 30 Hz for movement
GetCharacterMovement()->SetNetServerMoveTickEnabled(true);
4.2 Client Prediction
Unreal handles client prediction automatically, but ensure:
- Movement is server-authoritative
- RPCs validate on server
- Smooth interpolation enabled
4.3 Network Smoothing
Enable network smoothing in Character Movement Component:
- Network Smoothing: Enabled
- Network Smoothing Server Max Distance: 200.0
- Network Smoothing Server Max Distance (Debug): 500.0
Part 5: Testing & Polish
Step 5: Test Movement
5.1 Single Player Testing
- Test all movement inputs
- Verify camera controls feel smooth
- Test sprint and crouch mechanics
- Check animation transitions
5.2 Multiplayer Testing
- Launch Play in Editor with 2+ players
- Test movement replication
- Verify other players move smoothly
- Test sprint/crouch replication
- Check for lag or jitter
5.3 Performance Testing
- Monitor frame rate during movement
- Check network bandwidth usage
- Verify no performance drops with multiple players
- Test with 50+ simulated players if possible
Mini Challenge: Create Advanced Movement System
Task: Enhance the movement system with advanced features.
Requirements:
- Add sliding mechanic (slide when sprinting + crouch)
- Add wall-running or wall-jumping
- Add stamina system for sprinting
- Add movement speed modifiers (injured, carrying items)
- Create smooth camera transitions
- Add footstep sound effects based on surface
- Test all features in multiplayer
Deliverables:
- Enhanced character controller with new mechanics
- Stamina system implementation
- Sound integration
- Multiplayer testing results
Pro Tips:
- Use Blueprint for rapid iteration on movement feel
- Test movement feel extensively before moving on
- Get feedback from playtesters early
- Optimize network updates for large player counts
- Use animation montages for special moves
- Consider adding movement abilities (dash, roll, etc.)
Common Mistakes to Avoid
1. Not Testing in Multiplayer
- ❌ Only testing single-player movement
- ✅ Always test movement with multiple clients
2. Ignoring Network Performance
- ❌ Not optimizing replication frequency
- ✅ Set appropriate update frequencies for movement
3. Poor Input Handling
- ❌ Not validating input on server
- ✅ Always validate movement input server-side
4. Missing Animation States
- ❌ Not handling all movement states
- ✅ Create complete animation state machine
5. Camera Issues
- ❌ Camera lag or jitter
- ✅ Use smooth camera interpolation
Troubleshooting
Q: Movement feels laggy in multiplayer A: Check network update frequency, enable network smoothing, and verify server tick rate.
Q: Sprint doesn't replicate to other clients A: Ensure sprint state is replicated and RPCs are called correctly.
Q: Camera rotation is too fast/slow A: Adjust mouse sensitivity in input settings or add sensitivity multiplier.
Q: Animations don't play correctly A: Check animation blueprint state machine transitions and blend spaces.
Q: Character falls through ground A: Check collision settings, ensure capsule component is properly configured.
Key Takeaways
✅ Character Controller: Build responsive movement with C++ and Blueprint ✅ Input System: Configure input mappings for all movement actions ✅ Camera Controls: Implement smooth camera rotation and positioning ✅ Movement States: Create sprint, crouch, and jump mechanics ✅ Animation System: Set up animation blueprints and state machines ✅ Network Optimization: Optimize movement replication for multiplayer ✅ Testing: Test extensively in single-player and multiplayer
What's Next?
In Lesson 6: Multiplayer Networking & Replication, we'll:
- Deep dive into Unreal's replication system
- Implement advanced replication techniques
- Create reliable RPC systems
- Optimize network bandwidth
- Handle lag compensation and prediction
- Build robust multiplayer synchronization
Get ready to master the networking systems that make multiplayer games work!
Additional Resources
- Unreal Engine Character Movement Documentation
- Animation Blueprint Guide
- Input System Documentation
- Network Replication Best Practices
Ready to continue? Move on to Lesson 6 to master multiplayer networking!