Skip to main content

๐ŸŽฎ 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:

PieceWhat it isWhere it's made
Input Action (IA)An abstract intent: "Jump", "Move", "Look" โ€” plus its value typeEditor asset (UInputAction)
Input Mapping Context (IMC)A set of keyโ†’action mappings that can be layered on/offEditor asset (UInputMappingContext)
EnhancedInput subsystemRuntime manager that holds active contexts for a playerEngine-provided
Your Pawn/Controller C++Binds actions to functions and reactsYour code
graph LR Key["W key / Left stick"] --> IMC["Mapping Context
(key โ†’ IA_Move)"] IMC --> IA["IA_Move
(Axis2D intent)"] IA --> Sub["EnhancedInput Subsystem"] Sub --> Bind["Your bound C++ function
Move(const FInputActionValue&)"]
Figure 1: A key flows through a mapping context into an abstract Input Action, which the subsystem routes to your bound C++ function. Change the key in the IMC and your code never changes.

๐Ÿ“– 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

ETriggerEventFires whenTypical use
TriggeredEvery frame the action is active/heldContinuous move, look
StartedThe moment the action begins (press)Jump, fire-once, open menu
CompletedThe action ends (release)Stop jumping, release charge
Ongoing / CanceledDuring/aborting a held triggerHold-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.

  1. Add a UInputAction* LookAction property (Axis2D).
  2. Bind it in SetupPlayerInputComponent to a Look function on ETriggerEvent::Triggered.
  3. In Look, read the FVector2D and call AddControllerYawInput(Axis.X) and AddControllerPitchInput(Axis.Y).
  4. Explain why Look uses Triggered, not Started.
โœ… 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 as UPROPERTY slots set in the editor.
  • Add a mapping context via UEnhancedInputLocalPlayerSubsystem::AddMappingContext (in BeginPlay/on possession), with a priority.
  • Bind in SetupPlayerInputComponent by casting to UEnhancedInputComponent and calling BindAction(Action, ETriggerEvent, this, &Fn).
  • Read the value with Value.Get<T>() matching the action's type (bool / float / FVector2D); match ETriggerEvent to 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.