# Creates the Enhanced Input assets the first-person character loads: # /Game/Input/IA_Move Axis2D, WASD + left stick # /Game/Input/IA_Look Axis2D, mouse XY + right stick # /Game/Input/IA_Jump axis1d, Space / gamepad face button # /Game/Input/IMC_Default InputMappingContext holding the three mappings # # Run headless (imports/dirty packages are saved before exit): # /Engine/Binaries/Linux/UnrealEditor-Cmd -run=pythonscript \ # -script=/Tools/ue_create_input_assets.py -nullrhi -unattended -nopause \ # -stdout -FullStdOutLogOutput # # The result goes to /tmp/gigline/input-assets.txt because 5.8's pythonscript # commandlet swallows print() output with -stdout. # # Mapping choices: # - Look stays unit-free (no scalar modifier): the character applies its own # degrees-per-second, and a modifier here would double-scale. # - Move is a 2D axis: W = +Y, S = -Y, D = +X, A = -X via Negate on the Y axis. # For keyboard keys the Negate modifier's Y axis maps to the axis's Y. import unreal ASSET_DIR = "/Game/Input" _KEEP_ALIVE = [] def negate_y_modifier(): """A negate-by-Y modifier, built with its Y flag set and X/Z cleared. Returns the object plus its falsy reference; constructing the default modifier gives all axes true, so each flag is set explicitly. The caller owns keep-alive. """ neg = unreal.InputModifierNegate() neg.set_editor_property("x", False) neg.set_editor_property("y", True) neg.set_editor_property("z", False) return neg RESULT = "/tmp/gigline/input-assets.txt" lines = [] def log(msg): lines.append(str(msg)) unreal.log(str(msg)) def fresh(name, cls, factory): """Load the asset if it exists, otherwise create it. Deleting then recreating in the same commandlet run fails: create_asset returns None for a path deleted earlier in the process. Overwriting in place is also what keeps the assets' identities stable across reruns. """ path = f"{ASSET_DIR}/{name}" existing = unreal.EditorAssetLibrary.load_asset(path) if existing is not None: return existing return unreal.AssetToolsHelpers.get_asset_tools().create_asset( name, ASSET_DIR, cls, factory()) def save(asset): unreal.EditorAssetLibrary.save_loaded_asset(asset, only_if_is_dirty=False) def make_key(name): """Build an FKey by name. `unreal.InputCoreTypes` is not exposed to Python in 5.8 and `unreal.Key` does not accept a constructor argument, but the struct's `import_text` parses the key's export text, which is just its name. """ key = unreal.Key() key.import_text(name) return key def build(): if not unreal.EditorAssetLibrary.does_directory_exist(ASSET_DIR): unreal.EditorAssetLibrary.make_directory(ASSET_DIR) # --- actions --------------------------------------------------------- # Move keys are one-dimensional: W/S carry forward/back on the value's single # component and D/A carry strafe, combined by the character. A 2D value would # need per-key axis modifiers, and modifiers do not survive the save path here. ia_move = fresh("IA_Move", unreal.InputAction, unreal.InputAction_Factory) # Boolean (pressed = 1), not AXIS1D: the C++ handler reads Get() and # moves at full walk speed. An axis action from a keyboard key can deliver 0 # depending on the trigger, which showed up as "character stands, W does not # move" in QA. WASD are four separate per-direction actions below. ia_move.set_editor_property("value_type", unreal.InputActionValueType.BOOLEAN) save(ia_move) ia_look = fresh("IA_Look", unreal.InputAction, unreal.InputAction_Factory) ia_look.set_editor_property("value_type", unreal.InputActionValueType.AXIS2D) save(ia_look) ia_jump = fresh("IA_Jump", unreal.InputAction, unreal.InputAction_Factory) ia_jump.set_editor_property("value_type", unreal.InputActionValueType.BOOLEAN) save(ia_jump) # --- mapping context ------------------------------------------------- imc = fresh("IMC_Default", unreal.InputMappingContext, unreal.InputMappingContext_Factory) # Nine entries, one per key. No modifiers: the character negates S and A in # C++ instead, because a modifier object attached to a mapping struct # serialises as None on save in this build (the struct's object array does not # keep the modifier alive across the save path). One less moving part, and the # negation is visible where it is used. # Two actions feed the one-dimensional move: IA_Move for forward/back, and a # second action for strafe, both reporting on the value's single component. ia_strafe = fresh("IA_Strafe", unreal.InputAction, unreal.InputAction_Factory) ia_strafe.set_editor_property("value_type", unreal.InputActionValueType.BOOLEAN) save(ia_strafe) ia_back = fresh("IA_MoveBack", unreal.InputAction, unreal.InputAction_Factory) ia_back.set_editor_property("value_type", unreal.InputActionValueType.BOOLEAN) save(ia_back) ia_left = fresh("IA_StrafeLeft", unreal.InputAction, unreal.InputAction_Factory) ia_left.set_editor_property("value_type", unreal.InputActionValueType.BOOLEAN) save(ia_left) # Reset and pause are boolean: one press, no accumulator. ia_reset = fresh("IA_Reset", unreal.InputAction, unreal.InputAction_Factory) ia_reset.set_editor_property("value_type", unreal.InputActionValueType.BOOLEAN) save(ia_reset) ia_cam = fresh("IA_ToggleCamera", unreal.InputAction, unreal.InputAction_Factory) ia_cam.set_editor_property("value_type", unreal.InputActionValueType.BOOLEAN) save(ia_cam) ia_pause = fresh("IA_Pause", unreal.InputAction, unreal.InputAction_Factory) ia_pause.set_editor_property("value_type", unreal.InputActionValueType.BOOLEAN) save(ia_pause) mapping_table = [ (ia_move, "W"), (ia_back, "S"), (ia_strafe, "D"), (ia_left, "A"), (ia_move, "Gamepad_LeftY"), (ia_strafe, "Gamepad_LeftX"), (ia_look, "MouseX"), (ia_look, "MouseY"), (ia_look, "Gamepad_Right2D"), (ia_jump, "SpaceBar"), (ia_jump, "Gamepad_FaceButton_Bottom"), (ia_reset, "R"), (ia_pause, "Escape"), (ia_cam, "V"), ] mappings = [] for action, key_name in mapping_table: try: entry = unreal.EnhancedActionKeyMapping() entry.set_editor_property("action", action) entry.set_editor_property("key", make_key(key_name)) mappings.append(entry) except Exception as exc: # noqa: BLE001 log(f"skip {action.get_name()}/{key_name}: {exc}") # UE 5.8: the runtime reads DefaultKeyMappings.Mappings (FInputMappingContextMappingData), # NOT the deprecated 'mappings' array on the context. The deprecated array is # what editor UI shows and what the old script wrote, so the cooked game got 0 # runtime mappings (key->action lookup failed -> no input). Populate the new # struct field that the runtime actually consumes. default_mapping_data = imc.get_editor_property("default_key_mappings") if default_mapping_data is None: default_mapping_data = unreal.InputMappingContextMappingData() default_mapping_data.set_editor_property("mappings", mappings) # Mirror into the deprecated array so the editor UI and any legacy reader agree. imc.set_editor_property("mappings", mappings) imc.set_editor_property("default_key_mappings", default_mapping_data) save(imc) log(f"IA_Move={ia_move.get_path_name()} IA_Look={ia_look.get_path_name()} " f"IA_Jump={ia_jump.get_path_name()}") log(f"IMC_Default={imc.get_path_name()} mappings={len(mappings)}") log("assets=" + ",".join(unreal.EditorAssetLibrary.list_assets(ASSET_DIR))) try: build() except Exception as exc: # noqa: BLE001 - surface the failure in the result file import traceback log(f"FAILED: {type(exc).__name__}: {exc}") log(traceback.format_exc()) with open(RESULT, "w") as handle: handle.write("\n".join(lines))