makepad/skills/makepad-game-director/references/architecture.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

7.8 KiB
Raw Permalink Blame History

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:

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:

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:

// 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:

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.