Skip to content
Learni
View all tutorials
Unreal Engine

How to Create a Character Controller in Unreal Engine 2026

Lire en français

Introduction

In 2026, Unreal Engine leads AAA game development with mature Nanite/Lumen rendering and high-performance C++ support. Building a custom Character Controller in C++ is key to surpassing Blueprint limitations in performance and fine control. Unlike basic templates that lack flexibility, this tutorial walks you through a full system: omnidirectional movement (WASD), realistic gravity-based jumping, and a follow camera with SpringArm for immersion.

Why go this route? Blueprints are great for rapid prototyping, but for 60+ FPS games with complex AI or multiplayer, C++ cuts CPU latency by 30-50%. We start from a blank project for total control, generating VS files via command line—a pro studio method. By the end, you'll have a playable, compilable pawn ready for extensions like Montage animations or network replication. The guide progresses from foundations (project structure) to advanced features (input binding), using analogies like a 'car chassis' for physical components.

Prerequisites

  • Unreal Engine 5.4+ (or 6.0 in 2026) installed via Epic Games Launcher.
  • Visual Studio 2022 Community+ with 'C++ Games' and 'Unreal Engine Installer' workloads (includes MSVC, Windows SDK).
  • 10 GB free disk space for the project.
  • Basic C++ knowledge (classes, pointers) and UE concepts (Actors, Components).

Step 1: Initialize the Project Structure

Manually create the folder hierarchy for absolute control, like a skeleton ready for muscle attachments in C++. This skips cluttered GUI templates and allows customizations from the start. Open a terminal (PowerShell or Git Bash) in your dev workspace.

Create the Base Folders and Files

terminal-init.sh
mkdir CharacterControllerProject
cd CharacterControllerProject
mkdir Source
mkdir Source/CharacterControllerProject
mkdir Config
mkdir Content
mkdir Plugins
echo 'Projet UE initialisé'

This script sets up the standard UE structure: Source for C++, Config for .ini files, Content for assets. Run it in a parent folder; it preps the ground without unnecessary boilerplate code. Pitfall: Forgetting Source/CharacterControllerProject leads to 'module not found' build errors.

Step 2: Define the Main Project File

The .uproject file is the JSON manifest linking your code to the engine. It declares the main module and enables default plugins, like a contract between your code and UE.

Create CharacterControllerProject.uproject

CharacterControllerProject.uproject
{
  "FileVersion": 3,
  "EngineAssociation": "5.4",
  "Category": "",
  "Description": "Projet Character Controller C++",
  "Modules": [
    {
      "Name": "CharacterControllerProject",
      "Type": "Runtime",
      "LoadingPhase": "Default"
    }
  ],
  "Plugins": [
    {
      "Name": "EnhancedInput",
      "Enabled": true
    }
  ]
}

This JSON links to UE 5.4+ and enables EnhancedInput for modern bindings (better than legacy inputs). Copy-paste into the root. Version 3 is stable in 2026; avoid changing FileVersion to prevent corruptions.

Step 3: Generate Visual Studio Files

Use the UE batch tool to turn your skeleton into a compilable SLN. Analogy: like compiling a Makefile into a full-featured IDE.

Generate the VS Project

terminal-generate.bat
REM Remplacez par votre chemin UE
set UE_PATH="C:/Program Files/Epic Games/UE_5.4/Engine/Build/BatchFiles"
call %UE_PATH%/GenerateProjectFiles.bat -project="CharacterControllerProject.uproject" -game -makefileafter

REM Ouvrir VS (optionnel)
start CharacterControllerProject.sln

-game targets a standalone executable, -makefileafter for fast Ninja builds. Adjust UE_PATH to your install (check Epic Launcher). Major pitfall: VS without UE workload fails linking; reinstall if 'cl.exe not found'.

Step 4: Configure Build Dependencies

Build.cs defines the required UE libraries, like a pom.xml for C++. Add InputCore and EnhancedInput for our controller.

Source/CharacterControllerProject/CharacterControllerProject.Build.cs

Source/CharacterControllerProject/CharacterControllerProject.Build.cs
using UnrealBuildTool;

public class CharacterControllerProject : ModuleRules
{
    public CharacterControllerProject(ReadOnlyTargetRules Target) : base(Target)
    {
        PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;

        PublicDependencyModuleNames.AddRange(new string[] { 
            "Core", 
            "CoreUObject", 
            "Engine",
            "InputCore",
            "EnhancedInput",
            "HeadMountedDisplay",
            "AIModule"
        });

        PrivateDependencyModuleNames.AddRange(new string[] { });
    }
}

PublicDependency exposes headers, adds EnhancedInput for modern actions. AIModule optional for future AI. Common error: Forgetting 'Engine' causes spawn crashes; full rebuild after edits.

Step 5: Implement the Character Header

Define the class extending ACharacter: UPROPERTY components for Capsule (collision), SpringArm (smooth camera), Camera (FPS view). Input bindings.

Source/CharacterControllerProject/MyCharacter.h

Source/CharacterControllerProject/MyCharacter.h
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "InputActionValue.h"
#include "MyCharacter.generated.h"

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

public:
    AMyCharacter();

protected:
    virtual void BeginPlay() override;

    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true"))
    class USpringArmComponent* SpringArm;

    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true"))
    class UCameraComponent* Camera;

    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input)
    class UInputMappingContext* DefaultMappingContext;

    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input)
    class UInputAction* MoveAction;

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

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

public:
    virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
    
private:
    void Move(const FInputActionValue& Value);
    void Look(const FInputActionValue& Value);
};

Inherits ACharacter for built-in physics (gravity, collision). SpringArm prevents wall clipping, Camera handles rendering. Enhanced Inputs support multi-device (gamepad/keyboard). GENERATED_BODY() auto-generates UE boilerplate.

Step 6: Implement the CPP Logic

Constructor attaches components, SetupPlayerInput binds actions to functions. Move/Look use GetCharacterMovement()->AddInputVector for world-space direction.

Source/CharacterControllerProject/MyCharacter.cpp

Source/CharacterControllerProject/MyCharacter.cpp
#include "MyCharacter.h"
#include "Camera/CameraComponent.h"
#include "Components/CapsuleComponent.h"
#include "Components/InputComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "GameFramework/Controller.h"
#include "GameFramework/SpringArmComponent.h"
#include "EnhancedInputComponent.h"
#include "EnhancedInputSubsystems.h"
#include "InputMappingContext.h"
#include "InputAction.h"

AMyCharacter::AMyCharacter()
{
    PrimaryActorTick.bCanEverTick = true;

    GetCapsuleComponent()->InitCapsuleSize(42.f, 96.0f);

    SpringArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArm"));
    SpringArm->SetupAttachment(RootComponent);
    SpringArm->TargetArmLength = 300.0f;
    SpringArm->bUsePawnControlRotation = true;

    Camera = CreateDefaultSubobject<UCameraComponent>(TEXT("Camera"));
    Camera->SetupAttachment(SpringArm, USpringArmComponent::SocketName);

    GetCharacterMovement()->JumpZVelocity = 700.f;
    GetCharacterMovement()->AirControl = 0.35f;
    GetCharacterMovement()->MaxWalkSpeed = 500.f;
    GetCharacterMovement()->MinAnalogWalkSpeed = 20.f;
    GetCharacterMovement()->BrakingDecelerationWalking = 2000.f;
}

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

    if (APlayerController* PlayerController = Cast<APlayerController>(Controller))
    {
        if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer()))
        {
            Subsystem->AddMappingContext(DefaultMappingContext, 0);
        }
    }
}

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

    if (UEnhancedInputComponent* EnhancedInputComponent = CastChecked<UEnhancedInputComponent>(PlayerInputComponent))
    {
        EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &AMyCharacter::Move);
        EnhancedInputComponent->BindAction(LookAction, ETriggerEvent::Triggered, this, &AMyCharacter::Look);
        EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Triggered, this, &ACharacter::Jump);
        EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Completed, this, &ACharacter::StopJumping);
    }
}

void AMyCharacter::Move(const FInputActionValue& Value)
{
    FVector2D MovementVector = Value.Get<FVector2D>();

    if (Controller != nullptr)
    {
        AddMovementInput(GetActorForwardVector(), MovementVector.Y);
        AddMovementInput(GetActorRightVector(), MovementVector.X);
    }
}

void AMyCharacter::Look(const FInputActionValue& Value)
{
    FVector2D LookAxisVector = Value.Get<FVector2D>();

    if (Controller != nullptr)
    {
        AddControllerYawInput(LookAxisVector.X);
        AddControllerPitchInput(LookAxisVector.Y);
    }
}

Constructor scales Capsule (realistic 96cm height), tunes MovementComponent for 500u/s speed. BeginPlay adds MappingContext to subsystem. Move combines forward/right for smooth strafing; Look handles yaw/pitch camera. Jump uses legacy for compatibility.

Step 7: Compile and Set Up Input Assets

Compile in VS (Development Editor), open Editor via .uproject. Create Input Mapping Context and Actions in Content/Input (EnhancedInput assets), assign to MyCharacter BP subclass.

Compile the Project

terminal-compile.bat
REM Depuis root projet
call Engine/Build/BatchFiles/Build.bat CharacterControllerProjectEditor Win64 Development -project="CharacterControllerProject.uproject" -WaitMutex

REM Ou via VS: Development Editor > Build Solution (F7)
start CharacterControllerProject.uproject

Batch build is CI/CD friendly; -WaitMutex avoids parallel locks. ~2min first time (PCH gen). Error: 'module not found' → check Build.cs and regenerate SLN.

Step 8: Configure Enhanced Inputs

In Editor > Project Settings > Input > Add Mapping Context (DefaultMappingContext). Create Actions: Move (2D Axis), Look (2D Axis), Jump (Digital). Scale 1.0-2.0.

Example Config/DefaultInput.ini (for advanced mappings)

Config/DefaultInput.ini
[/Script/Engine.InputSettings]
NativeInputVisualization=Hidden

[+AxisMappings=(ActionName="MoveForward",Key=UpArrow,bAlt=false,Scale=1.0)]
[+AxisMappings=(ActionName="MoveForward",Key=W,bAlt=false,Scale=1.0)]
[+AxisMappings=(ActionName="MoveRight",Key=RightArrow,bAlt=false,Scale=1.0)]
[+AxisMappings=(ActionName="MoveRight",Key=D,bAlt=false,Scale=1.0)]

[+AxisMappings=(ActionName="Turn",Key=MouseX,bAlt=false,Scale=1.0)]
[+AxisMappings=(ActionName="LookUp",Key=MouseY,bAlt=false,Scale=-1.0)]

[+ActionMappings=(ActionName="Jump",Key=SpaceBar,bAlt=false,Shift=false,Control=false,Cmd=false)]

bUseNewInputStack=True

.ini backup for Git versioning; legacy mappings compatible but prioritize Enhanced. Scale=-1 inverts mouse Y. Reload Editor after edits.

Testing and Integration

Create a Blueprint subclass of MyCharacter (set as Default Pawn in World Settings). Play In Editor: WASD for movement, mouse for look, Space for jump. Air control 0.35 for precise handling.

Best Practices

  • Always use EnhancedInput: Scales to gamepad/VR, legacy is obsolete in 2026.
  • Tune MovementComponent early: MaxWalkSpeed=600 for running, GroundFriction=8 for realistic grip.
  • UPROPERTY meta=(AllowPrivateAccess) for Blueprint access without verbose public getters.
  • Version .uproject/Build.cs in Git; ignore Binaries/Intermediate.
  • Profile CPU with Stat Game to optimize Tick (disable if >16ms).

Common Errors to Avoid

  • 'Subsystem null' crash: Check Cast in BeginPlay; test non-local.
  • No movement: Forgot AddMappingContext or bUsePawnControlRotation=false on SpringArm.
  • Infinite compile loop: Delete DerivedDataCache before rebuild.
  • Jump stuck: Bind Completed event for StopJumping, not just Triggered.

Next Steps

  • Integrate Animation Blueprint with State Machine for idle/walk/jump.
  • Add replication for multiplayer: UPROPERTY(Replicated) on velocity.
  • Explore Chaos Physics for ragdoll.
Check out our advanced Unreal Engine courses for C++ networking and Niagara VFX. Official docs: UE Enhanced Input