Skip to main content

🏃 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 AddMovementInput and jumping to Jump()
  • 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.

graph TD Cap["CapsuleComponent (root, inherited)"] --> Mesh["SkeletalMesh (inherited)"] Cap --> Arm["SpringArm (you add)"] Arm --> Cam["Camera (you add)"] Cap --> Move["CharacterMovementComponent (inherited)"]
Figure 1: A third-person Character. The capsule is the root; the spring arm and camera are what you add in C++. The movement component isn't in the transform hierarchy — it's a logic component driving the capsule.

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:

PropertyControls
MaxWalkSpeedTop walking speed (cm/s)
JumpZVelocityUpward launch speed of a jump
AirControlHow much you can steer mid-air (0–1)
GravityScaleMultiplier on gravity (floaty vs heavy)
bOrientRotationToMovementBody turns to face movement direction
RotationRateHow 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 ACharacterUse APawn
Player/NPC on footVehicles, aircraft, boats
Walk, jump, crouch, swimFlying/hovering, 6-DOF space movement
Capsule + skeletal mesh humanoidA camera-only spectator, a security camera, a cursor
💡 The trade: ACharacter gives you a lot for free but assumes humanoid locomotion. A bare APawn gives you a blank body — you add a mesh, a collision component, and your own movement (or a UFloatingPawnMovement / UPawnMovementComponent). Pick the one whose assumptions match your creature.

Hands-on Exercise & Quiz

🏋️ Exercise: Add a sprint

Objective: Use input + the movement component together.

  1. Add a SprintAction (Digital).
  2. Bind StartedStartSprint and CompletedStopSprint.
  3. In those, set GetCharacterMovement()->MaxWalkSpeed to 900 (sprint) / 500 (walk).
  4. 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

  • ACharacter ships 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 camera false (rides the arm).
  • Feed movement through AddMovementInput using the controller's yaw for camera-relative control; use ACharacter::Jump for 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.