makepad/skills/makepad-game-ui-designer/references/menus-and-flow.md
Arena Agent fe21c07d84 skills: add Makepad game skills pack ported from threejs-game-skills
Nine agent skills for building Makepad/Rust games, ported from
majidmanzarpour/threejs-game-skills. Same director-routed workflow and
premium bar; runtime rewritten for this fork's game crates.

- makepad-game-director        entrypoint, routing, continuity, asset probe
- makepad-gameplay-systems     loop, movers vs rigid bodies, input, camera, netplay
- makepad-aaa-graphics-builder lighting, shaders, budget, visual scorecard
- makepad-game-ui-designer     HUD, menus, touch and XR UI
- makepad-debug-profiler       defect bisection and profiling
- makepad-qa-release           verification ladder, evidence, packaging
- makepad-3d-generator         CC0 model search, casts, procedural geometry
- makepad-image-generator      texgen textures, palettes, sky, icons
- makepad-audio-generator      sample bank, mixer, material impacts, 3D audio

Written against the real APIs in libs/game/*, libs/sim and apps/arcade:
the game.* verb table, GameRenderer adaptive quality, the packed 6-float
GameMeshVertex layout, script_mod! splash styling, makepad-test driving,
and the BUDGETS.md numbers. No paid generation API is required - the CC0
library plus seeded makepad-game-gen replaces them.

Includes install.sh (Codex/Claude), validate-skills.sh and a repo-aware
probe_assets.sh; both scripts verified against this checkout.
2026-09-05 21:18:58 +00:00

3.7 KiB

Menus and flow

The state machine

Boot -> Menu -> Playing -> Paused -> Playing
                   |-> Win  -> Menu | Retry
                   |-> Lose -> Menu | Retry
         Menu -> Settings -> Menu
        Paused -> Settings -> Paused

Model it as one enum with one transition function. Scattering is_paused / is_over booleans across widgets produces states that are reachable but not designed — paused and won, for instance.

enum GameState { Boot, Menu, Playing, Paused, Win, Lose, Settings(Box<GameState>) }

Settings carries its return state so one screen serves both entry points.

Transition rules

  • Pause must stop the sim step. Not just hide the HUD. Also pause audio voices and particle emitters, or the player returns to a changed world.
  • Retry must fully reset. Score, timers, spawns, particle pools, audio voices, camera. One reset() that rebuilds world state. Test it by winning twice in a row and losing after a win — stale state is nearly always found this way, not by inspection.
  • Win and lose need a latch. An unlatched condition fires every tick, which stacks overlays and replays the jingle.
  • Never trap the player. Every screen has a way back, including with a controller.

Loading

Under ~1 s: nothing. Over: show progress. Asset loading here is load_model plus library indexing (~120 ms for 5000 entries), so the usual wait is model loading and terrain generation.

Log a ready line — log "game: ready" — so wait_for_log_contains can synchronise tests instead of sleeping.

Focus and controller navigation

If the game supports a gamepad, menus must be fully navigable with it. That means an explicit focus model: a default focused item on every screen, a defined neighbour in each direction, a visible focus indicator that survives against every background, and consistent confirm/back buttons. Mouse-only menus in a controller game are a completeness failure, and they are usually noticed by the user rather than the agent.

Settings

Minimum viable set: master / SFX / music volume, quality (the renderer already exposes quality_level, set_shadow_budget, set_refresh_hz — surface those rather than inventing parallel settings), invert-Y and sensitivity, reduce shake / reduce flashes, and a control reference showing the active device's glyphs.

Apply immediately, do not require confirmation, and persist. A quality slider that fights GameRenderer's adaptive controller will oscillate — set the ceiling, and let adaptive quality work below it.

Results screens

Show, in this order: outcome, the number that matters, how it compares (best, par, previous), and the next action already focused. Retry is the primary action after a loss; next-level or menu after a win.

Do not show a table of statistics nobody asked for. One headline number plus a comparison is what gets read.

Text

Write it as UI, not as prose. Retry, not Would you like to try again?. Second person, present tense, no exclamation marks. Say what a control does, not that it exists: Hold Shift to drift, not Shift: Drift.

Keep every string in one place so it can be checked, changed, and eventually localised without hunting through widget definitions.

Verification

#[makepad_test]
fn pause_stops_and_resumes(app: TestApp) {
    app.wait_for_log_contains("game: ready");
    app.press_key(KeyCode::Escape);
    app.wait_for_log_contains("state: paused");
    let shot = app.screenshot();          // inspect it
    app.press_key(KeyCode::Escape);
    app.wait_for_log_contains("state: playing");
}

Capture every state at the target aspect ratio. Emit a state log line on each transition — it is the cheapest thing that makes flow machine-testable, and it costs one line per transition.