ue-input-system
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:
EnhancedInputplugin 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:
[/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):
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 entriesMappingProfileOverrides— per-profile key overrides for player remapping supportRegistrationTrackingMode:Untracked(default, first Remove wins) orCountRegistrations(IMC stays until Remove called N times, safe when multiple systems share it)
Binding Actions in C++
SetupPlayerInputComponent
// 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();
// 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:
// 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:
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:
| Event | State Transition | Use for |
|---|---|---|
Started |
None -> Ongoing/Triggered | First frame of input; press-once actions |
Triggered |
*->Triggered, Triggered->Triggered | Every active frame; continuous movement |
Ongoing |
Ongoing->Ongoing | Held but not yet triggered (charge build-up) |
Canceled |
Ongoing->None | Released before trigger threshold |
Completed |
Triggered->None | Input 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.
| Class | Name | Behavior |
|---|---|---|
UInputTriggerDown |
Down | Every frame input exceeds threshold (implicit default) |
UInputTriggerPressed |
Pressed | Once on first actuation; holding does not repeat |
UInputTriggerReleased |
Released | Once when input drops below threshold after actuation |
UInputTriggerHold |
Hold | After HoldTimeThreshold s; bIsOneShot=false repeats every frame |
UInputTriggerHoldAndRelease |
Hold And Release | On release after holding HoldTimeThreshold s |
UInputTriggerTap |
Tap | Released within TapReleaseTimeThreshold s |
UInputTriggerRepeatedTap |
Repeated Tap | N taps within RepeatDelay (NumberOfTapsWhichTriggerRepeat=2 for double-tap) |
UInputTriggerPulse |
Pulse | Repeatedly at Interval s while held; optional TriggerLimit |
UInputTriggerChordAction |
Chorded Action | Only 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.
| Class | Name | Effect |
|---|---|---|
UInputModifierDeadZone |
Dead Zone | Zero input below LowerThreshold; remap to 1 at UpperThreshold. Types: Axial, Radial, UnscaledRadial |
UInputModifierScalar |
Scalar | Multiply per axis by FVector Scalar |
UInputModifierScaleByDeltaTime |
Scale By Delta Time | Multiply by frame DeltaTime |
UInputModifierNegate |
Negate | Invert selected axes (bX, bY, bZ) |
UInputModifierSwizzleAxis |
Swizzle Input Axis Values | Reorder axes; YXZ (default) swaps X/Y — maps 1D key onto Y of Axis2D action |
UInputModifierSmooth |
Smooth | Rolling average over recent samples |
UInputModifierSmoothDelta |
Smooth Delta | Smoothed normalized delta; configurable interpolation (Lerp, Interp_To, ease curves) |
UInputModifierResponseCurveExponential |
Response Curve - Exponential | `sign(x)* |
UInputModifierResponseCurveUser |
Response Curve - User Defined | Separate UCurveFloat per axis |
UInputModifierFOVScaling |
FOV Scaling | Scale by camera FOV for consistent angular speed across zoom levels |
UInputModifierToWorldSpace |
To World Space | 2D axis -> world space (up/down = world X, left/right = world Y) |
WASD -> Axis2D recipe (AccumulationBehavior = Cumulative):
W:SwizzleAxis(YXZ)→ Y=+1S:SwizzleAxis(YXZ)+Negate(bY)→ Y=-1D: none → X=+1A: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
// 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:
// 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:
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:
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/afterPossess, not inSetupPlayerInputComponent(called earlier on some paths). - Legacy binding on UEnhancedInputComponent:
BindAction(FName,...)andBindAxisare= delete. Compile error. SetDefaultInputComponentClassinDefaultInput.ini. - Triggered for a button press:
Triggeredfires every active frame. UseStartedfor press-once,Completedfor release. - Completed not firing with Ongoing: If any trigger reports
Ongoingthat frame,Completedis 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
DownwhenHoldis needed, orTriggeredwhenStartedis needed. Match trigger type to the interaction pattern:Startedfor single press,Triggeredfor continuous,Holdfor 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():
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,SetupPlayerInputComponentue-ui-umg-slate—SetInputMode(FInputModeUIOnly())/FInputModeGameAndUI(), cursor visibility, CommonUIUCommonActivatableWidgetstacksue-gameplay-abilities— binding Enhanced Input actions to GAS ability activation via input ID tags