ue-input-system

Original🇺🇸 English
Translated

Use this skill when implementing player input with Unreal Engine's Enhanced Input system. Also use when the user mentions 'Enhanced Input', 'input', 'input action', 'InputAction', 'mapping context', 'InputMappingContext', 'input binding', 'key binding', 'input trigger', 'input modifier', 'gamepad', or 'keyboard'. Covers ETriggerEvent, built-in triggers (Hold, Tap, Pulse, ChordAction, Combo), built-in modifiers (DeadZone, Scalar, Negate, SwizzleAxis), and custom trigger/modifier authoring. See references/input-action-reference.md for the full catalogue. For UI input modes, see ue-ui-umg-slate.

6installs
Added on

NPX Install

npx skill4agent add quodsoler/unreal-engine-skills ue-input-system

Tags

Translated version includes tags in frontmatter

UE Enhanced Input System

You are an expert in Unreal Engine's Enhanced Input system.

Context Check

Read
.agents/ue-project-context.md
before proceeding. Confirm:
  • EnhancedInput
    plugin is listed as enabled
  • Target platforms (affects which modifiers are needed per platform)
  • Whether CommonUI is in use (it manages input mode switching automatically)
  • Whether the project still uses legacy input (migration may be needed)

Information Gathering

Ask the developer: what actions are needed and their value types (Bool/Axis1D/Axis2D/Axis3D), which platforms, any complex input requirements (hold-to-charge, double-tap, combos, chord shortcuts), and whether multiple input modes are required (gameplay vs UI vs vehicle).

Enhanced Input Setup

Plugin and Module

.uproject
: add
{ "Name": "EnhancedInput", "Enabled": true }
to Plugins.
Build.cs
: add
"EnhancedInput"
to
PublicDependencyModuleNames
.
DefaultInput.ini
:
ini
[/Script/Engine.InputSettings]
DefaultPlayerInputClass=/Script/EnhancedInput.EnhancedPlayerInput
DefaultInputComponentClass=/Script/EnhancedInput.EnhancedInputComponent

UInputAction Asset

UInputAction : UDataAsset
. Create one per logical player action. Key properties (from
InputAction.h
):
cpp
EInputActionValueType ValueType = EInputActionValueType::Boolean;
// Boolean | Axis1D (float) | Axis2D (FVector2D) | Axis3D (FVector)

EInputActionAccumulationBehavior AccumulationBehavior
    = EInputActionAccumulationBehavior::TakeHighestAbsoluteValue;
// TakeHighestAbsoluteValue — highest magnitude wins across all mappings to this action
// Cumulative — all mapping values sum (W + S cancel each other for WASD)

bool bConsumeInput = true;  // blocks lower-priority Enhanced Input mappings to same keys

TArray<TObjectPtr<UInputTrigger>> Triggers;   // applied AFTER per-mapping triggers
TArray<TObjectPtr<UInputModifier>> Modifiers; // applied AFTER per-mapping modifiers

UInputMappingContext Asset

UInputMappingContext : UDataAsset
. Maps physical keys to actions.
  • DefaultKeyMappings.Mappings
    TArray<FEnhancedActionKeyMapping>
    of key-to-action entries
  • MappingProfileOverrides
    — per-profile key overrides for player remapping support
  • RegistrationTrackingMode
    :
    Untracked
    (default, first Remove wins) or
    CountRegistrations
    (IMC stays until Remove called N times, safe when multiple systems share it)

Binding Actions in C++

SetupPlayerInputComponent

cpp
// MyCharacter.h — declare assets and handlers
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
TObjectPtr<UInputMappingContext> DefaultMappingContext;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
TObjectPtr<UInputAction> MoveAction;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
TObjectPtr<UInputAction> JumpAction;

void Move(const FInputActionValue& Value);
void StartJump();
void StopJump();
cpp
// MyCharacter.cpp
#include "EnhancedInputComponent.h"
#include "EnhancedInputSubsystems.h"

void AMyCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
    Super::SetupPlayerInputComponent(PlayerInputComponent);
    UEnhancedInputComponent* EIC = Cast<UEnhancedInputComponent>(PlayerInputComponent);
    if (!EIC) { return; }

    EIC->BindAction(MoveAction, ETriggerEvent::Triggered, this, &AMyCharacter::Move);
    EIC->BindAction(JumpAction, ETriggerEvent::Started,   this, &AMyCharacter::StartJump);
    EIC->BindAction(JumpAction, ETriggerEvent::Completed, this, &AMyCharacter::StopJump);
}

void AMyCharacter::BeginPlay()
{
    Super::BeginPlay();
    if (APlayerController* PC = Cast<APlayerController>(GetController()))
    {
        if (auto* Sub = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(
                PC->GetLocalPlayer()))
        {
            Sub->AddMappingContext(DefaultMappingContext, 0); // priority 0 = lowest
        }
    }
}

Callback Signatures

BindAction
accepts four delegate signatures:
cpp
// No params — press/release without value needed
void AMyCharacter::StartJump() { Jump(); }

// FInputActionValue — for axis values
void AMyCharacter::Move(const FInputActionValue& Value)
{
    const FVector2D Input = Value.Get<FVector2D>();
    AddMovementInput(GetActorForwardVector(), Input.Y);
    AddMovementInput(GetActorRightVector(),   Input.X);
}

// FInputActionInstance — when elapsed/triggered time is needed
void AMyCharacter::OnChargeAttack(const FInputActionInstance& Instance)
{
    const float HeldFor = Instance.GetElapsedTime();    // Started + Ongoing + Triggered
    const float ActiveFor = Instance.GetTriggeredTime(); // Triggered only
}

// Lambda variant
EIC->BindActionValueLambda(InteractAction, ETriggerEvent::Triggered,
    [this](const FInputActionValue& Value) { TryInteract(); });
Storing and removing a binding:
cpp
FEnhancedInputActionEventBinding& B =
    EIC->BindAction(DebugAction, ETriggerEvent::Started, this, &AMyCharacter::DebugToggle);
uint32 Handle = B.GetHandle();
// ...
EIC->RemoveBindingByHandle(Handle);        // remove one binding
EIC->ClearBindingsForObject(this);         // remove all bindings for an object

Trigger Events (ETriggerEvent)

Bitmask enum from
InputTriggers.h
:
EventState TransitionUse for
Started
None -> Ongoing/TriggeredFirst frame of input; press-once actions
Triggered
*->Triggered, Triggered->TriggeredEvery active frame; continuous movement
Ongoing
Ongoing->OngoingHeld but not yet triggered (charge build-up)
Canceled
Ongoing->NoneReleased before trigger threshold
Completed
Triggered->NoneInput released after triggering; stop continuous actions
Note:
Completed
does not fire if any trigger on the same action reports
Ongoing
that frame.

Built-in Triggers

Full parameter listings in
references/input-action-reference.md
.
ClassNameBehavior
UInputTriggerDown
DownEvery frame input exceeds threshold (implicit default)
UInputTriggerPressed
PressedOnce on first actuation; holding does not repeat
UInputTriggerReleased
ReleasedOnce when input drops below threshold after actuation
UInputTriggerHold
HoldAfter
HoldTimeThreshold
s;
bIsOneShot=false
repeats every frame
UInputTriggerHoldAndRelease
Hold And ReleaseOn release after holding
HoldTimeThreshold
s
UInputTriggerTap
TapReleased within
TapReleaseTimeThreshold
s
UInputTriggerRepeatedTap
Repeated TapN taps within
RepeatDelay
(
NumberOfTapsWhichTriggerRepeat=2
for double-tap)
UInputTriggerPulse
PulseRepeatedly at
Interval
s while held; optional
TriggerLimit
UInputTriggerChordAction
Chorded ActionOnly fires while
ChordAction
is active (Implicit type; auto-blocks solo key)
UInputTriggerCombo
Combo (Beta)All
ComboActions
completed in order within
TimeToPressKey
windows
Trigger type rules for multi-trigger evaluation:
Explicit
(default, at least one must fire),
Implicit
(all must fire),
Blocker
(blocks everything if active).

Built-in Modifiers

Applied in array order. Mapping-level modifiers run before action-level modifiers.
ClassNameEffect
UInputModifierDeadZone
Dead ZoneZero input below
LowerThreshold
; remap to 1 at
UpperThreshold
. Types: Axial, Radial, UnscaledRadial
UInputModifierScalar
ScalarMultiply per axis by
FVector Scalar
UInputModifierScaleByDeltaTime
Scale By Delta TimeMultiply by frame DeltaTime
UInputModifierNegate
NegateInvert selected axes (
bX
,
bY
,
bZ
)
UInputModifierSwizzleAxis
Swizzle Input Axis ValuesReorder axes;
YXZ
(default) swaps X/Y — maps 1D key onto Y of Axis2D action
UInputModifierSmooth
SmoothRolling average over recent samples
UInputModifierSmoothDelta
Smooth DeltaSmoothed normalized delta; configurable interpolation (
Lerp
,
Interp_To
, ease curves)
UInputModifierResponseCurveExponential
Response Curve - Exponential`sign(x)*
UInputModifierResponseCurveUser
Response Curve - User DefinedSeparate
UCurveFloat
per axis
UInputModifierFOVScaling
FOV ScalingScale by camera FOV for consistent angular speed across zoom levels
UInputModifierToWorldSpace
To World Space2D axis -> world space (up/down = world X, left/right = world Y)
WASD -> Axis2D recipe (
AccumulationBehavior = Cumulative
):
  • W
    :
    SwizzleAxis(YXZ)
    → Y=+1
  • S
    :
    SwizzleAxis(YXZ)
    +
    Negate(bY)
    → Y=-1
  • D
    : none → X=+1
  • A
    :
    Negate(bX)
    → X=-1
Gamepad stick:
DeadZone(Radial, LowerThreshold=0.2)
per stick mapping.
Mouse look:
[Scalar(0.4,0.4,1), Smooth, FOVScaling]
per mapping.

Mapping Context Priority

cpp
// Higher integer = higher priority; wins key conflicts
Subsystem->AddMappingContext(GameplayIMC, 0);
Subsystem->AddMappingContext(VehicleIMC,  1);

// FModifyContextOptions — prevent ghost inputs on switch
FModifyContextOptions Opts;
Opts.bIgnoreAllPressedKeysUntilRelease = true;
Subsystem->AddMappingContext(UIIMC, 2, Opts);

// Remove on mode exit
Subsystem->RemoveMappingContext(VehicleIMC);
When
bConsumeInput = true
on a
UInputAction
(the default), a higher-priority context that maps the same physical key will consume it, blocking all lower-priority bindings to that key from firing. This is intentional: use priority layering and
bConsumeInput
together to prevent input conflicts between modes (e.g., a vehicle context consuming Spacebar so the character's Jump action never fires while driving).

Split-Screen / Multiple Local Players

In split-screen, each local player has their own
UEnhancedInputLocalPlayerSubsystem
. Mapping contexts are per-player — adding a context to one player's subsystem does not affect others. To target a specific player, retrieve their subsystem directly from their
ULocalPlayer
:
cpp
// Access subsystem for a specific local player (e.g., player 2)
if (ULocalPlayer* LP = PlayerController->GetLocalPlayer())
{
    if (auto* Sub = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(LP))
    {
        Sub->AddMappingContext(PlayerTwoIMC, 0);
    }
}

Custom Triggers

Subclass
UInputTrigger
; override
UpdateState_Implementation
returning
ETriggerState::None / Ongoing / Triggered
:
cpp
UCLASS(EditInlineNew, meta=(DisplayName="Double Click"))
class MYGAME_API UDoubleClickTrigger : public UInputTrigger
{
    GENERATED_BODY()
public:
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Trigger Settings")
    float DoubleClickThreshold = 0.3f;
protected:
    virtual ETriggerType GetTriggerType_Implementation() const override
        { return ETriggerType::Explicit; }
    virtual ETriggerState UpdateState_Implementation(
        const UEnhancedPlayerInput* PlayerInput, FInputActionValue ModifiedValue, float DeltaTime) override;
private:
    float LastPressTime = 0.f;
    bool bWasActuated = false;
};

ETriggerState UDoubleClickTrigger::UpdateState_Implementation(
    const UEnhancedPlayerInput* PlayerInput, FInputActionValue ModifiedValue, float DeltaTime)
{
    const bool bActuated = IsActuated(ModifiedValue); // helper: magnitude >= ActuationThreshold
    const float Now = PlayerInput->GetWorld()->GetTimeSeconds();
    if (bActuated && !bWasActuated)
    {
        if ((Now - LastPressTime) <= DoubleClickThreshold)
            { LastPressTime = 0.f; bWasActuated = bActuated; return ETriggerState::Triggered; }
        LastPressTime = Now;
    }
    bWasActuated = bActuated;
    return ETriggerState::None;
}
UInputTriggerTimedBase
provides
HeldDuration
and
CalculateHeldDuration
for time-based triggers.

Custom Modifiers

Subclass
UInputModifier
; override
ModifyRaw_Implementation
:
cpp
UCLASS(EditInlineNew, meta=(DisplayName="Clamp Magnitude"))
class MYGAME_API UClampMagnitudeModifier : public UInputModifier
{
    GENERATED_BODY()
public:
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Settings)
    float MaxMagnitude = 1.0f;
protected:
    virtual FInputActionValue ModifyRaw_Implementation(
        const UEnhancedPlayerInput* PlayerInput, FInputActionValue CurrentValue, float DeltaTime) override
    {
        FVector V = CurrentValue.Get<FVector>();
        if (V.SizeSquared() > MaxMagnitude * MaxMagnitude)
            V = V.GetSafeNormal() * MaxMagnitude;
        return FInputActionValue(CurrentValue.GetValueType(), V);
    }
};

Common Mistakes

  • Mapping context not added: Add via subsystem in
    BeginPlay
    /after
    Possess
    , not in
    SetupPlayerInputComponent
    (called earlier on some paths).
  • Legacy binding on UEnhancedInputComponent:
    BindAction(FName,...)
    and
    BindAxis
    are
    = delete
    . Compile error. Set
    DefaultInputComponentClass
    in
    DefaultInput.ini
    .
  • Triggered for a button press:
    Triggered
    fires every active frame. Use
    Started
    for press-once,
    Completed
    for release.
  • Completed not firing with Ongoing: If any trigger reports
    Ongoing
    that frame,
    Completed
    is suppressed. Use separate actions for clean press/release events.
  • No dead zone on gamepad sticks: Sticks produce non-zero resting values. Always add
    DeadZone(Radial)
    per stick mapping.
  • Missing SwizzleAxis for WASD-to-2D: Keyboard produces 1D. Without
    SwizzleAxis(YXZ)
    on W/S, forward/backward stays on X and is ignored by Axis2D forward movement.
  • Replicating input actions: Input is client-local. Replicate results (movement, ability activation), not trigger events.
  • MapKey/UnmapKey at runtime for rebinding: These are editor/config-screen helpers. Use subsystem player mappable key APIs or swap contexts instead.
  • Wrong trigger type for intent: Using
    Down
    when
    Hold
    is needed, or
    Triggered
    when
    Started
    is needed. Match trigger type to the interaction pattern:
    Started
    for single press,
    Triggered
    for continuous,
    Hold
    for delayed activation.

Legacy Input Migration

To migrate from the legacy input system to Enhanced Input: search for
InputComponent->BindAction
and
InputComponent->BindAxis
calls and replace each with
UEnhancedInputComponent::BindAction
. Create a
UInputAction
data asset for every old action name, choosing the appropriate
ValueType
(Boolean for buttons, Axis1D for single-axis, Axis2D for stick/WASD). Create a
UInputMappingContext
asset and add key mappings corresponding to the old
DefaultInput.ini
ActionMappings
/
AxisMappings
entries. Set
DefaultInputComponentClass
in
DefaultInput.ini
and enable the EnhancedInput plugin.

UI Input Mode

Without CommonUI, manage input modes manually via
APlayerController::SetInputMode()
:
cpp
PC->SetInputMode(FInputModeUIOnly());          // cursor captured by UI, no game input
PC->SetInputMode(FInputModeGameAndUI());       // both UI and game receive input
PC->SetInputMode(FInputModeGameOnly());        // full game input, UI events suppressed
CommonUI automates input routing through
UCommonActivatableWidget
stacks and eliminates most manual
SetInputMode
calls — see
ue-ui-umg-slate
.

Related Skills

  • ue-gameplay-framework
    — PlayerController input lifecycle,
    Possess
    /
    UnPossess
    ,
    SetupPlayerInputComponent
  • ue-ui-umg-slate
    SetInputMode(FInputModeUIOnly())
    /
    FInputModeGameAndUI()
    , cursor visibility, CommonUI
    UCommonActivatableWidget
    stacks
  • ue-gameplay-abilities
    — binding Enhanced Input actions to GAS ability activation via input ID tags