Skip to content
hikari
RenderingEditorAIToolchainDocumentation
GitHub
hikari

© 2026 Flying Rat Studio.
All rights reserved.

Explore the engineDocumentationContributorsLicenseBack to top
Documentation / Systems
Browse docs
Overview
Tutorials16
OverviewFirst game projectFirst entityAuthored user_data and the inspectorFirst mesh and materialFirst physics body and triggerFirst character controllerFirst session servicesFirst UIFirst input actionFirst messagesPlay, Edit, and scenesFirst runtime spawnFirst motionFirst skeletal animationAssets in Play (soft refs and hot reload)Dynamic editor recompile
Guides16
OverviewDevelopment guideGameplay APIChoosing component storageBuild and packagingProject filePluginsHikari Plugin APIData-driven content, JSON, and pathsScripting with KawaShader authoringTime of dayUser interfaceUI layoutUI widgetsMigration from Unity / UnrealActor and component lifecycle
Systems28
OverviewArchitectureApplication lifecycleFrontends and driversPlatforms and supportSession services and cross-scene stateScenes and gameplayActor communication (hi.actors)Game-facing refsSave / replication wire versionCoordinate space and camera conventionsRenderingRenderer architecture mapFrame governorGPU particlesVisual ZonesVolumetric mediaInputAudioPhysicsMotion KitTemporal KitAssets and ShinraPrefabsAsset residencyAsset formats (Shinra pipeline)UI and editorEditor asset hot reloadEditor Project Selector
Language reference2
OverviewAkari language referenceKawa language reference
Engine overview
Start exploring
  • No matching sections. Try fewer words or another topic.
NavigateEnter Openesc Close
Overview
Tutorials16
OverviewFirst game projectFirst entityAuthored user_data and the inspectorFirst mesh and materialFirst physics body and triggerFirst character controllerFirst session servicesFirst UIFirst input actionFirst messagesPlay, Edit, and scenesFirst runtime spawnFirst motionFirst skeletal animationAssets in Play (soft refs and hot reload)Dynamic editor recompile
Guides16
OverviewDevelopment guideGameplay APIChoosing component storageBuild and packagingProject filePluginsHikari Plugin APIData-driven content, JSON, and pathsScripting with KawaShader authoringTime of dayUser interfaceUI layoutUI widgetsMigration from Unity / UnrealActor and component lifecycle
Systems28
OverviewArchitectureApplication lifecycleFrontends and driversPlatforms and supportSession services and cross-scene stateScenes and gameplayActor communication (hi.actors)Game-facing refsSave / replication wire versionCoordinate space and camera conventionsRenderingRenderer architecture mapFrame governorGPU particlesVisual ZonesVolumetric mediaInputAudioPhysicsMotion KitTemporal KitAssets and ShinraPrefabsAsset residencyAsset formats (Shinra pipeline)UI and editorEditor asset hot reloadEditor Project Selector
Language reference2
OverviewAkari language referenceKawa language reference
Engine overview
Systems7 min read

Input

On this page
On this pageModelFrame semanticsMouse wheel unitsPointer mode (cursor capture)Input contextsAction mapsAuthored formatPer-user override formatEditor input ownershipText entryExtending input Back to top

Hands-on walkthrough: Tutorials — First input action.

Model

The engine separates device sampling from game actions.

  • An InputDriver gathers platform keyboard, mouse, and gamepad state.
  • InputSystem.syncFrame keeps current and previous snapshots.
  • ActionRegistry loads named bindings from JSON.
  • ActionState exposes resolved gameplay intent to the world/game.

The public entry point is src/hikari/src/input/input.zig. Frame data and key-trigger helpers are in input_frame.zig; action binding parsing and evaluation are in action_binding.zig, action_registry.zig, and input_actions_json.zig.

Frame semantics

At the start of a frame, syncFrame copies current device values to their previous slots and asks the driver for a new snapshot. Read helpers can therefore distinguish a held value from transitions without querying the operating system again mid-frame.

Input remains backend-neutral above the driver. Game and scene code read the normalized snapshot/action state; they do not call Cocoa, Win32, Metal, or D3D12 APIs.

Mouse wheel units

MouseFrame.wheel_y is a per-frame signed scroll sample. Its unit depends on MouseFrame.flags:

FlagMeaningTypical source
mouse_flag_wheel_preciseLayout points (trackpad stream)macOS trackpad
mouse_flag_wheel_momentumOS inertia after fingers up (no live user sample this frame)macOS momentumPhase
neitherDiscrete line/notch units (±1 per click)Windows WM_MOUSEWHEEL, macOS mouse wheel

UI converts with ui.scroll_physics.wheelDeltaPoints / wheelDeltaPointsSens (precise path applies gain, not wheel_step). Momentum must not hold rubber-band open — only user samples do; outward momentum past an edge is ignored and damps residual velocity so spring-back can run. Non-UI consumers (fly camera) should use wheelLines.

Pointer mode (cursor capture)

PointerMode is free or captured. Captured hides the OS cursor and reports relative motion through the usual mouse position stream (mouseDelta / action delta_x/delta_y stay valid). Platform backends (macOS CoreGraphics association + Windows raw input) own hide/clip; focus loss always frees.

SurfaceCall
Game SDKhi.world().setPointerMode(.captured) / .free
Host inputInputSystem.setPointerMode

Editor play: Esc soft-unlocks the cursor (docks stay usable) without clearing the game’s requested mode; a left click on the scene viewport re-arms capture. Editor edit: temporary capture while fly-camera navigation is active. Standalone games typically capture when look is live and free when a settings/modal UI is open.

Input contexts

Code: src/hikari/src/input/input_context.zig.

A context is a named layer saying which actions are readable while it is active. Without one, every reader has to ask "…but is a menu open?" at each call site, and the day someone forgets, the player shoots through the pause screen.

Resolution walks layers highest priority first (equal priority → most recently pushed first):

  • a layer that claims the action → readable;
  • an exclusive layer that does not claim it → not readable, walk stops;
  • falling off the bottom → readable.

So an empty stack allows everything and adopting contexts never changes an existing game's input. A non-exclusive layer only adds actions (build mode on top of normal control); an exclusive layer masks everything below (pause menu, dialogue, cutscene, text entry).

For each action read, the first matching decision wins:

Diagram
Diagram source
flowchart TD
    read["Read an action"] --> next{"Another context layer?"}
    next -->|No| allow["Allow the read"]
    next -->|Highest remaining priority| claim{"Layer claims this action?"}
    claim -->|Yes| allow
    claim -->|No| exclusive{"Layer is exclusive?"}
    exclusive -->|Yes| block["Block the read"]
    exclusive -->|No · continue downward| next

Storage is fixed and inline in World — 8 layers × 24 actions, no allocation, so a push cannot fail for memory reasons mid-frame. Over capacity returns StackFull / ContextTooLarge rather than truncating; both mean a missing pop or an oversized layer.

SurfaceCall
Game SDKw.pushInputContext(.{ .name = "menu", .exclusive = true, .pointer = .free }) / popInputContext / hasInputContext / clearInputContexts / inputAllows
KawaInput.push_context({ name = "menu", exclusive = true, pointer = "free" }) / pop_context / has_context / clear_contexts / allows

Both surfaces share one stack on World, so a layer pushed from either gates both. Every action read — pressed, held, value, and released — passes through it. Releases are gated deliberately: a button released while a menu is open belongs to the menu, and leaking it is how a weapon fires on the frame the pause screen closes.

pointer on a layer sets cursor mode while it is the topmost layer declaring one, which is usually the whole reason a menu layer exists. It replaces hand-written "am I in a menu" pointer syncing.

Raw input needs a binding-less action. Mouse delta and gamepad axes are not actions, so nothing gates them automatically. Declare a name like look in the gameplay layer with no binding and ask inputAllows("look") (Kawa: Input.allows("look")) before consuming raw look input.

The stack outlives the scene. Clear it on scene unload, or a reload stacks a second set of layers on the first.

Action maps

Authored actions always live at configs/input_actions.json (URI config://input_actions.json, kind com.hikari.config.input_actions, schema version 2). The authored/staged filesystem document wins over its AssetStore blob. There is no path in startup policy.

The authored document is the action schema: it decides which action ids exist and supplies every default binding. Runtime rebinding never replaces it. User changes live separately under the product's writable root as sparse configs/input_bindings.json overrides (kind com.hikari.config.input_bindings, version 1). Only ids still present in the authored registry are applied; removed ids are ignored and omitted from the next save, while newly authored ids immediately receive their defaults.

An untouched session creates no user file. setActionBinding marks the live registry dirty only when the value changes, and shutdown writes only bindings that differ from authored defaults. Returning a binding to its authored value therefore removes its override; when none remain, the user file is deleted. Legacy per-user input_actions.json snapshots are deliberately ignored.

Authored format

Bindings name controls rather than exposing runtime bit masks. Fields are sparse: omit an unused device or a value that should keep its default.

json
{
  "kind": "com.hikari.config.input_actions",
  "version": 2,
  "actions": [
    {
      "id": "move_forward",
      "bindings": {
        "keys": ["w", "up"],
        "gamepad_buttons": ["dpad_up"]
      }
    },
    {
      "id": "move_x",
      "bindings": {
        "negative_keys": ["a", "left"],
        "positive_keys": ["d", "right"],
        "gamepad_axis": "left_x"
      }
    },
    {
      "id": "fire",
      "bindings": {
        "mouse_buttons": ["left"],
        "gamepad_buttons": ["r1"]
      }
    }
  ]
}

The accepted names are:

FieldValues
keys, negative_keys, positive_keysw, a, s, d, q, e, r, f, x, c, v, p, y, z, f2–f6, tab, enter, space, escape, up, down, left, right, shift, primary_modifier, backspace, delete, home, end
gamepad_buttonsa, b, x, y, l1, r1, l3, r3, menu, options, home, dpad_up, dpad_down, dpad_left, dpad_right
mouse_buttonsleft, right, middle
gamepad_axisleft_x, left_y, right_x, right_y, left_trigger, right_trigger
mouse_axisdelta_x, delta_y, wheel_y

Each named array is OR-combined. negative_keys subtract one and positive_keys add one, which makes a scalar action such as move_x directly usable with actionValue. axis_scale defaults to 1, deadzone to 0.15, and player to 0. A binding-less action is simply "bindings": {}.

Version 1 numeric-mask documents are intentionally rejected. Hikari is unreleased, so authored defaults should be converted to the named version 2 format instead of carrying a legacy reader.

Per-user override format

The engine writes this document; projects do not ship one:

json
{
  "kind": "com.hikari.config.input_bindings",
  "version": 1,
  "bindings": [
    {
      "action": "move_forward",
      "binding": { "keys": ["up"] }
    }
  ]
}

Editor input ownership

Editor retained UI and viewport navigation have separate ownership. Interactive editor panels consume their own input first; a hovered panel, including its wheel gesture, must not also navigate the viewport camera. EditorViewportController owns edit-camera movement through its narrow view-provider interface (scene_input_blocked + press-origin latches).

In Play, game UI and entity scripts receive scene-local dimensions and remapped input inside the viewport (World.gameInputFrame):

Pointer modeSample outside scene rect
FreeButtons, wheel, and inside cleared — dock/inspector clicks never reach mouse_left / look scripts
CapturedMotion and buttons kept (virtual aim point is unbounded; chrome is already inert)

Do not re-implement "am I over chrome?" in game scripts — the remapped frame is the ownership answer.

Text entry

UI text fields read printable characters from KeyboardFrame.text each frame. Focus, caret, Backspace/Delete, and Left/Right live in the UI layer; the platform only fills the text buffer and key bits.

Desktop platforms stream physical keyboard characters into that buffer. Consoles that cannot type directly open a native virtual keyboard through a short text-input session on InputDriver:

  • beginTextInput / endTextInput — edge-triggered when any focused editable field wants text
  • Session hosts call SessionCore.syncTextInput once per frame after all UI (game + editor) has run
  • Virtual-keyboard results still land in KeyboardFrame.text (streaming or bulk inject on dismiss)

Do not add a second text path in UI code. Platform SDKs own soft keyboards; UI owns buffers and caret.

Extending input

Add a platform device implementation behind InputDriver, normalize its state into InputState, and expose bindings/actions through the shared action layer. Do not make scene code platform-aware or build a second action-resolution path in a driver.

PreviousVolumetric mediaNext Audio

Documentation follows the current engine checkout.

Snapshot cc148c75Source docs/systems/input.md
On this pageModelFrame semanticsMouse wheel unitsPointer mode (cursor capture)Input contextsAction mapsAuthored formatPer-user override formatEditor input ownershipText entryExtending input Back to top