🏃 Lesson 4.2: Pawns, Characters & Movement
Last lesson, input reached your functions. Now we build the body those functions drive — a full third-person Character in C++, complete with a camera, and tuned via the Character Movement Component that gives you walking, jumping, and falling for free.
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Assemble a third-person Character with a spring arm and camera in C++
- Wire movement input to
AddMovementInputand jumping toJump() - Tune the Character Movement Component (speed, jump, rotation)
- Explain "orient rotation to movement" vs "use controller yaw"
- Decide when a bare Pawn beats a Character
Estimated Time: 60 minutes
Engine Version: Unreal Engine 5.8
In This Lesson
Anatomy of a Character
Recall from Lesson 3.1: ACharacter arrives with a capsule (collision), a skeletal mesh, and a Character Movement Component. For a third-person game you add a spring arm (a camera boom that pulls in near walls) and a camera on its end.
Building the Character
Create via New C++ Class → Character. Add the camera boom and camera in the constructor. Header (abbreviated to the new parts; input properties from Lesson 4.1 still apply):
// MyCharacter.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "MyCharacter.generated.h"
class USpringArmComponent;
class UCameraComponent;
class UInputAction;
class UInputMappingContext;
struct FInputActionValue;
UCLASS()
class MYPROJECT_API AMyCharacter : public ACharacter
{
GENERATED_BODY()
public:
AMyCharacter();
protected:
virtual void BeginPlay() override;
virtual void SetupPlayerInputComponent(UInputComponent* InputComponent) override;
UPROPERTY(VisibleAnywhere, Category = "Camera")
TObjectPtr<USpringArmComponent> SpringArm;
UPROPERTY(VisibleAnywhere, Category = "Camera")
TObjectPtr<UCameraComponent> Camera;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
TObjectPtr<UInputMappingContext> DefaultMappingContext;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
TObjectPtr<UInputAction> MoveAction;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
TObjectPtr<UInputAction> LookAction;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
TObjectPtr<UInputAction> JumpAction;
void Move(const FInputActionValue& Value);
void Look(const FInputActionValue& Value);
};
// MyCharacter.cpp (constructor)
#include "MyCharacter.h"
#include "GameFramework/SpringArmComponent.h"
#include "Camera/CameraComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
AMyCharacter::AMyCharacter()
{
PrimaryActorTick.bCanEverTick = false; // movement comp handles per-frame work
// Spring arm: a boom attached to the capsule root.
SpringArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArm"));
SpringArm->SetupAttachment(RootComponent);
SpringArm->TargetArmLength = 400.0f;
SpringArm->bUsePawnControlRotation = true; // arm rotates with the controller
// Camera on the end of the arm.
Camera = CreateDefaultSubobject<UCameraComponent>(TEXT("Camera"));
Camera->SetupAttachment(SpringArm);
Camera->bUsePawnControlRotation = false; // camera follows the arm, not input directly
// Movement feel: the body turns toward its movement direction, not the camera.
bUseControllerRotationYaw = false;
GetCharacterMovement()->bOrientRotationToMovement = true;
GetCharacterMovement()->RotationRate = FRotator(0.0f, 540.0f, 0.0f);
GetCharacterMovement()->JumpZVelocity = 500.0f;
GetCharacterMovement()->MaxWalkSpeed = 500.0f;
}
📖 bUsePawnControlRotation
On the spring arm, bUsePawnControlRotation = true means "rotate this boom with the player's look input." On the camera it's false because the camera just rides the arm. This split — arm follows input, camera follows arm — is the standard third-person rig.
Movement & Jump
Bind input as in Lesson 4.1, then implement the handlers. The movement handler is where the camera-relative feel comes together:
#include "EnhancedInputComponent.h"
#include "InputActionValue.h"
void AMyCharacter::SetupPlayerInputComponent(UInputComponent* InputComponent)
{
Super::SetupPlayerInputComponent(InputComponent);
if (UEnhancedInputComponent* EIC = Cast<UEnhancedInputComponent>(InputComponent))
{
EIC->BindAction(MoveAction, ETriggerEvent::Triggered, this, &AMyCharacter::Move);
EIC->BindAction(LookAction, ETriggerEvent::Triggered, this, &AMyCharacter::Look);
// ACharacter provides Jump()/StopJumping() out of the box:
EIC->BindAction(JumpAction, ETriggerEvent::Started, this, &ACharacter::Jump);
EIC->BindAction(JumpAction, ETriggerEvent::Completed, this, &ACharacter::StopJumping);
}
}
void AMyCharacter::Move(const FInputActionValue& Value)
{
const FVector2D Axis = Value.Get<FVector2D>();
if (!Controller) { return; }
// Move relative to where the CONTROLLER faces (camera-relative), ignoring pitch.
const FRotator YawRotation(0.0f, Controller->GetControlRotation().Yaw, 0.0f);
const FVector Forward = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
const FVector Right = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);
AddMovementInput(Forward, Axis.Y);
AddMovementInput(Right, Axis.X);
}
void AMyCharacter::Look(const FInputActionValue& Value)
{
const FVector2D Axis = Value.Get<FVector2D>();
AddControllerYawInput(Axis.X);
AddControllerPitchInput(Axis.Y);
}
✅ Why compute forward from the controller yaw?
Using the controller's yaw (not the actor's) makes "push up = move away from camera," which is what players expect in third person. We zero the pitch so looking up/down doesn't make the character try to fly. AddMovementInput hands the vector to the movement component, which does the actual physics.
Tuning the Movement Component
UCharacterMovementComponent is a deep, battle-tested system (it's even network-predicted — Module 10). You mostly tune it via properties. The essentials:
| Property | Controls |
|---|---|
MaxWalkSpeed | Top walking speed (cm/s) |
JumpZVelocity | Upward launch speed of a jump |
AirControl | How much you can steer mid-air (0–1) |
GravityScale | Multiplier on gravity (floaty vs heavy) |
bOrientRotationToMovement | Body turns to face movement direction |
RotationRate | How fast the body turns |
// Access it anywhere via GetCharacterMovement():
UCharacterMovementComponent* Move = GetCharacterMovement();
Move->MaxWalkSpeed = 600.0f; // e.g. when sprinting
Move->AirControl = 0.2f;
⚠️ Don't reinvent movement
It's tempting to move a character by directly setting its location in Tick. Resist — you'd lose collision response, step handling, slopes, jumping, and network prediction. Feed intent through AddMovementInput and let the movement component do the work. Custom movement is an advanced topic; the component covers the vast majority of games.
When to Use a Bare Pawn
ACharacter is purpose-built for humanoid walking. When your controllable thing doesn't walk like a person, a bare APawn is the better base:
Use ACharacter | Use APawn |
|---|---|
| Player/NPC on foot | Vehicles, aircraft, boats |
| Walk, jump, crouch, swim | Flying/hovering, 6-DOF space movement |
| Capsule + skeletal mesh humanoid | A camera-only spectator, a security camera, a cursor |
💡 The trade:ACharactergives you a lot for free but assumes humanoid locomotion. A bareAPawngives you a blank body — you add a mesh, a collision component, and your own movement (or aUFloatingPawnMovement/UPawnMovementComponent). Pick the one whose assumptions match your creature.
Hands-on Exercise & Quiz
🏋️ Exercise: Add a sprint
Objective: Use input + the movement component together.
- Add a
SprintAction(Digital). - Bind
Started→StartSprintandCompleted→StopSprint. - In those, set
GetCharacterMovement()->MaxWalkSpeedto 900 (sprint) / 500 (walk). - Why is toggling a movement-component property cleaner than special-casing speed in
Move?
✅ Handlers & reasoning
void AMyCharacter::StartSprint(const FInputActionValue&)
{
GetCharacterMovement()->MaxWalkSpeed = 900.0f;
}
void AMyCharacter::StopSprint(const FInputActionValue&)
{
GetCharacterMovement()->MaxWalkSpeed = 500.0f;
}
Because the movement component already applies MaxWalkSpeed to acceleration, friction, and network prediction. Changing one property lets all that machinery keep working; hand-scaling the input vector would fight it.
🎯 Quick Quiz
Question 1: Which component makes ACharacter walk, jump, and fall?
Question 2: How should you apply movement intent to a Character?
Question 3: You're building a controllable hovercraft. Best base class?
Summary
🎉 Key Takeaways
ACharacterships with capsule + skeletal mesh + movement; add a spring arm and camera in C++ for third person.- Set
SpringArm->bUsePawnControlRotation = true(arm follows input) and the camerafalse(rides the arm). - Feed movement through
AddMovementInputusing the controller's yaw for camera-relative control; useACharacter::Jumpfor jumping. - Tune feel via the Character Movement Component (
MaxWalkSpeed,JumpZVelocity,bOrientRotationToMovement) — never move by setting location in Tick. - Use a bare Pawn when movement isn't humanoid walking (vehicles, flying, spectator).
📚 Additional Resources
🚀 What's Next?
Your character moves. But who owns the input, the camera target, and the per-player data — and how does a brain attach to a body? The final Module 4 lesson covers the PlayerController and possession, tying the framework together.
🎉 Lesson complete!
You have a moving character. Let's understand who's really driving.