git clone https://github.com/ComeOnOliver/skillshub
T=$(mktemp -d) && git clone --depth=1 https://github.com/ComeOnOliver/skillshub "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/quodsoler/unreal-engine-skills/ue-input-system" ~/.claude/skills/comeonoliver-skillshub-ue-input-system && rm -rf "$T"
skills/quodsoler/unreal-engine-skills/ue-input-system/SKILL.mdUE 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:
plugin is listed as enabledEnhancedInput- 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
of key-to-action entriesTArray<FEnhancedActionKeyMapping>
— per-profile key overrides for player remapping supportMappingProfileOverrides
:RegistrationTrackingMode
(default, first Remove wins) orUntracked
(IMC stays until Remove called N times, safe when multiple systems share it)CountRegistrations
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 |
|---|---|---|
| None -> Ongoing/Triggered | First frame of input; press-once actions |
| *->Triggered, Triggered->Triggered | Every active frame; continuous movement |
| Ongoing->Ongoing | Held but not yet triggered (charge build-up) |
| Ongoing->None | Released before trigger threshold |
| 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 |
|---|---|---|
| Down | Every frame input exceeds threshold (implicit default) |
| Pressed | Once on first actuation; holding does not repeat |
| Released | Once when input drops below threshold after actuation |
| Hold | After s; repeats every frame |
| Hold And Release | On release after holding s |
| Tap | Released within s |
| Repeated Tap | N taps within ( for double-tap) |
| Pulse | Repeatedly at s while held; optional |
| Chorded Action | Only fires while is active (Implicit type; auto-blocks solo key) |
| Combo (Beta) | All completed in order within 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 |
|---|---|---|
| Dead Zone | Zero input below ; remap to 1 at . Types: Axial, Radial, UnscaledRadial |
| Scalar | Multiply per axis by |
| Scale By Delta Time | Multiply by frame DeltaTime |
| Negate | Invert selected axes (, , ) |
| Swizzle Input Axis Values | Reorder axes; (default) swaps X/Y — maps 1D key onto Y of Axis2D action |
| Smooth | Rolling average over recent samples |
| Smooth Delta | Smoothed normalized delta; configurable interpolation (, , ease curves) |
| Response Curve - Exponential | `sign(x)* |
| Response Curve - User Defined | Separate per axis |
| FOV Scaling | Scale by camera FOV for consistent angular speed across zoom levels |
| To World Space | 2D axis -> world space (up/down = world X, left/right = world Y) |
WASD -> Axis2D recipe (
AccumulationBehavior = Cumulative):
:W
→ Y=+1SwizzleAxis(YXZ)
:S
+SwizzleAxis(YXZ)
→ Y=-1Negate(bY)
: none → X=+1D
:A
→ X=-1Negate(bX)
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
/afterBeginPlay
, not inPossess
(called earlier on some paths).SetupPlayerInputComponent - Legacy binding on UEnhancedInputComponent:
andBindAction(FName,...)
areBindAxis
. Compile error. Set= delete
inDefaultInputComponentClass
.DefaultInput.ini - Triggered for a button press:
fires every active frame. UseTriggered
for press-once,Started
for release.Completed - Completed not firing with Ongoing: If any trigger reports
that frame,Ongoing
is suppressed. Use separate actions for clean press/release events.Completed - No dead zone on gamepad sticks: Sticks produce non-zero resting values. Always add
per stick mapping.DeadZone(Radial) - Missing SwizzleAxis for WASD-to-2D: Keyboard produces 1D. Without
on W/S, forward/backward stays on X and is ignored by Axis2D forward movement.SwizzleAxis(YXZ) - 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
whenDown
is needed, orHold
whenTriggered
is needed. Match trigger type to the interaction pattern:Started
for single press,Started
for continuous,Triggered
for delayed activation.Hold
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
— PlayerController input lifecycle,ue-gameplay-framework
/Possess
,UnPossessSetupPlayerInputComponent
—ue-ui-umg-slate
/SetInputMode(FInputModeUIOnly())
, cursor visibility, CommonUIFInputModeGameAndUI()
stacksUCommonActivatableWidget
— binding Enhanced Input actions to GAS ability activation via input ID tagsue-gameplay-abilities