# 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.