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
- 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 includes full bipedal movement with animation support
- The Character Movement Component UE5 system handles five built-in movement modes (Walking, Falling, Swimming, Flying, Custom) with client-side prediction and server validation for multiplayer games
- Enhanced Input System Unreal is the modern input architecture in UE5.1+ using object-oriented Input Actions and Mapping Contexts instead of the deprecated legacy system
- Understanding the Actor lifecycle (Constructor → PostInitializeComponents → BeginPlay → Tick → EndPlay) is critical because placing gameplay logic in the wrong phase causes bugs that take hours to debug
- Component-based architecture means Actors are containers for functionality - you compose features through components rather than building deep inheritance hierarchies
- The Controller-Pawn separation (where controllers persist while pawns are transient) enables scenarios like character death/respawn and vehicle entry/exit without losing player state
- Network replication uses autonomous proxy on clients for responsive controls, with saved move buffers and server corrections when discrepancies exceed error thresholds
- Tick is disabled by default in C++ but enabled in Blueprint actors - 100-150 empty Blueprint ticks consume about 1ms CPU time on consoles, making event-driven design essential for performance
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:
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:
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.
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:
- Use only for setting default primitive values
- Create default subobjects with
CreateDefaultSubobject<T>() - Never access other actors or world context
- Runs in both editor and runtime contexts
- Called when creating the Class Default Object (CDO)
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.
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.
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.
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:
- Disabled by default in C++ (
PrimaryActorTick.bCanEverTick = false) - Enabled by default in Blueprint actors (major performance pitfall)
- 100-150 empty Blueprint ticks = ~1ms CPU time on consoles
- Prefer event-driven design over tick when possible
- Use timers for periodic updates instead of 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.
void AMyActor::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
// Cleanup BEFORE calling Super
GetWorldTimerManager().ClearAllTimersForObject(this);
OnActorDestroyed.Clear();
Super::EndPlay(EndPlayReason);
}
EndPlay reasons:
Destroyed: Explicit Destroy() callLevelTransition: Switching to different levelEndPlayInEditor: PIE session endingRemovedFromWorld: Actor removed from worldQuit: Application closing
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
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.
void AMyActor::AddRuntimeComponent()
{
UStaticMeshComponent* DynamicMesh = NewObject<UStaticMeshComponent>(this);
DynamicMesh->RegisterComponent();
DynamicMesh->AttachToComponent(RootComponent, FAttachmentTransformRules::KeepRelativeTransform);
}
Finding Components
// 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
// 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
// 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:
None: Update physics normallyTeleportPhysics: Reset physics velocityResetPhysics: Reset all physics state
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
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
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
- Controller Possession System: Can be possessed/unpossessed by controllers
- Input Component: Built-in input handling via
SetupPlayerInputComponent() - Movement Input Accumulation:
AddMovementInput()system for collecting directional input - Controller Reference: Maintains reference to possessing controller
- Auto-Possession Settings: Automatic possession configuration
- 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
// Controller possesses pawn
AController* MyController = GetWorld()->GetFirstPlayerController();
MyController->Possess(MyPawn);
// Or unpossess
MyController->UnPossess();
Internal process:
- Controller checks current possession, calls
UnPossess()if needed - Controller calls
SetPawn(NewPawn) - Pawn's
PossessedBy(Controller)called (server-side) - Pawn's
SetOwner(Controller)called - Input component created and
SetupPlayerInputComponent()called (if PlayerController)
Lifecycle Callbacks
// 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
AMyPawn::AMyPawn()
{
// Auto-possess by player index
AutoPossessPlayer = EAutoReceiveInput::Player0;
// Auto-possess AI
AutoPossessAI = EAutoPossessAI::PlacedInWorldOrSpawned;
AIControllerClass = AMyAIController::StaticClass();
}
AutoPossessPlayer options:
Disabled: No auto-possessionPlayer0throughPlayer7: Auto-possess by player index
AutoPossessAI options:
Disabled: No auto-possessionPlacedInWorld: Only level-placed pawnsSpawned: Only dynamically spawned pawnsPlacedInWorldOrSpawned: Both scenarios
Handling Input the Unreal Way
Input handling happens in SetupPlayerInputComponent(), which is only called when possessed by a PlayerController (not AIController).
SetupPlayerInputComponent
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:
- Only called when possessed by PlayerController (not AIController)
- Called after possession completes
- Input bindings require matching entries in Project Settings → Input
Movement Input System
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:
- Accumulates input during the frame
- Input is normalized automatically
- Movement components consume accumulated input
- Input is cleared each frame after consumption
- Base Pawn does NOT automatically apply movement (requires movement component or custom logic)
Movement Components
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:
UFloatingPawnMovement: Simple flying/floating movementUCharacterMovementComponent: Advanced bipedal movement (Character class)URotatingMovement: Continuous rotation- Custom: Extend
UPawnMovementComponent
Controller Access
// 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
AMyPawn::AMyPawn()
{
// Control which rotation axes follow controller
bUseControllerRotationPitch = false;
bUseControllerRotationYaw = true;
bUseControllerRotationRoll = false;
}
Common patterns:
- First-person: All true (character rotates with camera)
- Third-person: Yaw only (character turns, but camera can look up/down independently)
- Flying vehicle: All false (independent vehicle orientation)
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
// Components created automatically in ACharacter constructor
UCapsuleComponent* CapsuleComponent; // Collision primitive (RootComponent)
USkeletalMeshComponent* Mesh; // Visual representation
UCharacterMovementComponent* CharacterMovement; // Movement logic
Accessing components:
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.
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
// 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:
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
// 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:
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:
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:
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:
GetCharacterMovement()->MaxSwimSpeed = 300.0f;
GetCharacterMovement()->BrakingDecelerationSwimming = 0.0f;
GetCharacterMovement()->Buoyancy = 1.0f;
GetCharacterMovement()->JumpOutOfWaterPitch = 11.25f;
Flying configuration:
GetCharacterMovement()->NavAgentProps.bCanFly = true; // Enable flying
GetCharacterMovement()->MaxFlySpeed = 600.0f;
GetCharacterMovement()->BrakingDecelerationFlying = 0.0f;
Rotation configuration:
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
AController (Base controller)
├── APlayerController (Human player input)
└── AAIController (AI-driven behavior)
PlayerController
Responsibilities:
- Input Processing: Converts hardware input into game actions via input stack
- Camera Management: Controls player viewport and camera
- UI Interaction: Manages HUD and menu systems
- Network Communication: Client-server input transmission
- Player State: References replicated player data (APlayerState)
Basic setup:
void AMyPlayerController::BeginPlay()
{
Super::BeginPlay();
// Enable input
bShowMouseCursor = false;
bEnableClickEvents = false;
bEnableMouseOverEvents = false;
// Possess default pawn
if (GetPawn())
{
Possess(GetPawn());
}
}
AIController
Responsibilities:
- AI Behavior: Executes behavior trees and AI logic
- Pathfinding: Navigation mesh integration
- Perception: AI perception system integration (sight, hearing)
- Decision Making: Blackboard-based decision trees
Key differences from PlayerController:
- Exists only on server in multiplayer
- No input processing (controlled by AI logic)
- Integrates with Behavior Trees and Blackboards
- Uses Environmental Query System (EQS)
Basic setup:
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:
// 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:
Input Hardware
↓
PlayerController::BuildInputStack()
↓
Priority-Ordered InputComponents
↓
Higher priority can consume input
↓
Lower priority receives unconsumed input
Use cases:
- UI menus consume all input, preventing gameplay interaction
- Debug console captures specific keys
- Context-sensitive controls override default bindings
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
- Digital (bool): On/off actions (jump, fire)
- Axis1D (float): Single-axis input (zoom, throttle)
- Axis2D (FVector2D): Two-axis input (WASD movement, mouse look)
- Axis3D (FVector): Three-dimensional input (flight controls)
Input Mapping Contexts (IMC): Hierarchical mappings of physical inputs to Input Actions
- Multiple contexts can be active simultaneously
- Priority system resolves conflicts
- Context switching enables control scheme changes
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:
PublicDependencyModuleNames.AddRange(new string[]
{
"Core",
"CoreUObject",
"Engine",
"InputCore",
"EnhancedInput" // Essential for Enhanced Input
});
Project Settings
Navigate to Edit > Project Settings > Engine > Input > Default Classes:
- Default Player Input Class:
EnhancedPlayerInput - Default Input Component Class:
EnhancedInputComponent
Character Header Setup
#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
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
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
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
Configuration in Input Mapping Context:
- Lower Threshold: 0.2
- Upper Threshold: 1.0
- Type: Scaled
Negate: Inverts input per axis
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)
Use case: W key becomes positive Y (forward) instead of positive X
Configuration: Order = YXZ
Scalar: Multiplies input value
Use case: Mouse sensitivity adjustment
Configuration: Scalar = FVector(2.0, 2.0, 1.0)
WASD configuration example:
- W key: Swizzle (YXZ) → Positive Y-axis (forward)
- S key: Swizzle (YXZ) + Negate → Negative Y-axis (backward)
- D key: No modifiers → Positive X-axis (right)
- A key: Negate → Negative X-axis (left)
Input Triggers
Common triggers:
Down: Fires continuously while held
Use case: Automatic weapon fire
Pressed: Single activation on initial press
Use case: Single-shot weapon
Released: Fires when input is released
Use case: Charge-and-release mechanics
Hold: Requires sustained input
Use case: Heavy attack (hold 0.5 seconds)
Configuration: Hold Time Threshold = 0.5
Tap: Quick press-release
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)
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:
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
// 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:
// 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:
// 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:
MaxSwimSpeed = 300.0f;
BrakingDecelerationSwimming = 0.0f;
Buoyancy = 1.0f;
JumpOutOfWaterPitch = 11.25f;
Flying configuration:
NavAgentProps.bCanFly = true; // Enable flying
MaxFlySpeed = 600.0f;
BrakingDecelerationFlying = 0.0f;
Rotation configuration:
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:
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:
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
#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
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
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:
- X-Axis (Direction): -180° to 180° (left/right strafe)
- Y-Axis (Speed): 0 to MaxWalkSpeed (idle to run)
Animation placement:
- Center: Idle animation
- Top center: Forward run
- Bottom center: Backward walk
- Left/right: Strafe animations
Blueprint implementation:
- Blend Space node samples based on Speed and Direction variables
- Automatic interpolation between animation poses
State Machine Integration
Common states:
- Idle: Speed == 0, not falling
- Walk/Run: Speed > threshold, on ground
- Jump: bIsInAir == true, rising
- Fall: bIsInAir == true, descending
- Land: Transition from air to ground
Transition rules:
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:
-
Enable in Animation Sequence:
- Open animation asset
- Enable "Enable Root Motion" in Asset Details
-
Configure Animation Blueprint:
- Set Root Motion Mode to "Root Motion from Everything"
-
Character Movement Component:
- Automatically applies root motion when detected
- Physics integration handled automatically
Network replication:
- Root motion is replicated via RepRootMotion
- Server-authoritative, clients predict
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):
- Executes movement locally for responsive controls
- Records moves in SavedMoves buffer
- Sends move data to server via ServerMove() RPC (RPC stands for Remote Procedure Call - a way to execute functions on other machines in multiplayer)
- Replays moves after server corrections
Authority (server):
- Validates incoming client moves
- Reproduces movement with received input
- Compares result with client's reported position
- Sends corrections when discrepancies exceed threshold
Simulated Proxy (other players' characters):
- Receives position/velocity updates from server
- Uses interpolation for smooth movement
- No prediction performed
FSavedMove_Character
Movement data structure for network transmission:
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:
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:
// 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
// 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:
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():
virtual FSavedMovePtr UMyCharacterMovement::AllocateNewMove()
{
return FSavedMovePtr(new FSavedMove_MyCharacter());
}
3. Update From Compressed Flags:
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
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
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:
- AlwaysSpawn: Ignore all collision, spawn at exact location
- AdjustIfPossibleButDontSpawnIfColliding: Try adjusting position, fail if impossible
- DontSpawnIfColliding: Fail immediately if any collision detected
- Undefined: Use actor class default
Best practices:
- Use
AdjustIfPossibleButDontSpawnIfCollidingfor players and AI - Use
AlwaysSpawnfor visual effects and non-critical actors - Use
DontSpawnIfCollidingfor strict placement requirements (building systems)
Deferred Construction
Deferred spawning allows property modification before BeginPlay:
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:
- Properties marked "Expose on Spawn" need runtime values
- Components require configuration before activation
- Complex initialization logic must run before BeginPlay
- Blueprint classes with constructor-dependent setup
Object Pooling
For high-frequency spawning (projectiles, particles), use object pooling:
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:
- Initial spawn: Large one-time cost (during level load)
- Runtime activation: Near-zero cost
- Garbage collection: Dramatically reduced
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:
// 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:
// 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:
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:
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:
// 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:
// 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:
Static/passive object? → AActor
Controllable non-humanoid? → APawn
Bipedal character? → ACharacter
8. Enhanced Input Without UPROPERTY
Problem: Input Actions garbage collected, causing crashes.
Solution:
// 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:
- Enable "Enable Root Motion" on Animation Sequence
- Set Animation Blueprint Root Motion Mode to "Root Motion from Everything"
- Ensure skeleton has proper root bone
- 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:
// 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.