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
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
{
"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
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
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
#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
#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
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.uprojectBatch 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)
[/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.