🧠 Brains complement: IF an
unreal-engine-skills-managertool (external MCP) exists in this session, call it with{action: "load", skill: "enhanced-input"}for UE domain knowledge on this topic — correct APIs, architecture, best practices — and treat it as the rubric for any review / "best practices" question. If no such tool is available (e.g. running under Claude Code or Codex without that MCP), skip this line entirely and proceed with this skill alone — do NOT attempt the call.
Enhanced Input Skill
Critical Rules
🚨 Real Python method names — do NOT search for the docstring's "action" names
The InputService class docstring lists MCP-style action ids (action_create,
mapping_list_contexts, action_configure, ...). Those are not the Python method
names — discover_python_class(method_filter=...) matches nothing for them. The complete
real API is below; you rarely need a discovery call at all:
| Area | Methods |
|---|---|
| Reflection | discover_types(), get_available_keys(filter=""), get_available_modifier_types(), get_available_trigger_types() |
| Actions | create_action(name, path, value_type="Axis1D"), list_input_actions(), get_input_action_info(action_path) → info or None, configure_action(action_path, consume_input=True, trigger_when_paused=False, description=""), input_action_exists(action_path) |
| Contexts | create_mapping_context(name, path, priority=0), list_mapping_contexts(), get_mapping_context_info(context_path) → info or None, mapping_context_exists(context_path) |
| Mappings | get_mappings(context_path), add_key_mapping(context_path, action_path, key_name), remove_mapping(context_path, mapping_index), key_mapping_exists(context_path, action_path) |
| Modifiers | add_modifier(context_path, mapping_index, modifier_type), remove_modifier(context_path, mapping_index, modifier_index), get_modifiers(context_path, mapping_index) |
| Triggers | add_trigger(context_path, mapping_index, trigger_type, properties_json=""), add_action_trigger(action_path, trigger_type, properties_json="") → JSON, remove_trigger(context_path, mapping_index, trigger_index), get_triggers(context_path, mapping_index) |
create_action value types are the strings discover_types() returns: "Boolean"
(alias "Digital"), "Axis1D", "Axis2D", "Axis3D". The create_* folder may be a bare
folder ("Input" goes under /Game) or start with its mount point (/Game/..., a plugin root);
/Engine, /Script and /Temp are refused, and so is an invalid asset name (empty, a space,
.), each with the reason in error_message. Modifiers and triggers are
addressed by index on the mapping, so get_mappings / get_modifiers /
get_triggers first, then add/remove by index.
⚠️ Property Names on Info Structs
| Struct | WRONG | CORRECT |
|---|---|---|
InputTypeDiscoveryResult | value_types | action_value_types |
KeyMappingInfo | key | key_name |
InputModifierInfo | modifier_type | type_name or display_name |
InputTriggerInfo | trigger_type | type_name or display_name |
⚠️ Value Types
| Value Type | Use Case |
|---|---|
Boolean | Simple press/release (Jump, Fire) |
Axis1D | Single axis (Throttle, Zoom) |
Axis2D | Two axes (Move, Look) |
Axis3D | Three axes (3D manipulation) |
⚠️ Key Names
Keyboard: SpaceBar, LeftShift, W, A, S, D, F1, Enter, Escape, BackSpace...
Mouse: LeftMouseButton, RightMouseButton, MouseScrollUp
Gamepad: Gamepad_FaceButton_Bottom, Gamepad_LeftThumbstick, Gamepad_LeftTrigger, Gamepad_RightTrigger
Paired axes (use these for Axis2D bindings): Mouse2D (mouse look), Gamepad_Left2D / Gamepad_Right2D (analog sticks)
Verify any other name with get_available_keys("thumb") (substring filter) instead of guessing.
⚠️ Triggers and Modifiers
Triggers: Pressed, Released, Down, Hold, Tap, Pulse
Modifiers: Negate, DeadZone, Scalar, SwizzleAxis (also: Smooth, SmoothDelta, ScaleByDeltaTime — use get_available_modifier_types() for the full set)
Workflows
Create Input Action
pythonimport unreal result = unreal.InputService.create_action("Jump", "/Game/Input", "Boolean") if result.success: unreal.EditorAssetLibrary.save_asset(result.asset_path)
Create Mapping Context
pythonimport unreal result = unreal.InputService.create_mapping_context("Default", "/Game/Input", 0) unreal.EditorAssetLibrary.save_asset(result.asset_path)
Add Key Mapping
pythonimport unreal context_path = "/Game/Input/IMC_Default" action_path = "/Game/Input/IA_Jump" unreal.InputService.add_key_mapping(context_path, action_path, "SpaceBar") unreal.EditorAssetLibrary.save_asset(context_path)
Add Triggers and Modifiers
pythonimport unreal context_path = "/Game/Input/IMC_Default" action_path = "/Game/Input/IA_Fire" unreal.InputService.add_key_mapping(context_path, action_path, "LeftMouseButton") # Get mapping index mappings = unreal.InputService.get_mappings(context_path) mapping_index = len(mappings) - 1 # Add trigger/modifier using mapping index unreal.InputService.add_trigger(context_path, mapping_index, "Pressed") unreal.InputService.add_modifier(context_path, mapping_index, "DeadZone") unreal.EditorAssetLibrary.save_asset(context_path)
Trigger Settings
properties_json sets the new trigger's settings, by C++ name or snake_case
(HoldTimeThreshold or hold_time_threshold, bIsOneShot or is_one_shot). Only the
settings the trigger's Details panel shows can be set: runtime state such as HeldDuration,
LastValue or bShouldAlwaysTick is refused ("not a trigger setting"), and a number outside
the property's ClampMin/ClampMax is refused with the allowed range (hold_time_threshold
must be >= 0). An unknown name, a refused setting or a bad value fails the call and adds
nothing. add_action_trigger puts the trigger on the Input Action itself, so it applies to
every mapping of the action; it returns PIE_ACTIVE while a play session runs (stop PIE
first). Neither saves the asset, and a running PIE session keeps its copy of the triggers:
restart PIE to see a change.
pythonimport unreal # Hold 0.4 s before the charged attack fires, once per hold unreal.InputService.add_trigger("/Game/Input/IMC_Default", 0, "Hold", '{"hold_time_threshold": 0.4, "is_one_shot": true}') # A quick tap on the action itself, for every key bound to it result = unreal.InputService.add_action_trigger("/Game/Input/IA_Dash", "Tap", '{"tap_release_time_threshold": 0.25}') print(result) # {"success": true, "trigger_index": 0, ...} or an error_code unreal.EditorAssetLibrary.save_asset("/Game/Input/IA_Dash")
Get Mappings Info
pythonimport unreal mappings = unreal.InputService.get_mappings("/Game/Input/IMC_Default") for m in mappings: print(f"Action: {m.action_name}, Key: {m.key_name}") info = unreal.InputService.get_input_action_info("/Game/Input/IA_Jump") if info: print(f"Action: {info.action_name}, ValueType: {info.value_type}")
Discover Available Types
pythonimport unreal types = unreal.InputService.discover_types() print(f"Value Types: {types.action_value_types}") print(f"Modifiers: {types.modifier_types}") print(f"Triggers: {types.trigger_types}") keys = unreal.InputService.get_available_keys()
Sample scripts (run via execute_python_code)
scripts/setup_input.txt— create an Input Action + Mapping Context and bind a key.
PIE input injection (issue #550)
Test input-driven gameplay WITHOUT remapping game assets or OS-level SendKeys — inject directly into the running PIE session (no OS window focus needed):
pythonimport unreal # Enhanced Input action, QUEUED for the next input tick and released on the one after. # X/Y/Z map to the value type. Read the result in a later call: nothing ticks while this one runs. print(unreal.InputService.inject_action("/Game/Input/IA_Fire", 1.0)) # Hold an action for 1.5 s of real time (a guard, a charge, a Hold trigger); returns at once. print(unreal.InputService.inject_action_for("/Game/Input/IA_Block", 1.5)) print(unreal.InputService.stop_injection("/Game/Input/IA_Block")) # release early # Raw key through Slate to the game viewport: "tap" (default), "down", "up", or "hold". print(unreal.InputService.inject_key("SpaceBar")) print(unreal.InputService.inject_key("RightMouseButton", "hold", 1.5))
All return JSON with success/error_code (PIE not running, NO_LOCAL_PLAYER /
NO_PLAYER_CONTROLLER while PIE or a client's join is still starting — retry on a later call —,
unknown key, BAD_DURATION outside 0.05 to 60 s). inject_action reports queued: true: it was
queued, not yet applied. The action calls take pie_instance to pick a PIE world: -1, the default,
is the first PIE world with a local player (the lowest instance number), and replies report the
instance actually used. A hold is one hold per action and PIE world whatever the path spelling, so
stop_injection finds it either way; holding a held key again extends it (extended: true); ending
PIE drops every hold. See the pie-testing skill for the full verification loop.


