Input System
Turian's input system decouples physical devices from game logic through named actions. You define actions in a .inputactions asset — keyboard, mouse, and gamepad bindings all map to the same action name — and your scripts only ever ask "is the jump action pressed?", never "is Space held?".
Quick start
1. Create an Input Actions asset
In the Asset Browser, right-click → Create → Input Actions. This writes a default .inputactions file (ZON format) into the selected folder.
Double-click the file to open the Input Actions Editor.
2. Define your actions
The editor shows a list of actions. Each action has:
- Name — the string your script will use (e.g.
jump,move,look). - Kind —
button(on/off),axis(1D −1…+1), orvector(2D, x/y). - Roles —
Positive/Negativefor axis;Right/Left/Up/Downfor vector;Buttonsfor button. Each role holds up to 4 bindings.
For each binding choose a device and a code:
| Device | Code examples |
|---|---|
key |
a, space, left_shift, left_ctrl |
mouse |
right, left, middle |
gamepad_button |
south, east, left_shoulder, left_trigger |
gamepad_axis |
left_x, left_y, right_x, right_y, left_trigger, right_trigger |
For gamepad_axis, the + checkbox selects the positive direction of that axis (e.g. stick-right, trigger-pressed). Uncheck it for the negative direction.
Click Save to write the ZON file and re-cook it into the project.
3. Package the asset
The game loader discovers every .inputactions asset in your project automatically — no code needed. The asset is applied to the shared engine.Input at startup.
Reading actions in a script
Scripts receive the engine.Frame context through update. frame.input is an *const engine.Input:
{% raw %}
const engine = @import("engine");
pub const PlayerController = struct {
pub const is_component = true;
speed: f32 = 5.0,
pub fn update(self: *@This(), frame: engine.Frame) void {
const input = frame.input;
const dt = frame.time.delta;
const t = frame.transform;
// Button action — digital on/off
if (input.wasPressed("jump")) {
// rising edge only
}
// Axis action — 1D continuous value
const throttle = input.axis("throttle"); // -1..+1
// Vector action — 2D (e.g. WASD / left stick)
const move = input.vector("move"); // .x = right, .y = forward
t.position.x += move.x * self.speed * dt;
t.position.z += move.y * self.speed * dt;
}
};
{% endraw %}
Action query API
| Method | Returns | Meaning |
|---|---|---|
isPressed(name) |
bool |
action value ≥ threshold (held) |
wasPressed(name) |
bool |
rose above threshold this frame |
axis(name) |
f32 |
1D value, −1…+1, with deadzone |
vector(name) |
engine.Vector2 |
2D value, clamped to unit circle |
All four work the same regardless of whether the binding is a key, mouse button, or gamepad stick — the action kind just determines which query makes semantic sense.
Raw device queries
For UI or debug code that needs device-specific state, frame.input also exposes raw queries:
{% raw %}
// Keyboard
if (input.isKeyDown(.space)) { ... }
if (input.wasKeyPressed(.a)) { ... }
// Mouse
const delta = input.mouseDelta(); // .x / .y pixels this frame
const wheel = input.wheelDelta(); // scroll ticks this frame
if (input.isMouseDown(.right)) { ... }
// Gamepad
const lx = input.gamepadAxis(.left_x); // -1..+1
if (input.isGamepadButtonDown(.south)) { ... }
{% endraw %}
Example: freefly camera
The example project ships FreeflyCamera.zig, which drives a no-clip camera from both keyboard+mouse and a gamepad using the same action names:
move — WASD / left stick
look_axis — right stick (analog, framerate-independent)
ascend — Space / gamepad A / right trigger
descend — LShift / gamepad B / left trigger
boost — LCtrl / left shoulder
look — hold Right Mouse to look with mouse delta
The script reads actions only — it never mentions a key code. Rebind everything in player-controls.inputactions without touching FreeflyCamera.zig.
Runtime rebinding
The engine exposes two functions for in-game "press a key to rebind" menus:
{% raw %}
// In a settings menu update:
if (frame.input.captureBinding()) |binding| {
// User pressed something — apply it
_ = frame.input.rebind("jump", .pos, 0, binding);
}
{% endraw %}
captureBinding() returns the first newly-pressed key, mouse button, gamepad button, or gamepad axis that crossed the press threshold. rebind replaces or appends a binding by action name, role (.pos/.neg/.up/.down), and index.
Persistence: saving remapped bindings across sessions requires the settings system (issue #13, not yet implemented). Track progress in issue #42.
engine.Frame reference
Every lifecycle hook that takes a second parameter receives engine.Frame:
{% raw %}
pub fn update(self: *@This(), frame: engine.Frame) void
pub fn start(self: *@This(), frame: engine.Frame) void
// awake, enable, disable, destroy also accept frame
{% endraw %}
| Field | Type | Meaning |
|---|---|---|
frame.time |
engine.Time |
delta, elapsed, frame counter |
frame.input |
*const engine.Input |
action queries + raw device state |
frame.transform |
*engine.Transform |
this object's position/rotation/scale |
frame.objects |
[]engine.SceneNode |
all objects in the scene |
frame.services |
*engine.Services |
user-registered service registry |
Use frame.service(MyService) for a typed ?*MyService lookup into frame.services.
The legacy
update(self, time: engine.Time)signature is still supported for backward compatibility.