---
title: "Input actions for keyboard, gamepad and tests"
url: https://recursiveai.org/onegame/cookbook/input-actions/
description: "Declare a C# game’s input as named actions with default bindings for the keyboard and a gamepad, read them in view code, test them with scenario steps for…"
version: "1.2.0"
---

# Input actions for keyboard, gamepad and tests

Declare a C# game’s input as named actions with default bindings for the keyboard and a gamepad, read them in view code, test them with scenario steps for keys, sticks and actions, inspect them live, and load a player’s saved bindings.

New in 1.2.0. A game declares actions such as `move` and `boost` once, each with its default bindings, and reads the actions rather than keys. The same declaration answers the keyboard, any connected gamepad, Steam Input and the test harness. The scaffold that `onegame new` writes already works this way. This recipe starts from it, as `onegame new pilot`.

## Declare and read

In `Game.cs`, the scaffold’s two methods on the game’s `GameDefinition`, with loading of saved bindings added at the end of `DefineActions`:

```csharp
    // Input is declared as actions with default bindings. One declaration answers the keyboard, a gamepad and the test harness.
    public override void DefineActions(ActionMap actions)
    {
        actions.Context("play", c => c
            .Vector("move", Bind.Wasd, Bind.Arrows, Bind.LeftStick)
            .Button("boost", Bind.Key(Key.Space), Bind.Pad(GamepadButton.South)));

        // The player's saved bindings, on top of the defaults. A missing file leaves the defaults.
        var loaded = actions.Load(ActionMap.DefaultPath(Name));
        foreach (var problem in loaded.Problems)
        {
            Console.Error.WriteLine("bindings: " + problem);
        }
    }

    // View code reads the actions and sends the ship a command when they change.
    public override void OnEachFrame(GameInstance game)
    {
        var ship = ShipOf(game);
        var move = game.Actions.Vector("move");
        int dx = (int)MathF.Round(move.X), dy = (int)MathF.Round(move.Y);
        var boost = game.Actions.Held("boost");
        if (dx != ship.Dx || dy != ship.Dy || boost != ship.Boost)
        {
            ship.Send.Steer(dx, dy, boost);
        }
    }
```

- **Kinds.** A `Button` is held, pressed or released. An `Axis` runs from -1 to 1. A `Vector` has `X` and `Y`, with y pointing down and length at most 1. Axes and vectors are also `Held` past a threshold (0.5 by default), so a stick can work as a button.
- **Bindings.** `Bind.Key`, `Bind.Pad`, `Bind.PadAxis`, `Bind.Mouse`, `Bind.Wheel`, `Bind.Touch`, and composites: `Bind.Wasd`, `Bind.Arrows`, `Bind.DPad`, `Bind.LeftStick`, `Bind.RightStick`, `Bind.Pair(negative, positive)`. An action takes several bindings, and the strongest wins. A binding of the wrong shape throws when it is declared.
- **Commands, not state.** Actions are view state. `OnEachFrame` reads them and sends the ship a `Steer` command when they change, so recordings, replays and the network see commands, never keys.
- **Contexts.** `actions.Context("menu", ...)` groups actions, and `.OnScreens("menu")` turns a context on only while that screen is open. `c.Tune("move", deadZone: 0.25f)` changes a dead zone or threshold.

## Test it three ways

One scenario steers the ship with a held key, then the left stick, then the actions themselves:

**`scenarios/three-ways.yaml`**

```yaml
name: three-ways
description: The keyboard, a gamepad and the move action steer the ship through the same declaration.
steps:
  - action: { name: move, state: up }     # start clean: in 1.2.0 a held action carries over from the scenario before
  - action: { name: boost, state: up }
  - press: { key: d, state: down }        # hold D
  - step: 10
  - press: { key: d, state: up }
  - step: 2
  - expect:
      state: { entity: Ship, property: X, equals: 15 }
  - pad: { axis: leftx, value: 1 }        # push the left stick right
  - step: 10
  - pad: { axis: leftx, value: 0 }
  - step: 2
  - expect:
      state: { entity: Ship, property: X, equals: 30 }
  - pad: { button: south, state: down }   # hold boost on the pad
  - action: { name: move, state: down, x: 0, y: 1 }
  - step: 10
  - expect:
      action: { name: boost, held: true }
  - expect:
      action: { name: move, held: true, y: 1 }
  - pad: { button: south, state: up }     # let go of everything
  - action: { name: move, state: up }
  - step: 2
expect:
  - state: { entity: Ship, property: Boost, equals: false }
  - state: { entity: Ship, property: Y, equals: 30 }
```

```shell
onegame test --no-unit
```

```text
PASS ship-looks-right (375 ms, tick 40, hash a5a533b83e128e35)
PASS steer-ship (4 ms, tick 22, hash 19dbaa512f757607)
PASS three-ways (7 ms, tick 36, hash 1619491e22571156)
3 passed, 0 failed
```

Holding D for ten frames and pushing the stick for ten move the ship the same 15 units at the normal speed of 1.5 a tick. With boost held, ten frames down make 30. Run alone, `three-ways` gives the same hash.

- `press: { key: d, state: down }` holds a key until `state: up`. A bare `press: d` taps it.
- `pad:` drives the harness’s virtual gamepad: `button` takes the names `nav:` uses (`south`, `east`, `start`...), and `axis` takes `leftx`, `lefty`, `rightx`, `righty`, `lefttrigger` or `righttrigger`.
- `action:` drives the action itself, whatever it is bound to: `action: boost` taps a button, and `state: down` holds it until `state: up`. The `action:` expectation checks `held`, `pressed`, `released`, `x` and `y`.
- An `expect:` step takes one expectation.

**At 1.2.0 a held action carries over** into the next scenario of the same `onegame test` run. The scaffold’s `steer-ship` ends with `move` and `boost` still held, so a scenario after it starts boosting. That is why `three-ways` releases both first and lets go of everything at the end.

## Look at them live

The `actions` tool lists every context and action with its state and bindings. Given a `name`, it drives that action as the harness does. With the MCP client from [Drive a running game over MCP](https://recursiveai.org/onegame/cookbook/live-control-mcp/), with the 200-character cut taken out:

```text
step {"frames": 1} -> {"frame":1,"tick":1,"hash":"a7f6119f33887e04"}
actions {} -> {"contexts":[{"name":"play","active":true,"screens":[]}],"actions":[{"name":"move","kind":"vector","context":"play","active":true,"held":false,"pressed":false,"released":false,"x":0,"y":0,"default":true,"bindings":["quad(key:A,key:D,key:W,key:S)","quad(key:ArrowLeft,key:ArrowRight,key:ArrowUp,key:ArrowDown)","stick:left"]},{"name":"boost","kind":"button","context":"play","active":true,"held":false,"pressed":false,"released":false,"x":0,"y":0,"default":true,"bindings":["key:Space","pad:South"]}]}
actions {"name": "boost", "state": "down"} -> {"contexts":[{"name":"play","active":true,"screens":[]}],"actions":[{"name":"move","kind":"vector","context":"play","active":true,"held":false,"pressed":false,"released":false,"x":0,"y":0,"default":true,"bindings":["quad(key:A,key:D,key:W,key:S)","quad(key:ArrowLeft,key:ArrowRight,key:ArrowUp,key:ArrowDown)","stick:left"]},{"name":"boost","kind":"button","context":"play","active":true,"held":false,"pressed":false,"released":false,"x":0,"y":0,"default":true,"bindings":["key:Space","pad:South"]}]}
step {"frames": 1} -> {"frame":2,"tick":2,"hash":"b7cdad13ee4ffa71"}
state_query {"entity": "Ship"} -> {"id":1,"type":"Ship","X":0,"Y":0,"Dx":0,"Dy":0,"Boost":true}
quit {} -> {"frame":2,"tick":2,"hash":"b7cdad13ee4ffa71"}
```

A change shows after the next step: the second `actions` call still reports `boost` up, and one frame later the ship is boosting. Query after a step, too. Before the first frame every action reads `active: false`. `input_pad` holds pad buttons and sticks the same way.

## Saved bindings

A bindings file lists only the actions that differ from their defaults, one line each, in the spelling the `actions` tool shows. `ActionMap.DefaultPath("pilot")` is `~/.local/share/pilot/bindings.txt` on Linux:

```text
onegame-bindings 1
boost = key:B | pad:South
move = quad(key:J,key:L,key:I,key:K) | stick:left
jump = key:Space
```

With that file in place, the game reports the line it skipped on stderr, and `actions` shows the player’s bindings:

```text
bindings: line 4: unknown action 'jump'.
```

```text
step {"frames": 1} -> {"frame":1,"tick":1,"hash":"a7f6119f33887e04"}
actions {} -> {"contexts":[{"name":"play","active":true,"screens":[]}],"actions":[{"name":"move","kind":"vector","context":"play","active":true,"held":false,"pressed":false,"released":false,"x":0,"y":0,"default":false,"bindings":["quad(key:J,key:L,key:I,key:K)","stick:left"]},{"name":"boost","kind":"button","context":"play","active":true,"held":false,"pressed":false,"released":false,"x":0,"y":0,"default":false,"bindings":["key:B","pad:South"]}]}
quit {} -> {"frame":1,"tick":1,"hash":"a7f6119f33887e04"}
```

The test run reads the same file, because `DefineActions` runs there too. D no longer moves the ship, so the key part of `three-ways` fails:

```text
PASS ship-looks-right (389 ms, tick 40, hash a5a533b83e128e35)
PASS steer-ship (4 ms, tick 22, hash 19dbaa512f757607)
FAIL three-ways (3 ms, tick 12, hash b04b131b59df8baa)
  step 6 (expect, line 10): expectation_failed: Ship.X is 0, expected 15
  expect state (line 11): Ship.X is 0, expected 15
    expected: 15
    actual:   0
2 passed, 1 failed
```

`action:` and `pad:` steps don’t depend on key bindings, so prefer them in tests. Test key bindings on a machine without a saved file.

- `actions.Save(path)` writes the file through a temporary file. `ToText()` and `LoadText(text)` do the same with a string. A bad line is skipped and listed in `Problems`.
- `actions.SetBinding`, `ResetBindings` and `BeginRebind` (which waits for the next key, button or stick push) change bindings in a running game. `ActionUi.BindingList(game)` builds a list of the actions with their bindings and Rebind and Reset buttons.
- These weren’t run for this page: Steam Input feeding the same actions (`SteamActionSource`), touch regions and the rebinding screen.

Every call is in the engine’s [input notes](https://github.com/recursive-ai-oss/onegame/blob/v1.2.0/docs/interfaces/input.md).

See both in a complete game: [C# game development without an editor](https://recursiveai.org/onegame/articles/csharp-game-development-without-editor/).
