Input code becomes difficult to test when game rules receive engine events directly. A key press, pointer event, or controller axis then carries engine-specific timing, device, and callback details into the same code that resolves turns, cooldowns, and inventory use. The result is often hard to replay outside a running scene.
A more durable boundary converts engine input into a small command record before it reaches game rules. The game consumes records such as move, wait, and use-item; each record has plain data, a schema version, and an explicit simulation step. The adapter changes when an engine changes, while the command executor and its tests can remain stable.
This is not a promise of whole-game determinism. Physics, random-number streams, clock reads, content versions, and network order still need their own controls. The value of the pattern is narrower and useful: it makes the input-to-rule boundary inspectable, serializable, and replayable.
Implementation
Define commands in the language of the game rather than the language of a device. Prefer a compact data shape such as:
{ version: 1, step: 84, type: "move", dx: 1, dy: 0 }
{ version: 1, step: 85, type: "use-item", itemId: "healing-potion" }Build the flow in four layers:
- An engine adapter receives raw callbacks or event objects.
- A mapper validates the event and produces a domain command.
- A deterministic executor applies one command to an explicit state.
- A log stores command order, schema version, and any checkpoint identifier.
Keep engine objects, callbacks, scene references, and screen coordinates out of the log. Use stable identifiers and primitive values instead. A command that can survive JSON serialization is far easier to use in a regression test, bug report, or migration tool.
Unity Input Actions are designed to separate an input purpose from its device controls, while Unreal Enhanced Input exposes Input Actions and runtime-adjustable mapping contexts. Both support an adapter that maps engine input to game-specific command data instead of letting bindings dictate game-rule APIs.
For a turn-based game, advance the executor once per accepted command. For a real-time game, record a fixed simulation step with each command and consume records at that step. Make the ordering rule explicit: Unity documents that actions reported in the same frame do not have a defined reporting order, so arrival order from a callback is not an adequate cross-platform contract.
Tradeoffs
The boundary adds a schema, conversion code, and version migrations. That cost is worthwhile when a project needs repeatable bug reports, rule-level tests, input rebinding, or an engine transition. It is less attractive for throwaway prototypes whose rules are tightly coupled to one scene and have no replay or test requirement.
Do not confuse this log with a complete engine replay. Unreal's Replay System uses a DemoNetDriver and replicated gameplay data to reconstruct a session. A command log is smaller and better suited to rule-level diagnosis, but it does not replace a replicated-world replay when that is the product requirement.
Adapters should remain intentionally engine-specific. Godot's InputEvent hierarchy exposes device and action information, whereas Unreal's action values and trigger states have a different lifecycle. A shared command schema is useful; a fake universal input API usually hides differences that still matter during debugging.
Failure modes
- Reading wall-clock time inside a command handler makes the same log produce different results. Pass a tick or step as data instead.
- Using an unseeded random source makes divergence impossible to localize. Record a seed or save the random-stream state at checkpoints.
- Silently ignoring an unknown command hides corrupted logs and incomplete migrations. Reject it with a clear error.
- Logging engine references or closures prevents durable serialization. Store stable IDs and values only.
- Treating pointer positions as game intent can make UI layout changes alter replays. Convert them to a deliberate domain choice before recording.
- Keeping one unbounded session log makes inspection and recovery expensive. Split logs at checkpoints and retain a snapshot compatible with the command schema.
Testing
Start with a pure executor test. Run the same initial state and command list twice, then compare final states. Serialize the list to JSON and deserialize it before the second run to catch accidental non-data fields.
Add boundary tests for each adapter: one representative engine event should create exactly one expected command, and unsupported events should be rejected or ignored by an explicit rule. Test migration functions with stored records from every supported schema version.
Use a divergence test for diagnosis: replay from a checkpoint, compare state after every command, and report the first mismatching step. This turns a vague report such as "the run desynced" into a reproducible input and state boundary.
Production considerations
Store a command schema version with every record and preserve a small migration path for old saves. Include build or content identifiers in a checkpoint when balance data can change command outcomes. Keep checkpoints frequent enough that replaying a long session remains practical, but measure size and restore time before choosing an interval.
Treat the command log as diagnostic data. Avoid recording account identifiers, free-form chat, or raw device details unless they are necessary and have an appropriate retention policy. For online games, decide separately whether commands are client intent, server authority, or debug evidence; the command format alone does not establish trust.
Instrument the executor with command count, restore duration, decode failures, and first-divergence step. Those measurements reveal whether log size, migration, or simulation work is the real limitation before introducing compression or a binary format.
Sources
- https://docs.unity3d.com/Packages/com.unity.inputsystem@1.11/manual/Actions.html - Unity documentation for action-to-binding separation, callbacks, and the undefined ordering of multiple actions in one frame.
- https://dev.epicgames.com/documentation/unreal-engine/enhanced-input-in-unreal-engine - Unreal documentation for Input Actions, mapping contexts, values, and trigger states.
- https://dev.epicgames.com/documentation/en-us/unreal-engine/using-the-replay-system-in-unreal-engine - Unreal documentation describing Replay System recording through DemoNetDriver and replicated data.
- https://docs.godotengine.org/en/stable/classes/class_inputevent.html - Godot documentation for the InputEvent base type, device details, and action-oriented event queries.
