How to Build Game Characters the Right Way in Unreal Engine 5

- Unreal Engine Actor Pawn Character classes form a three-tier inheritance system where Actor provides basic world placement, Pawn adds controller possession and input handling, and Character incl...

Key Takeaways

Ready to Start Building Your First Game?

Here's the thing - understanding Actor, Pawn, and Character classes is fundamental, but applying this knowledge to build an actual playable game takes structured practice. I've seen countless students learn the theory and then struggle when it's time to put everything together.

That's exactly why we created a hands-on course that takes you from basic concepts all the way to building professional game experiences. You'll work on real projects, solve actual game development problems, and build a portfolio that shows you can ship games.

Start your game development journey with our comprehensive course →

The Day I Realized I Was Fighting the Engine, Not Using It

Been there. Spent the better part of a week trying to figure out why my custom enemy character wasn't responding to input in my first Unreal project. The mesh was there, the AI controller was assigned, but nothing worked. Turned out I was inheriting from Actor when I needed APawn, and I had no clue about the possession system.

This isn't unusual. When you're starting out with Unreal Engine C++ tutorial content, the Actor-Pawn-Character hierarchy seems simple on paper. Three classes, right? But then you hit real development and suddenly you're dealing with component lifecycles, controller possession, network replication, and input stacks that don't behave the way you expect.

Here's what actually matters: these three classes aren't just organizational tools. They're the fundamental building blocks of every entity in your game world. Pick the wrong one and you'll spend days working around limitations that don't need to exist. Pick the right one and the engine does half your work for you.

Let me show you exactly how these classes work, when to use each one, and how to avoid the pitfalls that trip up most beginners.

What These Three Classes Actually Do (And Why You Care)

Let's talk about what you're actually building when you create game entities. In Unreal Engine, everything that exists in your level - from static props to player characters to AI enemies - inherits from one of these three classes.

AActor is the foundation. It's a UObject that can exist in the 3D world with a position, rotation, and scale. It has a component system, lifecycle management, and networking support. Think of it as a container with a transform that can hold functionality through components.

APawn extends Actor by adding the ability to be possessed by a Controller. This is the separation between "what is being controlled" (the Pawn) and "who is controlling it" (the Controller). Pawns have input handling, movement input accumulation, and can be controlled by either players (via PlayerController) or AI (via AIController).

ACharacter extends Pawn specifically for bipedal, vertically-oriented entities. It comes pre-configured with a capsule for collision, a skeletal mesh for visuals, and most importantly, a Character Movement Component that handles walking, jumping, crouching, swimming, and flying with built-in network replication.

Here's why this hierarchy matters for your projects: if you're building a player character or humanoid NPC, you want ACharacter because you get sophisticated movement for free. If you're building a vehicle, drone, or RTS unit, you want APawn because you need control but not bipedal movement. If you're building props, triggers, or environmental objects, you want AActor because you don't need the overhead of controller possession.

The inheritance chain looks like this:

plaintext
UObject (Base engine object with reflection, serialization, GC)
    ↓
AActor (Placeable/spawnable object with transform and components)
    ↓
APawn (Possessable entity with controller and input)
    ↓
ACharacter (Humanoid pawn with skeletal mesh and advanced movement)

Key architectural concepts you need to understand:

Composition Over Inheritance: Unreal Engine favors component-based architecture. Actors serve as containers for UActorComponent instances that provide specific functionality. This design enables flexible, reusable systems without deep inheritance hierarchies. Instead of creating a "ShootingMovingDamagableActor" class, you create an Actor with a Shooting Component, a Movement Component, and a Health Component.

Separation of Control and Representation: The framework separates "what is controlled" (Pawn) from "who/what is controlling" (Controller). This distinction allows controllers to persist while pawns are transient, enabling scenarios like character death/respawn or vehicle entry/exit. When your character dies, the Controller unpossesses the dead pawn and can possess a new one without losing player state.

Network-Aware Design: All three classes are designed with multiplayer in mind, providing built-in replication support, client-server architecture integration, and prediction/correction mechanisms. This isn't bolted on - it's fundamental to how these classes work.

The Decision You Need to Make First: Which Class Should You Inherit From?

Actually, wait - before you write a single line of code, you need to choose the right base class. I've seen students spend days implementing custom movement on an Actor when Character would have given them everything they needed. Here's the decision matrix I wish someone had shown me on day one:

Use Case Recommended Base Class Rationale
Environmental objects, props, triggers AActor No control needed; lightweight
Vehicles, drones, non-humanoid entities APawn Requires control but not bipedal movement
Player characters, humanoid NPCs ACharacter Full movement system, animation support
Static scenery, visual effects AActor Minimal overhead, no gameplay logic
Flying entities with custom physics APawn + Custom Movement More control than Character provides
Top-down game units, RTS entities APawn Control needed, Character is overkill

The rule of thumb: start with the least complex class that meets your needs. You can always extend functionality through components, but you can't easily strip out built-in features from a more complex base class.

AActor: Your Foundation for Everything That Exists in the World

AActor is the base class for all objects that can be placed or spawned in an Unreal Engine level. It provides four fundamental capabilities: 3D transformation via RootComponent, component-based architecture for composing functionality, lifecycle management from spawn to destruction, and network replication framework for multiplayer.

The component-based design means all Actor functionality is delivered through components. The Actor itself is primarily a container and coordinator. Every Actor has a RootComponent (typically USceneComponent or derived) that defines its world-space transform. All other components attach to the root, creating a hierarchy.

Transform management works through the RootComponent. Actors don't directly store transform data. Instead, all spatial information flows through the RootComponent. Methods like GetActorLocation() internally call RootComponent->GetComponentLocation(). This indirection provides flexibility - you can swap root components to change how an Actor behaves in space.

Understanding the Actor Lifecycle (Get This Wrong and Nothing Works)

Here's the thing - I spent months figuring out that the order in which initialization happens actually matters. Put your setup code in the wrong place and you'll get null references, incomplete initialization, or logic that runs in the editor when you only wanted it in-game.

The complete lifecycle sequence is:

plaintext
1. Constructor (C++)
2. PostActorCreated / PostLoad
3. OnConstruction (Blueprint)
4. PreInitializeComponents
5. InitializeComponent (per component)
6. PostInitializeComponents
7. BeginPlay
8. Tick (every frame, if enabled)
9. EndPlay
10. BeginDestroy
11. Garbage Collection

The Constructor: Where You Set Defaults Only

The constructor runs when creating the Class Default Object (CDO) and when spawning instances. It runs in both editor and runtime contexts. This means you cannot access other actors or world context here.

cpp
AMyActor::AMyActor()
{
    // Enable/disable tick
    PrimaryActorTick.bCanEverTick = true;
    PrimaryActorTick.TickGroup = TG_PrePhysics;

    // Create root component
    RootComponent = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));

    // Create and attach components
    MeshComponent = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));
    MeshComponent->SetupAttachment(RootComponent);

    // Set default properties
    bReplicates = true;
    NetUpdateFrequency = 10.0f;
}

Constructor rules:

PostInitializeComponents: When Components Are Ready

This is called after all components are initialized but before gameplay begins. It's network-agnostic - use this for setup that needs components but doesn't care about client vs server.

cpp
void AMyActor::PostInitializeComponents()
{
    Super::PostInitializeComponents();

    // Components are now fully initialized
    // Ideal for caching component references
    CachedMeshComponent = FindComponentByClass<UStaticMeshComponent>();

    // Setup cross-component interactions
    if (MeshComponent && CollisionComponent)
    {
        MeshComponent->AttachToComponent(CollisionComponent, ...);
    }
}

BeginPlay: Your Primary Entry Point for Gameplay Logic

This is where most of your gameplay initialization should happen. The world is fully loaded, other actors exist, and it's safe to query the environment.

cpp
void AMyActor::BeginPlay()
{
    Super::BeginPlay();

    // Gameplay initialization
    // Safe to access other actors in the world
    TArray<AActor*> OverlappingActors;
    GetOverlappingActors(OverlappingActors);

    // Start timers, spawn effects, etc.
    GetWorldTimerManager().SetTimer(TimerHandle, this, &AMyActor::TimerFunction, 1.0f, true);
}

Tick: Per-Frame Updates (Use Sparingly)

Tick is called every frame for continuous updates. The DeltaTime parameter is the time elapsed since last frame in seconds.

cpp
void AMyActor::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);

    // Per-frame updates
    // DeltaTime = time elapsed since last frame (seconds)

    // Example: Continuous movement
    FVector NewLocation = GetActorLocation() + (Velocity * DeltaTime);
    SetActorLocation(NewLocation);
}

Performance considerations for tick:

I learned this the hard way. In my first shipped project, we had performance issues and discovered hundreds of Blueprint actors ticking when they didn't need to. Disabling unnecessary ticks gave us back several milliseconds per frame.

EndPlay: Where You Clean Up Everything

EndPlay is called during Destroy(), level transitions, streaming unloads, and application quit. Epic recommends using EndPlay over destructors for gameplay cleanup.

cpp
void AMyActor::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
    // Cleanup BEFORE calling Super
    GetWorldTimerManager().ClearAllTimersForObject(this);
    OnActorDestroyed.Clear();

    Super::EndPlay(EndPlayReason);
}

EndPlay reasons:

How to Manage Components Without Losing Your Mind

Components are where the actual functionality lives. Here's how to work with them effectively.

Adding Components at Construction

cpp
AMyActor::AMyActor()
{
    // Create component
    MeshComponent = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));

    // Attach to root
    MeshComponent->SetupAttachment(RootComponent);

    // Configure component
    MeshComponent->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
    MeshComponent->SetCollisionResponseToAllChannels(ECR_Block);
}

Adding Components at Runtime

When you need to create components after the Actor has been spawned, you need to register them manually.

cpp
void AMyActor::AddRuntimeComponent()
{
    UStaticMeshComponent* DynamicMesh = NewObject<UStaticMeshComponent>(this);
    DynamicMesh->RegisterComponent();
    DynamicMesh->AttachToComponent(RootComponent, FAttachmentTransformRules::KeepRelativeTransform);
}

Finding Components

cpp
// Template version (preferred)
UStaticMeshComponent* Mesh = FindComponentByClass<UStaticMeshComponent>();

// By interface
TArray<UActorComponent*> Components;
GetComponents<IMyInterface>(Components);

// Iterate all components
ForEachComponent<UStaticMeshComponent>([](UStaticMeshComponent* Comp) {
    Comp->SetVisibility(false);
    return true; // Continue iteration
});

Transform Operations: Moving Things Around the Right Way

Transform operations are fundamental to positioning and moving actors in your world.

Getting Transform Data

cpp
// Transform
FTransform Transform = GetActorTransform();
FVector Location = GetActorLocation();
FRotator Rotation = GetActorRotation();
FQuat Quat = GetActorQuat();
FVector Scale = GetActorScale3D();

// Direction vectors
FVector Forward = GetActorForwardVector();
FVector Right = GetActorRightVector();
FVector Up = GetActorUpVector();

Setting Transform

cpp
// Immediate teleport (no physics)
SetActorLocation(NewLocation, false, nullptr, ETeleportType::TeleportPhysics);

// With sweep (collision detection)
FHitResult Hit;
SetActorLocation(NewLocation, true, &Hit, ETeleportType::None);

// Additive movement
AddActorWorldOffset(FVector(0, 0, 100), true);
AddActorLocalOffset(FVector(100, 0, 0));

ETeleportType options:

Setting Up Network Replication That Actually Works

If you're building multiplayer games, replication setup is non-negotiable. Missing even one piece breaks everything.

Basic Setup

cpp
AMyActor::AMyActor()
{
    bReplicates = true;
    bNetUseOwnerRelevancy = false;
    NetUpdateFrequency = 10.0f; // Updates per second
    MinNetUpdateFrequency = 2.0f;
}

void AMyActor::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);

    // Replicate to all clients
    DOREPLIFETIME(AMyActor, Health);

    // Replicate only to owner
    DOREPLIFETIME_CONDITION(AMyActor, Ammo, COND_OwnerOnly);

    // Replicate with custom conditions
    DOREPLIFETIME_CONDITION(AMyActor, SecretData, COND_Custom);
}

Replicated Properties

cpp
UPROPERTY(Replicated)
float Health;

UPROPERTY(ReplicatedUsing=OnRep_Ammo)
int32 Ammo;

UFUNCTION()
void OnRep_Ammo()
{
    // Called on clients when Ammo changes
    UpdateAmmoUI();
}

APawn: When Your Object Needs to Be Controlled

APawn extends AActor with the capability to be possessed by Controllers. Pawns represent the physical manifestation of players or AI entities in the world.

Key Additions Over AActor

  1. Controller Possession System: Can be possessed/unpossessed by controllers
  2. Input Component: Built-in input handling via SetupPlayerInputComponent()
  3. Movement Input Accumulation: AddMovementInput() system for collecting directional input
  4. Controller Reference: Maintains reference to possessing controller
  5. Auto-Possession Settings: Automatic possession configuration
  6. Player State: Reference to replicated player information

The Controller Possession System Explained

Here's what's funny - the Controller-Pawn relationship confused me for months. I kept thinking the Pawn was the "player" but actually the Controller is the persistent player identity, and Pawns are just bodies they inhabit.

Possession Flow

cpp
// Controller possesses pawn
AController* MyController = GetWorld()->GetFirstPlayerController();
MyController->Possess(MyPawn);

// Or unpossess
MyController->UnPossess();

Internal process:

  1. Controller checks current possession, calls UnPossess() if needed
  2. Controller calls SetPawn(NewPawn)
  3. Pawn's PossessedBy(Controller) called (server-side)
  4. Pawn's SetOwner(Controller) called
  5. Input component created and SetupPlayerInputComponent() called (if PlayerController)

Lifecycle Callbacks

cpp
// Server-side: Called when possessed
void AMyPawn::PossessedBy(AController* NewController)
{
    Super::PossessedBy(NewController);

    // Initialize controller-dependent systems
    if (APlayerController* PC = Cast<APlayerController>(NewController))
    {
        // Player-specific initialization
    }
}

// Server-side: Called when unpossessed
void AMyPawn::UnPossessed()
{
    Super::UnPossessed();

    // Cleanup controller-dependent systems
}

// Client-side: Controller changed
void AMyPawn::ReceiveControllerChanged(AController* OldController, AController* NewController)
{
    // React to controller changes on client
}

Auto-Possession Configuration

cpp
AMyPawn::AMyPawn()
{
    // Auto-possess by player index
    AutoPossessPlayer = EAutoReceiveInput::Player0;

    // Auto-possess AI
    AutoPossessAI = EAutoPossessAI::PlacedInWorldOrSpawned;
    AIControllerClass = AMyAIController::StaticClass();
}

AutoPossessPlayer options:

AutoPossessAI options:

Handling Input the Unreal Way

Input handling happens in SetupPlayerInputComponent(), which is only called when possessed by a PlayerController (not AIController).

SetupPlayerInputComponent

cpp
void AMyPawn::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
    Super::SetupPlayerInputComponent(PlayerInputComponent);

    // Bind axis inputs (continuous)
    PlayerInputComponent->BindAxis("MoveForward", this, &AMyPawn::MoveForward);
    PlayerInputComponent->BindAxis("MoveRight", this, &AMyPawn::MoveRight);
    PlayerInputComponent->BindAxis("Turn", this, &AMyPawn::AddControllerYawInput);
    PlayerInputComponent->BindAxis("LookUp", this, &AMyPawn::AddControllerPitchInput);

    // Bind action inputs (discrete)
    PlayerInputComponent->BindAction("Jump", IE_Pressed, this, &AMyPawn::StartJump);
    PlayerInputComponent->BindAction("Jump", IE_Released, this, &AMyPawn::StopJump);
    PlayerInputComponent->BindAction("Fire", IE_Pressed, this, &AMyPawn::Fire);
}

Key points:

Movement Input System

cpp
void AMyPawn::MoveForward(float Value)
{
    if (Controller && Value != 0.0f)
    {
        // Get forward direction based on controller rotation
        const FRotator Rotation = Controller->GetControlRotation();
        const FRotator YawRotation(0, Rotation.Yaw, 0);
        const FVector Direction = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);

        // Accumulate movement input
        AddMovementInput(Direction, Value);
    }
}

void AMyPawn::MoveRight(float Value)
{
    if (Controller && 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);
    }
}

AddMovementInput behavior:

Movement Components

cpp
AMyPawn::AMyPawn()
{
    // Add floating pawn movement (simple flying movement)
    MovementComponent = CreateDefaultSubobject<UFloatingPawnMovement>(TEXT("Movement"));
    MovementComponent->UpdatedComponent = RootComponent;
    MovementComponent->MaxSpeed = 600.0f;
    MovementComponent->Acceleration = 4000.0f;
    MovementComponent->Deceleration = 8000.0f;
}

Common movement components:

Controller Access

cpp
// Get controller
AController* Controller = GetController();

// Template version with casting
APlayerController* PC = GetController<APlayerController>();
AAIController* AI = GetController<AAIController>();

// Check possession state
bool bIsPossessed = IsPawnControlled(); // Controller != nullptr

Use Controller Rotation

cpp
AMyPawn::AMyPawn()
{
    // Control which rotation axes follow controller
    bUseControllerRotationPitch = false;
    bUseControllerRotationYaw = true;
    bUseControllerRotationRoll = false;
}

Common patterns:

ACharacter: Pre-Built for Humanoid Game Characters

ACharacter is a specialized Pawn designed for vertically-oriented bipedal characters. It includes three pre-configured components and sophisticated movement capabilities that would take weeks to implement from scratch.

Pre-Built Components

cpp
// Components created automatically in ACharacter constructor
UCapsuleComponent* CapsuleComponent;        // Collision primitive (RootComponent)
USkeletalMeshComponent* Mesh;               // Visual representation
UCharacterMovementComponent* CharacterMovement; // Movement logic

Accessing components:

cpp
UCapsuleComponent* Capsule = GetCapsuleComponent();
USkeletalMeshComponent* Mesh = GetMesh();
UCharacterMovementComponent* Movement = GetCharacterMovement();

Setting Up Your Character the Right Way in Unreal Engine C++ Tutorial Style

This is the basic setup I use for every new character. It configures the three pre-built components and sets up common movement parameters.

cpp
AMyCharacter::AMyCharacter()
{
    // Configure capsule collision
    GetCapsuleComponent()->InitCapsuleSize(42.f, 96.0f); // Radius, Half-height
    GetCapsuleComponent()->SetCollisionResponseToChannel(ECC_Camera, ECR_Ignore);

    // Configure mesh
    GetMesh()->SetRelativeLocation(FVector(0.0f, 0.0f, -90.0f)); // Offset down
    GetMesh()->SetRelativeRotation(FRotator(0.0f, -90.0f, 0.0f)); // Rotate to face forward

    // Set skeletal mesh asset
    static ConstructorHelpers::FObjectFinder<USkeletalMesh> MeshAsset(TEXT("/Path/To/Mesh"));
    if (MeshAsset.Succeeded())
    {
        GetMesh()->SetSkeletalMesh(MeshAsset.Object);
    }

    // Configure movement
    GetCharacterMovement()->MaxWalkSpeed = 600.0f;
    GetCharacterMovement()->JumpZVelocity = 600.0f;
    GetCharacterMovement()->AirControl = 0.2f;
    GetCharacterMovement()->GravityScale = 1.0f;
    GetCharacterMovement()->bOrientRotationToMovement = true;
    GetCharacterMovement()->RotationRate = FRotator(0.0f, 540.0f, 0.0f);

    // Camera control
    bUseControllerRotationPitch = false;
    bUseControllerRotationYaw = false;
    bUseControllerRotationRoll = false;
}

Built-In Movement Actions: Jumping, Crouching, and Movement Modes

One of the huge advantages of ACharacter is that jumping and crouching are already implemented. You just need to call the methods.

Jumping

cpp
// Jump methods
void Jump();          // Start jump
void StopJumping();   // Stop continuous jump
bool CanJump() const; // Check if jump is possible

// Custom jump force
void LaunchCharacter(FVector LaunchVelocity, bool bXYOverride, bool bZOverride);

// Input binding
void AMyCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
    Super::SetupPlayerInputComponent(PlayerInputComponent);

    PlayerInputComponent->BindAction("Jump", IE_Pressed, this, &ACharacter::Jump);
    PlayerInputComponent->BindAction("Jump", IE_Released, this, &ACharacter::StopJumping);
}

Jump properties:

cpp
GetCharacterMovement()->JumpZVelocity = 600.0f;        // Initial velocity
GetCharacterMovement()->AirControl = 0.2f;              // Control while airborne
GetCharacterMovement()->GravityScale = 1.0f;            // Gravity multiplier
GetCharacterMovement()->JumpMaxCount = 1;               // Multi-jump support

Crouching

cpp
// Enable crouching
GetCharacterMovement()->NavAgentProps.bCanCrouch = true;
GetCharacterMovement()->MaxWalkSpeedCrouched = 300.0f;
GetCharacterMovement()->CrouchedHalfHeight = 60.0f;

// Crouch methods
void Crouch(bool bClientSimulation = false);
void UnCrouch(bool bClientSimulation = false);
bool CanCrouch() const;

// Check crouch state
bool bIsCrouched; // Public member variable

// Input binding
PlayerInputComponent->BindAction("Crouch", IE_Pressed, this, &ACharacter::Crouch);
PlayerInputComponent->BindAction("Crouch", IE_Released, this, &ACharacter::UnCrouch);

Movement Modes

The Character Movement Component UE5 supports five movement modes:

cpp
enum EMovementMode
{
    MOVE_None,      // No movement
    MOVE_Walking,   // Walking on ground
    MOVE_Falling,   // Falling or jumping
    MOVE_Swimming,  // Swimming in fluid volume
    MOVE_Flying,    // Flying movement
    MOVE_Custom,    // Custom movement implementation
};

// Get current mode
EMovementMode CurrentMode = GetCharacterMovement()->MovementMode;

// Check specific modes
bool bIsWalking = GetCharacterMovement()->IsMovingOnGround();
bool bIsFalling = GetCharacterMovement()->IsFalling();
bool bIsSwimming = GetCharacterMovement()->IsSwimming();
bool bIsFlying = GetCharacterMovement()->IsFlying();

// Set movement mode manually
GetCharacterMovement()->SetMovementMode(MOVE_Flying);

// React to mode changes
void AMyCharacter::OnMovementModeChanged(EMovementMode PrevMovementMode, uint8 PreviousCustomMode)
{
    Super::OnMovementModeChanged(PrevMovementMode, PreviousCustomMode);

    if (GetCharacterMovement()->MovementMode == MOVE_Falling)
    {
        // Started falling
    }
    else if (PrevMovementMode == MOVE_Falling)
    {
        // Landed
    }
}

Movement Configuration

Here are all the configuration parameters for each movement mode. I keep these as a reference because tuning movement feel is 90% tweaking these values.

Walking configuration:

cpp
GetCharacterMovement()->MaxWalkSpeed = 600.0f;
GetCharacterMovement()->MaxWalkSpeedCrouched = 300.0f;
GetCharacterMovement()->MinAnalogWalkSpeed = 20.0f;
GetCharacterMovement()->GroundFriction = 8.0f;
GetCharacterMovement()->BrakingDecelerationWalking = 2048.0f;
GetCharacterMovement()->MaxAcceleration = 2048.0f;
GetCharacterMovement()->MaxStepHeight = 45.0f;
GetCharacterMovement()->PerchRadiusThreshold = 0.0f;
GetCharacterMovement()->SetWalkableFloorAngle(44.0f);

Falling configuration:

cpp
GetCharacterMovement()->JumpZVelocity = 600.0f;
GetCharacterMovement()->AirControl = 0.2f;
GetCharacterMovement()->AirControlBoostMultiplier = 2.0f;
GetCharacterMovement()->AirControlBoostVelocityThreshold = 25.0f;
GetCharacterMovement()->FallingLateralFriction = 0.0f;
GetCharacterMovement()->BrakingDecelerationFalling = 0.0f;

Swimming configuration:

cpp
GetCharacterMovement()->MaxSwimSpeed = 300.0f;
GetCharacterMovement()->BrakingDecelerationSwimming = 0.0f;
GetCharacterMovement()->Buoyancy = 1.0f;
GetCharacterMovement()->JumpOutOfWaterPitch = 11.25f;

Flying configuration:

cpp
GetCharacterMovement()->NavAgentProps.bCanFly = true; // Enable flying
GetCharacterMovement()->MaxFlySpeed = 600.0f;
GetCharacterMovement()->BrakingDecelerationFlying = 0.0f;

Rotation configuration:

cpp
GetCharacterMovement()->bOrientRotationToMovement = true;
GetCharacterMovement()->bUseControllerDesiredRotation = false;
GetCharacterMovement()->RotationRate = FRotator(0.0f, 720.0f, 0.0f);

Controllers: The Brain Behind Your Pawns

Controllers are non-physical Actors that possess and control Pawns. The separation of control logic (Controller) from physical representation (Pawn) enables several key scenarios: controllers persist while Pawns are transient, controller switching between different Pawns, centralized player/AI state management, and clean separation of input processing and physics.

Controller Class Hierarchy

plaintext
AController (Base controller)
    ├── APlayerController (Human player input)
    └── AAIController (AI-driven behavior)

PlayerController

Responsibilities:

  1. Input Processing: Converts hardware input into game actions via input stack
  2. Camera Management: Controls player viewport and camera
  3. UI Interaction: Manages HUD and menu systems
  4. Network Communication: Client-server input transmission
  5. Player State: References replicated player data (APlayerState)

Basic setup:

cpp
void AMyPlayerController::BeginPlay()
{
    Super::BeginPlay();

    // Enable input
    bShowMouseCursor = false;
    bEnableClickEvents = false;
    bEnableMouseOverEvents = false;

    // Possess default pawn
    if (GetPawn())
    {
        Possess(GetPawn());
    }
}

AIController

Responsibilities:

  1. AI Behavior: Executes behavior trees and AI logic
  2. Pathfinding: Navigation mesh integration
  3. Perception: AI perception system integration (sight, hearing)
  4. Decision Making: Blackboard-based decision trees

Key differences from PlayerController:

Basic setup:

cpp
AAIController* AIController = GetWorld()->SpawnActor<AAIController>();
AIController->Possess(MyPawn);

// Start behavior tree
if (BehaviorTreeAsset)
{
    AIController->RunBehaviorTree(BehaviorTreeAsset);
}

Control Rotation vs Pawn Rotation

This one trips up a lot of beginners. Control Rotation (owned by Controller) represents viewing/aiming direction, is independent from Pawn's physical rotation, uses full 3D rotation (pitch, yaw, roll), and is accessed via GetControlRotation().

Pawn Rotation is the physical orientation of character mesh, determines collision boundaries, may differ significantly from aim direction, and is accessed via GetActorRotation().

Typical configuration:

cpp
// Third-person shooter: Character body follows movement, camera looks independently
bUseControllerRotationPitch = false;
bUseControllerRotationYaw = false;
bUseControllerRotationRoll = false;
GetCharacterMovement()->bOrientRotationToMovement = true; // Body faces movement direction

// First-person: Character rotates with camera
bUseControllerRotationPitch = true;
bUseControllerRotationYaw = true;
bUseControllerRotationRoll = false;
GetCharacterMovement()->bOrientRotationToMovement = false;

Input Stack Architecture

PlayerController manages an input stack that prioritizes input processing:

plaintext
Input Hardware
    ↓
PlayerController::BuildInputStack()
    ↓
Priority-Ordered InputComponents
    ↓
Higher priority can consume input
    ↓
Lower priority receives unconsumed input

Use cases:

Enhanced Input System Unreal - The Modern Way to Handle Input

Here's what actually matters - if you're starting a new project in UE 5.1+, don't use the legacy input system. The Enhanced Input System Unreal is the default now, and it's objectively better.

Enhanced Input is Unreal Engine 5's modern input system, replacing the legacy Action/Axis Mapping system. It provides object-oriented Input Actions (IA - Input Action is a data asset defining what actions players can perform) and Mapping Contexts (IMC - Input Mapping Context is a hierarchical mapping of physical inputs to Input Actions), per-player input customization, advanced input processing through modifiers (which transform raw input like dead zones, negation, swizzling, scaling) and triggers (which determine when actions fire like down, hold, tap, chorded actions, combos), and runtime input remapping.

Status: Default in UE 5.1+, legacy system deprecated

Core Concepts

Input Actions (IA): Data assets defining what actions players can perform

Input Mapping Contexts (IMC): Hierarchical mappings of physical inputs to Input Actions

Input Modifiers: Transform raw input (dead zones, negation, swizzling, scaling)

Input Triggers: Determine when actions fire (down, hold, tap, chorded actions, combos)

Setting Up Enhanced Input System in Your Project

Module Dependencies (.Build.cs)

First, add the EnhancedInput module to your project's Build.cs file:

csharp
PublicDependencyModuleNames.AddRange(new string[]
{
    "Core",
    "CoreUObject",
    "Engine",
    "InputCore",
    "EnhancedInput"  // Essential for Enhanced Input
});

Project Settings

Navigate to Edit > Project Settings > Engine > Input > Default Classes:

Character Header Setup

cpp
#include "EnhancedInputComponent.h"
#include "EnhancedInputSubsystems.h"
#include "InputActionValue.h"

// Forward declarations
class UInputMappingContext;
class UInputAction;

UCLASS()
class AMyCharacter : public ACharacter
{
    GENERATED_BODY()

public:
    // Input Actions
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
    UInputAction* MoveAction;

    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
    UInputAction* LookAction;

    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
    UInputAction* JumpAction;

    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
    UInputAction* SprintAction;

    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
    UInputAction* CrouchAction;

    // Mapping Context
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
    UInputMappingContext* DefaultMappingContext;

protected:
    virtual void BeginPlay() override;
    virtual void SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) override;

    // Callback functions
    void Move(const FInputActionValue& Value);
    void Look(const FInputActionValue& Value);
    void Sprint(const FInputActionValue& Value);

private:
    UPROPERTY(EditAnywhere, Category = "Movement")
    float WalkSpeed = 600.0f;

    UPROPERTY(EditAnywhere, Category = "Movement")
    float SprintSpeed = 1000.0f;
};

BeginPlay Implementation

cpp
void AMyCharacter::BeginPlay()
{
    Super::BeginPlay();

    // Register mapping context with Enhanced Input subsystem
    if (APlayerController* PlayerController = Cast<APlayerController>(GetController()))
    {
        if (UEnhancedInputLocalPlayerSubsystem* Subsystem =
            ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(
                PlayerController->GetLocalPlayer()))
        {
            // Clear existing mappings (optional)
            Subsystem->ClearAllMappings();

            // Add mapping context with priority (0 = highest)
            Subsystem->AddMappingContext(DefaultMappingContext, 0);
        }
    }
}

SetupPlayerInputComponent Implementation

cpp
void AMyCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
    Super::SetupPlayerInputComponent(PlayerInputComponent);

    // Cast to Enhanced Input Component
    if (UEnhancedInputComponent* EnhancedInputComponent =
        CastChecked<UEnhancedInputComponent>(PlayerInputComponent))
    {
        // Bind Movement (Triggered = continuous)
        EnhancedInputComponent->BindAction(MoveAction,
            ETriggerEvent::Triggered, this, &AMyCharacter::Move);

        // Bind Camera Look (Triggered = continuous)
        EnhancedInputComponent->BindAction(LookAction,
            ETriggerEvent::Triggered, this, &AMyCharacter::Look);

        // Bind Jump (Started/Completed = one-time events)
        EnhancedInputComponent->BindAction(JumpAction,
            ETriggerEvent::Started, this, &ACharacter::Jump);
        EnhancedInputComponent->BindAction(JumpAction,
            ETriggerEvent::Completed, this, &ACharacter::StopJumping);

        // Bind Sprint (Triggered = continuous)
        EnhancedInputComponent->BindAction(SprintAction,
            ETriggerEvent::Triggered, this, &AMyCharacter::Sprint);

        // Bind Crouch (Started/Completed)
        EnhancedInputComponent->BindAction(CrouchAction,
            ETriggerEvent::Started, this, &ACharacter::Crouch);
        EnhancedInputComponent->BindAction(CrouchAction,
            ETriggerEvent::Completed, this, &ACharacter::UnCrouch);
    }
}

Callback Function Implementations

cpp
void AMyCharacter::Move(const FInputActionValue& Value)
{
    // Extract 2D movement vector
    const FVector2D MovementVector = Value.Get<FVector2D>();

    if (Controller != nullptr)
    {
        // Get control rotation (camera direction)
        const FRotator Rotation = Controller->GetControlRotation();
        const FRotator YawRotation(0, Rotation.Yaw, 0);

        // Calculate forward and right directions
        const FVector ForwardDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
        const FVector RightDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);

        // Add movement input
        AddMovementInput(ForwardDirection, MovementVector.Y); // W/S
        AddMovementInput(RightDirection, MovementVector.X);   // A/D
    }
}

void AMyCharacter::Look(const FInputActionValue& Value)
{
    // Extract 2D look vector
    const FVector2D LookAxisVector = Value.Get<FVector2D>();

    if (Controller != nullptr)
    {
        // Add yaw (horizontal) and pitch (vertical) input
        AddControllerYawInput(LookAxisVector.X);
        AddControllerPitchInput(LookAxisVector.Y);
    }
}

void AMyCharacter::Sprint(const FInputActionValue& Value)
{
    bool bIsSprinting = Value.Get<bool>();
    GetCharacterMovement()->MaxWalkSpeed = bIsSprinting ? SprintSpeed : WalkSpeed;
}

Input Modifiers

Common modifiers:

Dead Zone: Filters small stick movements

plaintext
Configuration in Input Mapping Context:
- Lower Threshold: 0.2
- Upper Threshold: 1.0
- Type: Scaled

Negate: Inverts input per axis

plaintext
Use case: A key for left movement (negative X)

Swizzle: Reorders axis components (Swizzle is a modifier that reorders the X, Y, Z components of input, like turning X into Y)

plaintext
Use case: W key becomes positive Y (forward) instead of positive X
Configuration: Order = YXZ

Scalar: Multiplies input value

plaintext
Use case: Mouse sensitivity adjustment
Configuration: Scalar = FVector(2.0, 2.0, 1.0)

WASD configuration example:

Input Triggers

Common triggers:

Down: Fires continuously while held

plaintext
Use case: Automatic weapon fire

Pressed: Single activation on initial press

plaintext
Use case: Single-shot weapon

Released: Fires when input is released

plaintext
Use case: Charge-and-release mechanics

Hold: Requires sustained input

plaintext
Use case: Heavy attack (hold 0.5 seconds)
Configuration: Hold Time Threshold = 0.5

Tap: Quick press-release

plaintext
Use case: Light attack (release within 0.2 seconds)
Configuration: Release Time Threshold = 0.2

Chorded Action: Requires another action (Chorded Action is a trigger that requires another action to be active simultaneously, like Shift+Click)

plaintext
Use case: Shift+Click for special ability
Configuration: Chord Action = ShiftAction

Character Movement Component UE5: The Engine That Powers Your Characters

UCharacterMovementComponent is the sophisticated movement system for ACharacter. It's one of the most complex components in Unreal Engine, providing five built-in movement modes, physics integration with collision, gravity, and friction, network replication with client-side prediction, extensive configuration parameters, and customization support for game-specific movement.

Architecture

Tick flow:

plaintext
TickComponent()
    ↓
PerformMovement()
    ↓
Calculate Acceleration from Input
    ↓
PhysWalking() / PhysFalling() / PhysSwimming() / PhysFlying() / PhysCustom()
    ↓
Update Velocity & Position
    ↓
Update Character Location

How Movement Modes Work (And How to Add Custom Ones)

Movement Modes

cpp
// Check current mode
EMovementMode Mode = GetCharacterMovement()->MovementMode;

// Mode-specific checks
bool bIsWalking = GetCharacterMovement()->IsMovingOnGround();
bool bIsFalling = GetCharacterMovement()->IsFalling();
bool bIsSwimming = GetCharacterMovement()->IsSwimming();
bool bIsFlying = GetCharacterMovement()->IsFlying();

// Set mode programmatically
GetCharacterMovement()->SetMovementMode(MOVE_Flying);

// Custom mode with sub-mode
GetCharacterMovement()->SetMovementMode(MOVE_Custom, CustomSubMode);

Configuration Parameters

Walking configuration:

cpp
// Speed limits
MaxWalkSpeed = 600.0f;
MaxWalkSpeedCrouched = 300.0f;
MinAnalogWalkSpeed = 20.0f;

// Acceleration & Deceleration
MaxAcceleration = 2048.0f;
BrakingDecelerationWalking = 2000.0f;
BrakingFriction = 0.0f;
GroundFriction = 8.0f;

// Step & Slope
MaxStepHeight = 45.0f;
SetWalkableFloorAngle(44.0f);
PerchRadiusThreshold = 0.0f;

Jumping & falling configuration:

cpp
// Jump
JumpZVelocity = 600.0f;
JumpMaxCount = 1;

// Air Control
AirControl = 0.2f;
AirControlBoostMultiplier = 2.0f;
AirControlBoostVelocityThreshold = 25.0f;
FallingLateralFriction = 0.0f;

// Gravity
GravityScale = 1.0f;
BrakingDecelerationFalling = 0.0f;

Swimming configuration:

cpp
MaxSwimSpeed = 300.0f;
BrakingDecelerationSwimming = 0.0f;
Buoyancy = 1.0f;
JumpOutOfWaterPitch = 11.25f;

Flying configuration:

cpp
NavAgentProps.bCanFly = true; // Enable flying
MaxFlySpeed = 600.0f;
BrakingDecelerationFlying = 0.0f;

Rotation configuration:

cpp
bOrientRotationToMovement = true;
bUseControllerDesiredRotation = false;
RotationRate = FRotator(0.0f, 720.0f, 0.0f);

Custom Movement Implementation

When you need movement types beyond the built-in modes (like wall-running, climbing, or sliding), you implement custom movement by extending UCharacterMovementComponent.

PhysCustom override:

cpp
void UMyCharacterMovement::PhysCustom(float DeltaTime, int32 Iterations)
{
    Super::PhysCustom(DeltaTime, Iterations);

    if (CustomMovementMode == ECustomMovementMode::Climbing)
    {
        PhysClimbing(DeltaTime, Iterations);
    }
}

void UMyCharacterMovement::PhysClimbing(float DeltaTime, int32 Iterations)
{
    // Custom climbing physics
    FVector InputDirection = Acceleration.GetSafeNormal();
    Velocity = InputDirection * ClimbSpeed;

    FHitResult Hit;
    FVector Delta = Velocity * DeltaTime;
    SafeMoveUpdatedComponent(Delta, UpdatedComponent->GetComponentQuat(), true, Hit);
}

GetMaxSpeed override:

cpp
float UMyCharacterMovement::GetMaxSpeed() const
{
    if (MovementMode == MOVE_Custom && CustomMovementMode == ECustomMovementMode::Climbing)
    {
        return ClimbSpeed;
    }

    return Super::GetMaxSpeed();
}

Connecting Your Character to Animation Blueprints

Animation Blueprints control character animations by reading data from the Character and applying it to animation logic. The integration uses custom AnimInstance classes extending UAnimInstance.

AnimInstance Class Structure

cpp
#include "Animation/AnimInstance.h"

UCLASS()
class UMyAnimInstance : public UAnimInstance
{
    GENERATED_BODY()

public:
    virtual void NativeInitializeAnimation() override;
    virtual void NativeUpdateAnimation(float DeltaTimeX) override;

protected:
    // Cached character reference
    UPROPERTY(BlueprintReadOnly, Category = "Character")
    AMyCharacter* OwningCharacter;

    // Animation variables
    UPROPERTY(BlueprintReadOnly, Category = "Movement")
    float Speed;

    UPROPERTY(BlueprintReadOnly, Category = "Movement")
    float Direction;

    UPROPERTY(BlueprintReadOnly, Category = "Movement")
    bool bIsInAir;

    UPROPERTY(BlueprintReadOnly, Category = "Movement")
    bool bIsCrouching;
};

Implementation

cpp
void UMyAnimInstance::NativeInitializeAnimation()
{
    Super::NativeInitializeAnimation();

    // Cache character reference
    OwningCharacter = Cast<AMyCharacter>(TryGetPawnOwner());
}

void UMyAnimInstance::NativeUpdateAnimation(float DeltaTimeX)
{
    Super::NativeUpdateAnimation(DeltaTimeX);

    if (OwningCharacter)
    {
        // Calculate speed from velocity
        FVector Velocity = OwningCharacter->GetVelocity();
        Speed = Velocity.Size2D(); // 2D speed (XY plane)

        // Calculate direction relative to actor rotation
        if (Speed > 0.0f)
        {
            Direction = CalculateDirection(Velocity, OwningCharacter->GetActorRotation());
        }

        // Update state flags
        bIsInAir = OwningCharacter->GetMovementComponent()->IsFalling();
        bIsCrouching = OwningCharacter->bIsCrouched;
    }
}

Setting Animation Blueprint on Character

cpp
AMyCharacter::AMyCharacter()
{
    // Assign animation blueprint class
    GetMesh()->SetAnimationMode(EAnimationMode::AnimationBlueprint);

    static ConstructorHelpers::FClassFinder<UAnimInstance> AnimBP(
        TEXT("/Game/Characters/Animations/MyAnimBP")
    );
    if (AnimBP.Succeeded())
    {
        GetMesh()->SetAnimInstanceClass(AnimBP.Class);
    }
}

Blend Space Integration

Blend spaces blend between animations based on two input parameters.

Locomotion blend space setup:

Animation placement:

Blueprint implementation:

State Machine Integration

Common states:

Transition rules:

plaintext
Idle → Walk: Speed > 10.0
Walk → Idle: Speed <= 10.0
Walk → Jump: bIsInAir == true
Jump → Land: bIsInAir == false AND bWasInAir == true

Root Motion

Root motion allows animations to drive character movement.

Setup steps:

  1. Enable in Animation Sequence:

    • Open animation asset
    • Enable "Enable Root Motion" in Asset Details
  2. Configure Animation Blueprint:

    • Set Root Motion Mode to "Root Motion from Everything"
  3. Character Movement Component:

    • Automatically applies root motion when detected
    • Physics integration handled automatically

Network replication:

Network Replication: Making Multiplayer Characters Work

Character Movement Replication

Understanding game character implementation in multiplayer requires knowing the client-server architecture.

Autonomous Proxy (locally controlled character on client):

Authority (server):

Simulated Proxy (other players' characters):

FSavedMove_Character

Movement data structure for network transmission:

cpp
struct FSavedMove_Character
{
    // Core data
    float TimeStamp;
    float DeltaTime;
    FVector SavedLocation;
    FRotator SavedRotation;
    FVector Acceleration;

    // Movement state
    uint8 CompressedFlags;
    EMovementMode MovementMode;

    // Methods
    void Clear();
    void SetMoveFor(ACharacter* C, float InDeltaTime, FVector const& NewAccel);
    bool CanCombineWith(const FSavedMove_Character* NewMove);
};

Prediction and Correction

Client prediction flow:

plaintext
1. Client executes move locally
2. Client saves move to buffer
3. Client sends move to server
4. Server validates and responds
5. If correction received:
   - Find correction point in buffer
   - Discard older moves
   - Replay newer moves from corrected position

Server validation:

cpp
// Server compares positions
FVector ServerLocation = CalculatedLocation;
FVector ClientLocation = ReceivedLocation;
float Error = (ServerLocation - ClientLocation).Size();

if (Error > ErrorTolerance || TimeSinceLastResponse > MaxResponseTime)
{
    // Send correction to client
    ClientAdjustPosition(...);
}

Network Smoothing

cpp
// Configuration
GetCharacterMovement()->NetworkSimulatedSmoothLocationTime = 0.100f;
GetCharacterMovement()->NetworkSimulatedSmoothRotationTime = 0.050f;

Smoothing interpolates position corrections rather than snapping, reducing visual artifacts.

Custom Movement Replication

To replicate custom movement state:

1. Extend FSavedMove_Character:

cpp
struct FSavedMove_MyCharacter : public FSavedMove_Character
{
    uint8 SavedCustomFlags;

    virtual void Clear() override
    {
        Super::Clear();
        SavedCustomFlags = 0;
    }

    virtual uint8 GetCompressedFlags() const override
    {
        uint8 Result = Super::GetCompressedFlags();

        if (bCustomFlag1) Result |= FLAG_Custom_0;
        if (bCustomFlag2) Result |= FLAG_Custom_1;

        return Result;
    }
};

2. Override AllocateNewMove():

cpp
virtual FSavedMovePtr UMyCharacterMovement::AllocateNewMove()
{
    return FSavedMovePtr(new FSavedMove_MyCharacter());
}

3. Update From Compressed Flags:

cpp
virtual void UMyCharacterMovement::UpdateFromCompressedFlags(uint8 Flags)
{
    Super::UpdateFromCompressedFlags(Flags);

    bCustomFlag1 = (Flags & FLAG_Custom_0) != 0;
    bCustomFlag2 = (Flags & FLAG_Custom_1) != 0;
}

Spawning Actors the Smart Way (Including Object Pooling)

Basic Spawning

cpp
FVector Location = FVector(0, 0, 100);
FRotator Rotation = FRotator::ZeroRotator;

// Template version (preferred)
AMyActor* SpawnedActor = GetWorld()->SpawnActor<AMyActor>(Location, Rotation);

// With class pointer
UClass* ActorClass = AMyActor::StaticClass();
AActor* SpawnedActor = GetWorld()->SpawnActor(ActorClass, &Location, &Rotation);

FActorSpawnParameters

cpp
FActorSpawnParameters SpawnParams;
SpawnParams.Owner = this;
SpawnParams.Instigator = GetInstigator();
SpawnParams.Name = FName(TEXT("MyUniqueActorName"));
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;

AMyActor* SpawnedActor = GetWorld()->SpawnActor<AMyActor>(
    AMyActor::StaticClass(),
    Location,
    Rotation,
    SpawnParams
);

Spawn Collision Handling

ESpawnActorCollisionHandlingMethod options:

  1. AlwaysSpawn: Ignore all collision, spawn at exact location
  2. AdjustIfPossibleButDontSpawnIfColliding: Try adjusting position, fail if impossible
  3. DontSpawnIfColliding: Fail immediately if any collision detected
  4. Undefined: Use actor class default

Best practices:

Deferred Construction

Deferred spawning allows property modification before BeginPlay:

cpp
FTransform SpawnTransform;
SpawnTransform.SetLocation(GetActorLocation());
SpawnTransform.SetRotation(GetActorRotation().Quaternion());

// Spawn deferred - actor exists but not fully initialized
AMyActor* DeferredActor = GetWorld()->SpawnActorDeferred<AMyActor>(
    AMyActor::StaticClass(),
    SpawnTransform,
    this,  // Owner
    GetInstigator(),
    ESpawnActorCollisionHandlingMethod::AlwaysSpawn
);

if (DeferredActor)
{
    // Modify properties before BeginPlay
    DeferredActor->CustomProperty = CalculatedValue;
    DeferredActor->InitialHealth = MaxHealth;

    // Access components
    UStaticMeshComponent* Mesh = DeferredActor->FindComponentByClass<UStaticMeshComponent>();
    if (Mesh)
    {
        Mesh->SetMaterial(0, CustomMaterial);
    }

    // Complete spawning - triggers BeginPlay
    UGameplayStatics::FinishSpawningActor(DeferredActor, SpawnTransform);
}

When to use deferred spawning:

Object Pooling

For high-frequency spawning (projectiles, particles), use object pooling:

cpp
class AActorPool : public AActor
{
public:
    void InitializePool(TSubclassOf<AActor> ActorClass, int32 PoolSize)
    {
        for (int32 i = 0; i < PoolSize; i++)
        {
            AActor* PooledActor = GetWorld()->SpawnActor<AActor>(ActorClass);
            PooledActor->SetActorHiddenInGame(true);
            PooledActor->SetActorEnableCollision(false);
            PooledActor->SetActorTickEnabled(false);
            Pool.Add(PooledActor);
        }
    }

    AActor* GetFromPool()
    {
        for (AActor* Actor : Pool)
        {
            if (!Actor->IsActorTickEnabled())
            {
                Actor->SetActorHiddenInGame(false);
                Actor->SetActorEnableCollision(true);
                Actor->SetActorTickEnabled(true);
                return Actor;
            }
        }
        return nullptr; // Pool exhausted
    }

    void ReturnToPool(AActor* Actor)
    {
        Actor->SetActorHiddenInGame(true);
        Actor->SetActorEnableCollision(false);
        Actor->SetActorTickEnabled(false);
    }

private:
    TArray<AActor*> Pool;
};

Performance impact:

Ten Mistakes I Made So You Don't Have To

1. Constructor vs BeginPlay Confusion

Problem: Placing gameplay logic in constructor where it doesn't execute as expected.

Solution:

cpp
// CORRECT: Constructor for defaults only
AMyActor::AMyActor()
{
    PrimaryActorTick.bCanEverTick = true;
    RootComponent = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
    Health = 100.0f; // Default value
}

// CORRECT: BeginPlay for gameplay logic
void AMyActor::BeginPlay()
{
    Super::BeginPlay();

    // Access other actors
    TArray<AActor*> Enemies;
    UGameplayStatics::GetAllActorsOfClass(this, AEnemy::StaticClass(), Enemies);
}

// WRONG: Gameplay logic in constructor
AMyActor::AMyActor()
{
    // This won't work - other actors don't exist yet
    TArray<AActor*> Enemies;
    UGameplayStatics::GetAllActorsOfClass(this, AEnemy::StaticClass(), Enemies);
}

2. Tick Performance

Problem: Tick enabled by default on Blueprint actors, causing massive CPU overhead.

Solution:

cpp
// Disable tick in C++
AMyActor::AMyActor()
{
    PrimaryActorTick.bCanEverTick = false;
}

// Use event-driven design
GetWorldTimerManager().SetTimer(TimerHandle, this, &AMyActor::UpdateFunction, 0.1f, true);

// Or conditional tick
void AMyActor::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);

    // Only tick if recently rendered
    if (GetWorld()->GetTimeSeconds() - GetLastRenderTime() > 0.1f)
    {
        return; // Skip tick for off-screen actors
    }
}

3. Missing Replication Setup

Problem: Forgetting to set bReplicates = true or missing GetLifetimeReplicatedProps().

Solution:

cpp
AMyActor::AMyActor()
{
    bReplicates = true; // CRITICAL
    NetUpdateFrequency = 10.0f;
}

void AMyActor::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
    Super::GetLifetimeReplicatedProps(OutLifetimeProps); // MUST CALL SUPER

    DOREPLIFETIME(AMyActor, Health);
}

4. EndPlay Not Overridden

Problem: Memory leaks from timers, delegates, or references not cleaned up.

Solution:

cpp
void AMyActor::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
    // Clear timers
    GetWorldTimerManager().ClearAllTimersForObject(this);

    // Unbind delegates
    OnDamageDelegate.Clear();

    // Release references
    CachedActor = nullptr;

    Super::EndPlay(EndPlayReason); // Call Super last
}

5. Pawn Not Auto-Possessed

Problem: Spawning pawns at runtime that don't receive input.

Solution:

cpp
// Option 1: Auto-possess settings
AMyPawn::AMyPawn()
{
    AutoPossessPlayer = EAutoReceiveInput::Player0;
}

// Option 2: Manual possession after spawn
AMyPawn* SpawnedPawn = GetWorld()->SpawnActor<AMyPawn>(Location, Rotation);
APlayerController* PC = GetWorld()->GetFirstPlayerController();
PC->Possess(SpawnedPawn);

6. Component Initialization Order

Problem: Components invalid when accessed in BeginPlay.

Solution:

cpp
// Use PostInitializeComponents for component-dependent setup
void AMyActor::PostInitializeComponents()
{
    Super::PostInitializeComponents();

    // Components are guaranteed to be initialized here
    if (MeshComponent)
    {
        MeshComponent->SetMaterial(0, CustomMaterial);
    }
}

// Or cache in BeginPlay with null check
void AMyActor::BeginPlay()
{
    Super::BeginPlay();

    CachedMesh = FindComponentByClass<UStaticMeshComponent>();
    if (CachedMesh)
    {
        // Safe to use
    }
}

7. Choosing Wrong Base Class

Problem: Using Character for non-humanoid entities or Actor when control is needed.

Decision guide:

plaintext
Static/passive object? → AActor
Controllable non-humanoid? → APawn
Bipedal character? → ACharacter

8. Enhanced Input Without UPROPERTY

Problem: Input Actions garbage collected, causing crashes.

Solution:

cpp
// CORRECT: UPROPERTY prevents GC
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
UInputAction* MoveAction;

// WRONG: No UPROPERTY = garbage collection
UInputAction* MoveAction; // Will be collected and crash

9. Root Motion Not Working

Problem: Root motion works in preview but not in PIE (Play In Editor - the play mode inside the Unreal Editor).

Solution:

  1. Enable "Enable Root Motion" on Animation Sequence
  2. Set Animation Blueprint Root Motion Mode to "Root Motion from Everything"
  3. Ensure skeleton has proper root bone
  4. Verify Character Movement Component is configured correctly

10. Network Prediction Errors

Problem: Client-server desync or rubber-banding (rubber-banding is when your character teleports backward due to server correction).

Solution:

cpp
// Tune error thresholds
GetCharacterMovement()->NetworkMaxSmoothUpdateDistance = 256.0f;
GetCharacterMovement()->NetworkNoSmoothUpdateDistance = 512.0f;

// Adjust correction timing
GetCharacterMovement()->NetworkSimulatedSmoothLocationTime = 0.100f;

// For custom movement, extend FSavedMove_Character
// and override GetCompressedFlags() / UpdateFromCompressedFlags()

Wrapping Up: Building Characters That Actually Work

Here's the thing - understanding Unreal Engine Actor Pawn Character classes is the foundation of everything you'll build in Unreal. Get the architecture right from the start and you'll save yourself countless hours of refactoring later.

The hierarchy exists for good reasons. AActor gives you a transform and components. APawn adds controller possession and input handling. ACharacter provides the full bipedal movement system with animation support. Choose the right base class for your needs and the engine does half your work for you.

The lifecycle matters. Constructor for defaults only. PostInitializeComponents when components are ready. BeginPlay for gameplay logic. Tick sparingly (or not at all). EndPlay for cleanup. Put code in the wrong place and you'll chase phantom bugs for days.

The Enhanced Input System Unreal is the modern standard. If you're starting a new project, use it. The Character Movement Component UE5 handles walking, jumping, crouching, swimming, and flying with built-in network replication. Extend it for custom movement types instead of building from scratch.

For multiplayer games, understand the client-server architecture. Autonomous proxy predicts locally. Server validates and corrects. Simulated proxy interpolates smoothly. Get replication right early because retrofitting it later is painful.

Start simple. Build a basic character with movement and input first. Add complexity incrementally. Test multiplayer early if that's your target. Profile performance regularly. Disable tick on every actor that doesn't absolutely need it.

You've got the knowledge now. Time to build something.

Common Questions

What is the difference between AActor, APawn, and ACharacter in Unreal Engine?

AActor is the base class for all objects that can exist in a level with a 3D transform. APawn extends Actor by adding controller possession and input handling - it can be controlled by players or AI but has no built-in movement. ACharacter extends Pawn specifically for bipedal humanoid entities and includes a pre-configured capsule collider, skeletal mesh, and Character Movement Component with walking, jumping, crouching, swimming, and flying built-in.

When should I use AActor vs APawn vs ACharacter for my game entity?

Use AActor for environmental objects, props, triggers, and static scenery that don't need controller input. Use APawn for vehicles, drones, RTS units, top-down game characters, and any controllable entity that doesn't need bipedal movement. Use ACharacter for player characters, humanoid NPCs, and any entity that needs the full suite of bipedal movement capabilities with animation support.

How does the Controller-Pawn possession system work in Unreal Engine?

Controllers (PlayerController or AIController) possess Pawns to control them. The Controller is persistent and represents the player or AI identity, while Pawns are transient bodies they inhabit. When a Controller possesses a Pawn, it calls PossessedBy() on the server, sets up the input component, and establishes the ownership relationship. This separation enables scenarios like character death/respawn where the Controller unpossesses the dead pawn and possesses a new one without losing player state.

What is the Enhanced Input System and why should I use it?

The Enhanced Input System is Unreal Engine 5's modern input architecture that replaced the legacy Action/Axis system. It uses object-oriented Input Actions (defining what actions exist) and Input Mapping Contexts (defining how physical inputs map to actions). It provides per-player input customization, advanced input modifiers (dead zones, scaling, swizzling), input triggers (hold, tap, chorded actions), and runtime remapping. It's the default in UE 5.1+ and the legacy system is deprecated.

How do I set up network replication for my Actor or Character?

Set bReplicates = true in the constructor. Implement GetLifetimeReplicatedProps() and call Super::GetLifetimeReplicatedProps() first. Use DOREPLIFETIME() macros to register properties for replication. Use DOREPLIFETIME_CONDITION() for conditional replication like owner-only properties. For replicated properties that need client-side reactions, use ReplicatedUsing=OnRep_FunctionName and implement the UFUNCTION() void OnRep_FunctionName() callback.

What is the Actor lifecycle and why does it matter?

The Actor lifecycle is the sequence of initialization and cleanup methods: Constructor → PostActorCreated → OnConstruction → PreInitializeComponents → InitializeComponent → PostInitializeComponents → BeginPlay → Tick → EndPlay → BeginDestroy → Garbage Collection. Understanding this matters because placing code in the wrong phase causes bugs. Put defaults in Constructor, component setup in PostInitializeComponents, gameplay logic in BeginPlay, per-frame updates in Tick, and cleanup in EndPlay.

How does the Character Movement Component handle multiplayer movement?

The Character Movement Component uses client-side prediction with server validation. On the client (Autonomous Proxy), movement executes locally for responsive controls, saves moves to a buffer, and sends move data to the server. The server validates moves, reproduces them, and sends corrections when the client's position differs from the server's beyond a threshold. The client replays moves from the correction point. Other players' characters (Simulated Proxies) receive position updates and interpolate smoothly.

What are Input Modifiers and Input Triggers in the Enhanced Input System?

Input Modifiers transform raw input before it reaches your code. Common modifiers include Dead Zone (filters small joystick movements), Negate (inverts input), Swizzle (reorders X/Y/Z components), and Scalar (multiplies input values). Input Triggers determine when input actions fire. Examples include Down (continuous while held), Pressed (single activation), Hold (requires sustained input for threshold time), Tap (quick press-release), and Chorded Action (requires another action simultaneously).

How do I implement custom movement modes in Unreal Engine?

Create a custom UCharacterMovementComponent subclass. Override PhysCustom() to handle your custom movement logic. Call SetMovementMode(MOVE_Custom, YourCustomSubMode) to activate it. Override GetMaxSpeed() to return appropriate speed for your custom mode. For network replication, extend FSavedMove_Character, override GetCompressedFlags() to pack your custom state, override UpdateFromCompressedFlags() to unpack it, and override AllocateNewMove() to return your custom saved move structure.

Why is my Blueprint actor causing performance problems?

Blueprint actors have tick enabled by default, and empty Blueprint ticks are expensive. 100-150 empty Blueprint ticks consume about 1ms CPU time on consoles. Disable tick on actors that don't need per-frame updates. Use event-driven design with timers (GetWorldTimerManager().SetTimer()) for periodic updates instead. Implement logic in C++ for performance-critical code. Use conditional tick that returns early for off-screen actors.

What is the difference between Control Rotation and Pawn Rotation?

Control Rotation is owned by the Controller and represents the viewing/aiming direction (where the player is looking). It's independent from the Pawn's physical rotation, uses full 3D rotation including pitch, and is accessed via GetControlRotation(). Pawn Rotation is the physical orientation of the character mesh, determines collision boundaries, and is accessed via GetActorRotation(). In third-person games, these often differ - the character faces the movement direction while the camera looks independently.

How do I properly clean up Actors to avoid memory leaks?

Override EndPlay() and perform all cleanup there before calling Super::EndPlay(). Clear all timers with GetWorldTimerManager().ClearAllTimersForObject(this). Unbind all delegates with .Clear() or .RemoveAll(). Set cached actor references to nullptr. Remove dynamic components. Epic recommends using EndPlay over destructors for gameplay cleanup because EndPlay is called reliably during level transitions, streaming unloads, and explicit Destroy() calls.