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.
4.4 KiB
Audio workflows
Loading
let pcm = makepad_game_audio::decode(&bytes)?; // WAV or Ogg Vorbis -> Pcm
let id: SampleId = bank.add(pcm);
SampleBank owns loaded samples; SampleId is the handle you keep. Load at startup or during level load — decoding during play is a stutter source, and Vorbis decode is the most expensive audio operation in the stack.
AudioError distinguishes decode failures. Handle it: a missing or corrupt sound should log and continue, never panic mid-frame.
Playback
let handle: VoiceHandle = mixer.play(VoiceSpec {
sample: id,
gain,
pitch,
priority: Priority::/* .. */,
// placement for positional voices
});
Keep VoiceHandles for anything you must stop or modulate later — looping engine notes, ambience beds. Fire-and-forget for one-shots.
Event-driven emission
for impact in dynamics.take_mover_impacts() {
let pair = MaterialPair::new(impact.a_material, impact.b_material);
director.emit(SoundEvent {
category: Category::Impact,
placement: Placement::At(impact.pos),
// gain shaped by ImpactCurve from impact energy
});
}
take_mover_impacts() is the single source of truth for collisions. Re-detecting collisions in gameplay code to trigger audio is how you get double sounds that drift out of sync with the visual effect.
Material table
Material, MaterialPair, ImpactCurve.
Map every pair a scene can produce; an unmapped pair either falls back to a generic thud (annotated-sounding) or is silent (feels broken). Enumerate the materials in the scene and fill the matrix — it is small, and it is the difference between a world that sounds physical and one that sounds labelled.
ImpactCurve maps impact energy to gain and brightness: a light tap and a heavy crash should not be the same sample at the same level. Energy comes from the impact, not from a fixed constant per event type.
Variation
let pitch = 1.0 + rng.range(-0.08, 0.08);
let gain = base * (1.0 + rng.range(-0.10, 0.10));
let sample = variants[rng.next_index(variants.len())];
LocalRng, seeded — never thread RNG, or networked clients hear different things and any replay diverges.
For sample choice, shuffle a bag rather than picking uniformly at random: pure random repeats the same variant back-to-back often enough to be audible, which defeats the purpose.
Voice budget
| Category | Suggested cap |
|---|---|
| Player feedback | always plays, never evicted |
| UI | 4 |
| Nearby impacts | 8 |
| Footsteps | 6, coalesced |
| Ambience | 2–4 loops |
| Music | 1 |
Enforce with Priority. Cull by distance before mixing. Coalesce identical events within a frame into one louder voice. Debounce repeats of the same sample within ~30 ms — overlapping identical copies phase-cancel into a metallic artefact that sounds like a bug, because it is one.
3D
let listener = Listener { pos: camera.pos, forward: camera.forward, up: camera.up };
On the camera, not the player. Attenuate roughly inverse-square with a near clamp, cull beyond a max range, pan by the listener's right vector. Keep UI and player-feedback sounds non-positional so they never get quiet or pan away at the moment they matter.
Ambience
One or two long loops, quiet, continuous, crossfaded on area change. Layer sparse one-shots (a bird, a distant creak) at randomised intervals — this is what stops a loop being recognisable as a loop, and it is much cheaper than a longer bed.
Music and ducking
Music sits under everything. Duck it (and ambience) by a few dB with a fast attack and slow release when player feedback fires; this is what keeps important sounds audible without raising their level to the point of harshness.
The Kenney jingle pack covers win/lose/level-complete stings. Short stings beat looping tracks for most game-jam-scope games — cheaper, and they never outstay their welcome.
Testing
cargo test -p makepad-game-audio
Cx-free, so it all tests headlessly. Assert: decode round-trips (rate, channels, length); the mixer honours its voice cap under a flood of events; priority evicts the lowest-priority voice, never the player feedback; every MaterialPair a scene can produce resolves; no emission path grows a Vec without bound.
Then listen for several minutes of real play. Numbers cannot tell you that a footstep is repeating audibly, that the mix collapses in a busy wave, or that a critical event has no sound at all.