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.
140 lines
7.8 KiB
Markdown
140 lines
7.8 KiB
Markdown
# Makepad game architecture
|
||
|
||
The crate map an agent needs before touching anything. Paths are relative to repo root.
|
||
|
||
## Crate map
|
||
|
||
| Crate | Path | Owns | Needs a `Cx`? |
|
||
| --- | --- | --- | --- |
|
||
| `makepad-game-sim` | `libs/sim` | `GameWorld`, `Entity`, movers, rigid dynamics, terrain, water, voxels, nav, particles state, HUD state | no |
|
||
| `makepad-game-math` | `libs/sim/math` | deterministic transcendentals — the reason two devices generate byte-identical meshes | no |
|
||
| `makepad-game-render` | `libs/game/render` | forward renderer, shaders, shadows, skinning, models, particles, fireworks, sun, stage/quality | **yes** |
|
||
| `makepad-game-gen` | `libs/game/gen` | procedural mesh/terrain/texture/tree/L-system/interior/level generation, seeded RNG | no |
|
||
| `makepad-game-blocks` | `libs/game/blocks` | `Car`, `Character`, `Plane`, `Npc`, `Brain`, `RaceKit`, controllers | no |
|
||
| `makepad-game-audio` | `libs/game/audio` | WAV/Vorbis decode, `SampleBank`, `Mixer`, `AudioDirector`, material impacts | no |
|
||
| `makepad-game-script` | `libs/game/script` | the `game.*` verb table, `ScriptHost`, sandbox/trust, compose, interact | partly |
|
||
| `makepad-game-net` | `libs/game/net` | host-authoritative LAN transport, lobby auth, protocol | no |
|
||
| `makepad-game-session` | `libs/game/session` | `HostSession` / `ClientSession`, replication, per-player input | no |
|
||
| `makepad-game-coedit` | `libs/game/coedit` | host-serialized collaborative edits, leases, diff3 merge | no |
|
||
| `makepad-game-pkg` | `libs/game/pkg` | package manifest, library, registry, sha256 verification | no |
|
||
| `makepad-game-assets` | `libs/game/assets` | the searchable CC0 index, aliases, casts, packs, palettes | no |
|
||
| `makepad-arcade` | `apps/arcade` | the host app: viewport widget, AI chat, pairing, settings, XR input | yes |
|
||
|
||
**The Cx column is the single most useful fact in this table.** Everything without a `Cx` is unit-testable headlessly, which is why sim/gen/audio/net logic must never be written inside a widget. If you find yourself needing a `Cx` to test a rule, the rule is in the wrong crate.
|
||
|
||
## Dependency direction
|
||
|
||
```
|
||
math ──► sim ──► blocks ──► script ──► arcade
|
||
│ │ │ ▲ ▲
|
||
├──► gen ┘ │ │ │
|
||
├──► render ────────┘ │ │
|
||
├──► audio ────────────┘ │
|
||
└──► net ──► session ───────────┘
|
||
coedit, pkg, assets ┘
|
||
```
|
||
|
||
Never introduce an edge back up this graph. `sim` must not depend on `render`; if a rule needs a mesh, it needs *bounds*, and bounds come from `render::model_bounds` passed in by the caller.
|
||
|
||
## Route A — script (`game.*` verbs)
|
||
|
||
The game is a script the `ScriptHost` evaluates. Roughly 180 verbs, dispatched through:
|
||
|
||
```rust
|
||
pub type VerbFn = fn(&mut ScriptVm, &Ctx, ScriptObject) -> ScriptValue;
|
||
pub static VERBS: &[(&str, VerbFn)] = &[ /* ... */ ];
|
||
```
|
||
|
||
Verb families (from `libs/game/script/src/dispatch.rs`):
|
||
|
||
- **World**: `box`, `block`, `mover`, `spawn`, `terrain`, `sky`, `gravity`, `remove`
|
||
- **Transform / physics**: `pos`, `vel`, `set_pos`, `teleport`, `set_vel`, `push`, `walk`, `jump`, `on_floor`, `face`, `yaw`, `ground_y`, `ground_normal`
|
||
- **Query**: `tag`, `find`, `distance`
|
||
- **Look**: `set_color`, `glow`, `scale`, `camera`, `sun`
|
||
- **HUD**: `text`, `bar`, `crosshair`, `label`, `label_text`
|
||
- **Events / time**: `on_tick`, `on_touch`, `on_join`, `on_leave`, `after`, `every`, `cancel`, `time`
|
||
- **Random**: `rand`, `rand_range` (seeded — never `std` rand)
|
||
- **Audio**: `sfx`, `sfx_at`, `beep`, `jingle`
|
||
- **FX**: `particles`, `burst`, `particles_stop`
|
||
- **Blocks**: `car`, `character`, `player_character`, `plane`, `drive`, `autodrive`, `speed`, `interactable`, `interact_prompt`
|
||
- **AI**: `wander`, `chase`, `patrol`, `caught`
|
||
- **Race**: `spawnpoint`, `checkpoint`, `place`, `race`, `standings`, `lap`, `rank`, `finished`
|
||
- **Score / players**: `score`, `score_of`, `players`, `player_name`
|
||
|
||
Get the authoritative list at any time — do not trust this summary if it disagrees:
|
||
|
||
```rust
|
||
println!("{}", makepad_game_script::api_text());
|
||
```
|
||
|
||
Two rules that cost real debugging time:
|
||
|
||
1. **Options keys are typo-guarded.** Every options-taking verb validates its keys, because a silently ignored misspelled option used to burn whole test cycles. If a verb rejects a key, the key is wrong — do not work around it.
|
||
2. **The script budget is one cumulative 500k-instruction pool per tick**, shared by `on_tick` + timers + touch events, target ≤ 2 ms. A per-entity `on_tick` loop over thousands of entities will exhaust it; batch instead.
|
||
|
||
Sandboxing: `Trust` and `strip_capabilities` decide what a script may touch. Untrusted (AI- or peer-authored) scripts must be stripped before evaluation. `EvalReport` carries the result.
|
||
|
||
## Route B — Rust app crate
|
||
|
||
Model it on `apps/arcade/src/arcade_view.rs`. The shape of a frame:
|
||
|
||
```rust
|
||
// 1. sim, fixed step
|
||
world.step(TICK_DT); // makepad-game-sim
|
||
|
||
// 2. camera + scene state
|
||
let scene = render_scene_state(&world, &camera_rig, /* ... */);
|
||
set_pass_camera(cx, &self.pass, &scene);
|
||
|
||
// 3. feed the renderer
|
||
renderer.set_models(model_instances);
|
||
renderer.set_particles(particle_instances);
|
||
renderer.set_fireworks(firework_instances);
|
||
|
||
// 4. draw
|
||
renderer.draw_scene_full(cx, &mut self.draws, &world, &scene /* ... */);
|
||
draw_hud_overlay(cx, /* ... */);
|
||
```
|
||
|
||
`GameRenderer` also owns adaptive quality: `report_frame_ms()` returns whether quality changed, and `quality()`, `quality_level()`, `quality_reason()`, `frame_p90_ms()`, `set_shadow_budget()`, `set_refresh_hz()` expose the controls. Do not build a second frame-pacing system beside it.
|
||
|
||
Register the crate in the root `Cargo.toml` `workspace.members` list, in the arcade block.
|
||
|
||
## Styling: `script_mod!`, not `live_design!`
|
||
|
||
This fork's widgets use the splash DSL through `script_mod!`. Shader and style overrides are inheritance-based:
|
||
|
||
```rust
|
||
script_mod! {
|
||
use mod.prelude.widgets_internal.*
|
||
use mod.widgets.*
|
||
|
||
mod.widgets.MyGameViewBase = #(MyGameView::register_widget(vm))
|
||
mod.widgets.MyGameView = set_type_default() do mod.widgets.MyGameViewBase {
|
||
width: Fill
|
||
height: Fill
|
||
draw_cube += { light_dir: vec3(0.35, 0.8, 0.45) }
|
||
draw_firework += { /* fn override for the look only */ }
|
||
}
|
||
}
|
||
```
|
||
|
||
This seam is deliberate: **the engine shader owns the structure, the DSL owns the look.** A generated game can restyle the sky without being able to break the simulation. Keep it that way — do not move gameplay-relevant constants into the DSL, and do not hardcode look constants in Rust.
|
||
|
||
## Determinism
|
||
|
||
Networked and replayable play both depend on it:
|
||
|
||
- Use `makepad-game-gen`'s seeded `rng`, and `makepad-game-math` transcendentals, not `std` float math, anywhere a result crosses the wire or is regenerated on another device.
|
||
- Content replicates as `(preset, seed, position)`, not vertex data. A forest is a seed.
|
||
- Fixed `TICK_DT` step; never advance the sim by a variable frame delta.
|
||
|
||
## Budgets
|
||
|
||
`apps/arcade/BUDGETS.md` is the authority. The headline numbers:
|
||
|
||
- 100 movers + 50 rigid bodies + 65×65 terrain = **0.038 ms/tick**; the 60 Hz budget is 16.6 ms. Simulation is never your problem.
|
||
- **Draw calls and CPU skinning are.** Skinned vertices are re-uploaded every frame.
|
||
- Packed vertex layout `geom.GameMeshVertex` is 6 floats / 24 B (position exact, octahedral normal, f16 uv, unorm8 colour). Use it; do not invent a fatter vertex.
|
||
- Per-instance data that is constant across a batch belongs in a **uniform**, not the instance stream. Moving `sun_color`/`sun_sky`/`sun_ground`/`fog_color` off-instance cut the cube instance 27%.
|
||
- Quest is bandwidth-bound before it is ALU-bound. Optimise bytes-per-frame first, shader complexity second.
|