ponko 0.0.1

A fast 2D game engine for simulating millions of things: ECS on all cores, hot reload, an in-game inspector, AI-agent control, and a C++ API.
Documentation
//! Gamepads: every connected controller gets a crab. Left stick moves it,
//! the bottom face button (A, Cross) spins it, the right trigger grows it.
//! Plug pads in and out while it runs. Without a pad, the arrow keys move
//! a crab of their own.
//!
//!   `cargo run --example gamepad`

use ponko::*;

/// The crab of the pad in `slot`, or of the keyboard (`slot: None`).
struct Player {
    slot: Option<usize>,
    spin: f32,
}

const SPEED: f32 = 400.0;

/// A tint per player, so pads can tell their crabs apart.
const PLAYER_COLORS: [Color; MAX_GAMEPADS] = [
    Color::rgb(1.0, 0.6, 0.6),
    Color::rgb(0.6, 0.8, 1.0),
    Color::rgb(0.6, 1.0, 0.6),
    Color::rgb(1.0, 1.0, 0.5),
    Color::rgb(1.0, 0.6, 1.0),
    Color::rgb(0.6, 1.0, 1.0),
    Color::rgb(1.0, 0.8, 0.5),
    Color::rgb(0.8, 0.8, 0.8),
];

// ANCHOR: gamepad
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]);
    }
}
// ANCHOR_END: gamepad

/// Arrow keys move the keyboard's crab.
fn move_keyboard_player(ctx: &mut Ctx) {
    let dt = ctx.dt();
    let mut dir = Vec2::ZERO;
    for (key, d) in [
        (Key::Left, -Vec2::X),
        (Key::Right, Vec2::X),
        (Key::Up, -Vec2::Y),
        (Key::Down, Vec2::Y),
    ] {
        if ctx.key_down(key) {
            dir += d;
        }
    }
    let spin = ctx.key_pressed(Key::Space);
    for (t, player) in ctx.world.query::<(&mut Transform2D, &mut Player)>().iter() {
        if player.slot.is_none() {
            t.position += dir.normalize_or_zero() * SPEED * dt;
            if spin {
                player.spin = std::f32::consts::TAU;
            }
        }
    }
}

/// One crab per connected pad: spawn on plug-in, despawn on unplug.
fn join_and_leave(ctx: &mut Ctx) {
    let mut connected = [false; MAX_GAMEPADS];
    for pad in ctx.gamepads() {
        connected[pad.slot()] = true;
    }
    let mut has_crab = [false; MAX_GAMEPADS];
    for (e, player) in ctx.world.query::<(Entity, &Player)>().iter() {
        if let Some(slot) = player.slot {
            if connected[slot] {
                has_crab[slot] = true;
            } else {
                ctx.commands.despawn(e);
            }
        }
    }
    for slot in (0..MAX_GAMEPADS).filter(|&s| connected[s] && !has_crab[s]) {
        let mut sprite = Sprite::new(ctx.load_texture("crab.png"));
        sprite.color = PLAYER_COLORS[slot];
        let at = vec2(200.0 + slot as f32 * 120.0, 360.0);
        ctx.world.spawn((
            Transform2D::at(at),
            sprite,
            Player {
                slot: Some(slot),
                spin: 0.0,
            },
        ));
    }
}

/// Spins play out over half a second.
fn spin(ctx: &mut Ctx) {
    let dt = ctx.dt();
    for (t, player) in ctx.world.query::<(&mut Transform2D, &mut Player)>().iter() {
        let step = player.spin.min(4.0 * std::f32::consts::TAU * dt);
        t.rotation += step;
        player.spin -= step;
    }
}

fn hud(ctx: &mut Ctx) {
    let pads = ctx.gamepads().len();
    let text = match pads {
        0 => "No gamepad: plug one in, or use the arrow keys and Space".to_string(),
        n => format!("{n} gamepad(s): left stick moves, A/Cross spins, right trigger grows"),
    };
    ctx.draw_text(&text, vec2(20.0, 20.0), 20.0, Color::WHITE);
}

fn main() {
    Game::new("Crabby Engine: gamepads", 1280, 720)
        .system(join_and_leave)
        .system(move_players)
        .system(move_keyboard_player)
        .system(spin)
        .system(hud)
        .run(|ctx| {
            let crab = ctx.load_texture("crab.png");
            ctx.world.spawn((
                Transform2D::at(vec2(640.0, 560.0)),
                Sprite::new(crab),
                Player { slot: None, spin: 0.0 },
            ));
        });
}