๐ฎ Lesson 4.1: Enhanced Input in C++
Enhanced Input is Unreal 5's modern input system, and it's how you connect a keypress or thumbstick to your C++ gameplay. It's more setup than the old system โ but it buys you rebindable controls, contexts, and clean data-driven bindings. Let's wire it end to end.
๐ฏ Learning Objectives
By the end of this lesson, you will be able to:
- Explain Input Actions and Input Mapping Contexts and how they relate
- Add a mapping context through the Enhanced Input local player subsystem
- Bind an Input Action to a C++ function with
UEnhancedInputComponent - Read the input value via
FInputActionValue(bool, axis1D, axis2D) - Choose the right
ETriggerEvent(Triggered, Started, Completed)
Estimated Time: 60 minutes
Engine Version: Unreal Engine 5.8
โ ๏ธ Enhanced Input is the standard now
The older "Action/Axis Mappings" input system is legacy in UE5. New projects should use Enhanced Input โ it's what the 5.8 templates ship with and what this course teaches. If you find old tutorials using BindAxis("MoveForward", ...), they predate this system.
In This Lesson
The Four Pieces
Enhanced Input separates "what the player did" from "which key did it" from "your code." Four pieces cooperate:
| Piece | What it is | Where it's made |
|---|---|---|
| Input Action (IA) | An abstract intent: "Jump", "Move", "Look" โ plus its value type | Editor asset (UInputAction) |
| Input Mapping Context (IMC) | A set of keyโaction mappings that can be layered on/off | Editor asset (UInputMappingContext) |
| EnhancedInput subsystem | Runtime manager that holds active contexts for a player | Engine-provided |
| Your Pawn/Controller C++ | Binds actions to functions and reacts | Your code |
(key โ IA_Move)"] IMC --> IA["IA_Move
(Axis2D intent)"] IA --> Sub["EnhancedInput Subsystem"] Sub --> Bind["Your bound C++ function
Move(const FInputActionValue&)"]
๐ Why the indirection?
Because "the player wants to move" and "the W key" are different concerns. Rebinding, gamepad vs keyboard, and context switches (on foot vs in a menu vs driving) all become data changes in IMCs โ your gameplay code binds to the intent (IA_Move) and stays untouched. That decoupling is the whole point, and it mirrors the loose-coupling lesson from Module 3.5.
Module Setup
Enhanced Input lives in its own module. Add it to your .Build.cs (Lesson 2.1) or you'll get unresolved-symbol errors:
// MyProject.Build.cs
PublicDependencyModuleNames.AddRange(new string[]
{
"Core", "CoreUObject", "Engine", "InputCore",
"EnhancedInput" // โ required for Enhanced Input
});
The IA and IMC assets are created in the editor (right-click โ Input). In C++ you reference them via UPROPERTY pointers a designer assigns:
// In your Pawn/Character header:
class UInputAction;
class UInputMappingContext;
// Assigned in the editor to IMC_Default, IA_Move, IA_Look, IA_Jump...
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
TObjectPtr<UInputMappingContext> DefaultMappingContext;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
TObjectPtr<UInputAction> MoveAction;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
TObjectPtr<UInputAction> JumpAction;
โ Assets in the editor, references in C++
You don't create IA/IMC assets in code โ you make them in the editor and expose UPROPERTY slots your C++ binds against. This is the hybrid pattern again: C++ owns the logic, the editor owns the data (which keys, which actions).
Adding a Mapping Context
A mapping context is inactive until you add it to the player's Enhanced Input subsystem. Do this in BeginPlay (or on possession โ Lesson 4.3), because it needs the world and the player controller.
#include "EnhancedInputSubsystems.h" // UEnhancedInputLocalPlayerSubsystem
void AMyCharacter::BeginPlay()
{
Super::BeginPlay();
// The PlayerController driving this pawn (may be null for AI-possessed pawns):
if (APlayerController* PC = Cast<APlayerController>(GetController()))
{
// Get the per-player Enhanced Input subsystem and add our context.
if (UEnhancedInputLocalPlayerSubsystem* Subsystem =
ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(
PC->GetLocalPlayer()))
{
Subsystem->AddMappingContext(DefaultMappingContext, /*Priority=*/0);
}
}
}
๐ Contexts have priority
You can layer multiple contexts โ e.g. a base movement context plus a temporary "vehicle" context on top. The priority number decides which wins when two contexts map the same key (higher priority takes precedence). Add and remove contexts to switch control schemes without rebinding anything.
Binding Actions in C++
Binding happens in the overridable SetupPlayerInputComponent. The incoming component is an UEnhancedInputComponent โ cast it, then BindAction.
#include "EnhancedInputComponent.h"
void AMyCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
// Enhanced Input provides a richer component; cast to reach BindAction.
if (UEnhancedInputComponent* EIC = Cast<UEnhancedInputComponent>(PlayerInputComponent))
{
// BindAction(Action, TriggerEvent, Object, &Function)
EIC->BindAction(MoveAction, ETriggerEvent::Triggered, this, &AMyCharacter::Move);
EIC->BindAction(JumpAction, ETriggerEvent::Started, this, &AMyCharacter::StartJump);
EIC->BindAction(JumpAction, ETriggerEvent::Completed, this, &AMyCharacter::StopJump);
}
}
Trigger events โ when the function fires
ETriggerEvent | Fires when | Typical use |
|---|---|---|
Triggered | Every frame the action is active/held | Continuous move, look |
Started | The moment the action begins (press) | Jump, fire-once, open menu |
Completed | The action ends (release) | Stop jumping, release charge |
Ongoing / Canceled | During/aborting a held trigger | Hold-to-charge mechanics |
โ ๏ธ Match the event to the intent
Binding movement to Started would move the player for a single frame; binding it to Triggered keeps it going while held. Conversely, binding "jump" to Triggered would re-trigger every frame. Getting Started vs Triggered right is the most common Enhanced Input fix.
Reading Input Values
Every bound function receives a const FInputActionValue& โ a typed container for the input's current value. You read it according to the action's value type (set on the IA asset).
#include "InputActionValue.h"
// A 2D move action (WASD or left stick) -> a Vector2D:
void AMyCharacter::Move(const FInputActionValue& Value)
{
const FVector2D Axis = Value.Get<FVector2D>(); // X = right/left, Y = fwd/back
if (Controller)
{
AddMovementInput(GetActorForwardVector(), Axis.Y);
AddMovementInput(GetActorRightVector(), Axis.X);
}
}
// A digital (button) action -> a bool:
void AMyCharacter::StartJump(const FInputActionValue& Value)
{
const bool bPressed = Value.Get<bool>(); // usually true on Started
Jump(); // ACharacter's built-in jump (Lesson 4.2)
}
// A 1D axis action (trigger, throttle) -> a float:
void AMyCharacter::Throttle(const FInputActionValue& Value)
{
const float Amount = Value.Get<float>();
}
๐ Value type must match the action
An IA declares its value type โ Digital (bool), Axis1D (float), or Axis2D (Vector2D). Call Value.Get<T>() with the matching T. Ask a bool action for a FVector2D and you'll get zeros. Set the action's type in the editor and read the same type in C++.
โ You just built the movement spine
That Move function โ read the 2D axis, feed it to AddMovementInput โ is the core of nearly every character controller. Next lesson we pair it with the Character Movement Component to make an actual walking, jumping character.
Hands-on Exercise & Quiz
๐๏ธ Exercise: Add a "Look" action
Objective: Extend the setup with mouse/stick look.
- Add a
UInputAction* LookActionproperty (Axis2D). - Bind it in
SetupPlayerInputComponentto aLookfunction onETriggerEvent::Triggered. - In
Look, read theFVector2Dand callAddControllerYawInput(Axis.X)andAddControllerPitchInput(Axis.Y). - Explain why
LookusesTriggered, notStarted.
โ Look function & reasoning
void AMyCharacter::Look(const FInputActionValue& Value)
{
const FVector2D Axis = Value.Get<FVector2D>();
AddControllerYawInput(Axis.X);
AddControllerPitchInput(Axis.Y);
}
Triggered because looking is continuous โ you want it every frame the mouse moves or the stick is held, not just once at the start.
๐ฏ Quick Quiz
Question 1: What must you add to .Build.cs for Enhanced Input?
Question 2: Which trigger event suits continuous movement while a key is held?
Question 3: A 2D movement action's value is read with:
Summary
๐ Key Takeaways
- Enhanced Input separates intent (Input Action) from keys (Mapping Context) from your code โ rebinding is a data change, not a code change.
- Add
"EnhancedInput"to.Build.cs; expose IA/IMC assets asUPROPERTYslots set in the editor. - Add a mapping context via
UEnhancedInputLocalPlayerSubsystem::AddMappingContext(inBeginPlay/on possession), with a priority. - Bind in
SetupPlayerInputComponentby casting toUEnhancedInputComponentand callingBindAction(Action, ETriggerEvent, this, &Fn). - Read the value with
Value.Get<T>()matching the action's type (bool / float / FVector2D); matchETriggerEventto the intent.
๐ Additional Resources
๐ What's Next?
Input is flowing into your functions. Now let's make it move a real character: Pawns, Characters, and the Character Movement Component โ turning that AddMovementInput call into walking, jumping, and falling.
๐ Lesson complete!
The player's intent reaches your code. Let's give it a body to drive.