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

Input: keyboard, mouse, gamepads

Input is read from ctx in any system. Every kind works the same way: _down while held, _pressed on the frame it went down, _released on the frame it came up. A tap shorter than a frame still shows up as pressed and released.

Keyboard

#![allow(unused)]
fn main() {
use ponko::*;
fn update(ctx: &mut Ctx, pos: &mut Vec2) {
if ctx.key_down(Key::Right) { pos.x += 200.0 * ctx.dt(); }
if ctx.key_pressed(Key::Space) { println!("jump"); }
}
}

Keys are named by their position on a US keyboard: Key::W is the key above S on every layout, so WASD controls stay in place for AZERTY and Dvorak players too. In C++: ctx.key_down(CRABBY_KEY_RIGHT).

Mouse

  • ctx.mouse_pos(): in window pixels; ctx.mouse_world(): through the camera, in world units.
  • ctx.mouse_down(MouseButton::Left) (and _pressed, _released).
  • ctx.mouse_delta() since the last frame, and ctx.mouse_wheel().
  • ctx.mouse_over_ui(): true over a UI panel or the inspector, so a click on a button doesn’t also act in the world.

Mouse look

For a first-person or orbit camera, lock the cursor and read raw motion:

  • ctx.set_cursor_locked(true) hides the cursor and holds it in the window (where the platform can’t hold it in place, it’s kept inside the window instead); false lets it go. It takes effect after the frame and comes back when the window regains focus.
  • ctx.mouse_motion(): how far the mouse moved this frame in raw device units (+y is down), even while the cursor is locked and can’t move. mouse_delta() stops at the window’s edge; this doesn’t.

C++: ctx.set_cursor_locked(true), ctx.mouse_motion(). See 3D physics for a walker that uses both.

Gamepads

Xbox, PlayStation, Switch and most other controllers work, plugged in or over Bluetooth, and can come and go while the game runs. ctx.gamepads() lists the connected ones. Each keeps its slot while connected (pad.slot(): 0 is player one), and a pad plugged in later takes the lowest free slot.

#![allow(unused)]
fn main() {
fn move_players(ctx: &mut Ctx) {
    let dt = ctx.dt();
    // Read the pads first: the query below borrows the world.
    let mut sticks = [Vec2::ZERO; MAX_GAMEPADS];
    let mut spins = [false; MAX_GAMEPADS];
    let mut grow = [0.0; MAX_GAMEPADS];
    for &pad in ctx.gamepads() {
        sticks[pad.slot()] = ctx.gamepad_stick(pad, GamepadStick::Left);
        spins[pad.slot()] = ctx.gamepad_pressed(pad, GamepadButton::South);
        grow[pad.slot()] = ctx.gamepad_axis(pad, GamepadAxis::RightTrigger);
    }
    for (t, player) in ctx.world.query::<(&mut Transform2D, &mut Player)>().iter() {
        let Some(slot) = player.slot else { continue };
        t.position += sticks[slot] * SPEED * dt;
        if spins[slot] {
            player.spin = std::f32::consts::TAU;
        }
        t.scale = Vec2::splat(1.0 + 2.0 * grow[slot]);
    }
}
}
struct Player { std::uint32_t slot; float spin; };   // one per connected pad

static void move_players(crabby::ctx& ctx) {
    const float dt = ctx.dt();
    // Read the pads first: the loop below borrows the world.
    crabby::vec2 sticks[CRABBY_MAX_GAMEPADS] = {};
    bool spins[CRABBY_MAX_GAMEPADS] = {};
    float grow[CRABBY_MAX_GAMEPADS] = {};
    for (crabby::gamepad pad : ctx.gamepads()) {
        sticks[pad.slot] = ctx.gamepad_stick(pad, crabby::stick::left);
        spins[pad.slot] = ctx.gamepad_pressed(pad, CRABBY_GAMEPAD_SOUTH);
        grow[pad.slot] = ctx.gamepad_axis(pad, CRABBY_GAMEPAD_RIGHT_TRIGGER_AXIS);
    }
    ctx.world().each([&](crabby::transform2d& t, Player& player) {
        t.position += sticks[player.slot] * 400 * dt;
        if (spins[player.slot]) player.spin = 6.2831853f;
        t.scale = crabby::vec2{1, 1} * (1 + 2 * grow[player.slot]);
    });
}

Buttons are named by position, so a game’s controls mean the same on every brand:

GamepadButtonXboxPlayStationNintendo
SouthACrossB
EastBCircleA
NorthYTriangleX
WestXSquareY
LeftBumper / RightBumperLB / RBL1 / R1L / R
LeftTrigger / RightTriggerLT / RTL2 / R2ZL / ZR
Select / StartView / MenuShare / OptionsMinus / Plus
LeftStick / RightStickstick clicksL3 / R3stick clicks
DPadUp, DPadDown, DPadLeft, DPadRightd-padd-padd-pad

Sticks and triggers are axes:

  • ctx.gamepad_stick(pad, GamepadStick::Left) is a Vec2 from -1 to 1, y down like screen coordinates, so adding it to a position moves that way. Small wobbles around the center read as zero.
  • ctx.gamepad_axis(pad, GamepadAxis::RightTrigger) goes from 0 (released) to 1 (fully pulled): use it for throttles and charge-ups. The triggers also count as buttons once pulled past halfway.

examples/gamepad.rs gives every connected pad its own crab: cargo run --example gamepad.

Gamepads need the gamepad cargo feature, on by default. Without it, or in headless runs, ctx.gamepads() is empty. On Linux, building it needs the udev headers (sudo apt-get install libudev-dev).