Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Named input actions

Bind physical inputs to names once, then query those names in game systems. An action is a button, a scalar axis, or a vector axis. Several sources can control the same action; this makes keyboard and controller support share the same game logic. Configuration and rebinding may allocate. Event handling, frame resets and action queries allocate nothing.

fn main() {
    let pad = Gamepad::from_slot(0).unwrap();
    let mut bindings = ActionMap::default();
    bindings.set(
        "jump",
        Action::Button(vec![
            ButtonBinding::Key(Key::Space),
            ButtonBinding::Gamepad {
                pad,
                button: GamepadButton::South,
            },
        ]),
    );
    bindings.set(
        "move",
        Action::Axis2(vec![
            Axis2Binding::Keys {
                left: Key::A,
                right: Key::D,
                up: Key::W,
                down: Key::S,
            },
            Axis2Binding::Gamepad {
                pad,
                stick: GamepadStick::Left,
                deadzone: 0.2,
            },
        ]),
    );
    bindings.set(
        "throttle",
        Action::Axis(vec![AxisBinding::Gamepad {
            pad,
            axis: GamepadAxis::RightTrigger,
            deadzone: 0.1,
        }]),
    );
    let saved = bindings.to_json().unwrap();
    let restored = ActionMap::from_json(&saved).unwrap();
    Game::new("actions", 1, 1).run_headless(0, |ctx| {
        ctx.set_actions(restored);
        ctx.feed_input(InputEvent::Key(Key::Space, true));
        assert!(ctx.action_pressed("jump") && ctx.action_down("jump"));
        let movement = ctx.action_axis2("move");
        let throttle = ctx.action_axis("throttle");
        assert_eq!((movement, throttle), (Vec2::ZERO, 0.0));
        ctx.feed_input(InputEvent::Key(Key::Space, false));
        assert!(ctx.action_released("jump"));
        let mut changed = ctx.actions().clone();
        changed.set("jump", Action::Button(vec![ButtonBinding::Key(Key::Enter)]));
        ctx.set_actions(changed);
        assert!(!ctx.action_pressed("jump"));
    });
}
static void action_map_snippet() {
    crabby::action_map bindings;
    bindings.set("jump", crabby::action::button({
        crabby::action_binding::key(CRABBY_KEY_SPACE),
        crabby::action_binding::button({0}, CRABBY_GAMEPAD_SOUTH)}));
    bindings.set("move", crabby::action::axis2({
        crabby::action_binding::keys2(CRABBY_KEY_A, CRABBY_KEY_D, CRABBY_KEY_W, CRABBY_KEY_S),
        crabby::action_binding::stick({0}, crabby::stick::left, 0.2f)}));
    bindings.set("throttle", crabby::action::axis({
        crabby::action_binding::axis({0}, CRABBY_GAMEPAD_RIGHT_TRIGGER_AXIS, 0.1f)}));
    const std::string saved = bindings.to_json();
    auto restored = crabby::action_map::from_json(saved);
    crabby::game("actions", 1, 1).run_headless(0, [&](crabby::ctx& ctx) {
        ctx.set_actions(restored);
        ctx.feed_input(crabby::input_event::key(CRABBY_KEY_SPACE, true));
        if (!ctx.action_pressed("jump") || !ctx.action_down("jump")) throw std::runtime_error("missing action press");
        const crabby::vec2 move = ctx.action_axis2("move");
        const float throttle = ctx.action_axis("throttle");
        if (move.x != 0 || move.y != 0 || throttle != 0) throw std::runtime_error("unexpected axis input");
        ctx.feed_input(crabby::input_event::key(CRABBY_KEY_SPACE, false));
        if (!ctx.action_released("jump")) throw std::runtime_error("missing action release");
        auto changed = ctx.actions();
        changed.set("jump", crabby::action::button({crabby::action_binding::key(CRABBY_KEY_ENTER)}));
        ctx.set_actions(changed);
        if (ctx.action_pressed("jump")) throw std::runtime_error("rebinding generated a press");
    });
}

feed_input supplies events manually for headless tests and replay. Live keyboard, mouse and gamepad input updates the same action state automatically. Frame edges clear after each frame (after each tick in headless runs).

ActionRust sourcesC++ sources
Action::Button / action::buttonButtonBinding::Key, Mouse, Gamepadaction_binding::key, mouse, button
Action::Axis / action::axisAxisBinding::Keys, Gamepadaction_binding::keys, axis
Action::Axis2 / action::axis2Axis2Binding::Keys, Gamepadaction_binding::keys2, stick

For example, add ButtonBinding::Mouse(MouseButton::Left) or action_binding::mouse(CRABBY_MOUSE_LEFT) to a button action. A scalar key pair takes the negative key followed by the positive key. Four-key vectors take left, right, up, down, so WASD is A, D, W, S. Opposing keys cancel and keyboard diagonals are normalized. Vectors use screen coordinates: positive Y is down.

Buttons combine with OR. action_pressed fires when the first source becomes held; action_released fires when the last held source releases. Pressing a second source or releasing one of two held sources produces neither edge. A press and release within one frame preserves both edges.

Scalar and vector axes choose the source with the largest absolute value or vector length; the first binding wins ties. Sources do not add together. Sticks use a radial deadzone; scalar gamepad axes use a component deadzone. Inside it the value is zero, and outside it the remaining range is rescaled to reach one. A deadzone must be finite and in 0..1; invalid values disable that source. Non-finite analog input reads zero. Disconnection releases the pad’s buttons and centers its axes. An unconnected slot starts at zero.

Use action_down, action_pressed, action_released, action_axis and action_axis2 on Ctx / ctx. Missing names and queries of the wrong kind return false or zero. Names are case-sensitive. Queries borrow their names and allocate nothing.

To rebind, copy ctx.actions(), call set or remove, then install it with ctx.set_actions. Rust uses .clone() to copy the map; C++ returns an owned map. Installation resets action edges and uses currently held inputs as the baseline. Rebinding a held key therefore does not generate a new press.

ActionMap::to_json / action_map::to_json save bindings only, with the same serde JSON format in both languages. Write that string to your settings file using normal file I/O. Read it back with ActionMap::from_json / action_map::from_json, then install the result. Invalid JSON fails before changing active bindings. Rust returns Result; C++ throws the usual engine exception. Button state is never saved.

The C API uses an owned CrabbyActionMap: create/free, set/remove, load/save JSON, then crabby_ctx_set_actions. crabby_action_map_set takes a kind (0 button, 1 scalar, 2 vector) and an array of CrabbyActionBinding. CrabbyActionSource documents each source’s fields; unused fields are zero. Save follows the standard two-call buffer convention: query the length, allocate length + 1 bytes, then save again. Context queries are named crabby_ctx_action_down, pressed, released, axis and axis2.

This map has no chords, action contexts, smoothing, interactive binding UI, or device-specific profiles. Each gamepad binding names a stable controller slot, the same slot used by the raw gamepad API.