Bevy: project structure and ECS patterns¶
Written 2026-10-27 from the project's own docs.
How Fboard is laid out on Bevy 0.19, and the patterns it uses. Code names below are Fboard's own.
Keep the rules out of the engine¶
Fboard is a Rust workspace of four crates. Only one of them uses the full engine:
| Crate | Depends on | Role |
|---|---|---|
fboard_sim |
bevy_math, serde, ron |
Physics, tricks, combos, runs, goals, level and item data, network message types |
fboard_music |
serde, serde_json, ron, hound |
Adaptive music, with no engine and no audio device |
fboard_game |
bevy 0.19, kira 0.12, the two crates above |
Everything you see and hear |
fboard_tas |
fboard_sim, fboard_music, bevy_math |
Headless tool-assisted run search |
bevy_math is published as its own crate, so the simulation gets Bevy's vector and math types (with serialization turned on) without pulling in rendering, windowing or the ECS. The payoff is that the rules are plain Rust: the TAS tool runs them headless with no window, a future server can run them without a GPU, and tests don't need an app.
The project's working rule: gameplay rules belong in the simulation crate (no Bevy), and presentation belongs in the client.
The app¶
The client's main does four things:
- Loads settings and the active profile before Bevy starts.
- Adds
DefaultPlugins, configuring the window (title, resolution, vsync) and disabling Bevy's ownAudioPlugin, because sound runs on Kira instead (see Audio with Kira). - Inserts resources: settings, game content, the profile store, the clear colour, ambient light, and
Time::<Fixed>::from_hz(60)so the fixed-timestep schedule runs at the simulation's tick rate. - Initializes an
AppStatewith two states,MenuandInGame, and adds one plugin per module.
Each module (settings, input, audio, rider, gameplay, HUD, menu, camera, dev tools, playback, capture) exposes a plugin that registers its own systems and resources. main itself stays under 90 lines.
States and scoped entities¶
OnEnter(AppState::InGame)starts a session: it spawns the level, the rider and the HUD.- Every entity spawned for a state gets a
DespawnOnExit(AppState::InGame)component (orMenu), so leaving the state cleans it up without a hand-written teardown system. - Systems are gated with run conditions, for example running only
in_state(AppState::InGame)and only while aSessionresource exists.
Fixed timestep and interpolation¶
The simulation is stepped in Bevy's FixedUpdate schedule at 60 Hz, gated so it pauses while a menu is open. Rendering runs at whatever the frame rate is. To avoid stutter between the two:
- The session keeps the previous tick's state as well as the current one.
- Each frame, the rider's pose stores both states plus a blend factor, taken from
Time<Fixed>::overstep_fraction(), which says how far the clock has moved into the next fixed tick. - The rider and the chase camera are drawn at the blended position.
The same "two states and a blend" shape is planned for networked riders: remote players would be fed from server snapshots and drawn about 100 ms behind, using the interpolation that already draws the local rider.
Events become messages¶
Simulation and run events (a pop, a landing, a goal) are drained once per frame in an Update system and turned into sound requests, HUD toasts and profile progress. These are sent as Bevy messages: types that derive Message, written with MessageWriter and read with MessageReader. (In Bevy 0.19 these replace what older Bevy versions and tutorials call events; see Lessons and gotchas.) The audio and capture systems both read the same PlaySfx messages, so a video capture hears exactly what the game played.
Other patterns¶
- Nothing assumes one rider. Rider visuals are built per entity from a loadout and a pose, so remote riders can reuse the same bundle.
- Immediate-style menus. Every menu screen is a list of rows rebuilt from state each frame and re-rendered only when it changes.
- React to changed resources. Settings are applied by a system with
run_if(resource_changed::<Settings>), so changing a setting anywhere takes effect without explicit plumbing. - What you see is what you collide with. Level meshes are generated from the same data the simulation collides against, and a test checks they agree.
- Placeholder art, real pipeline later. The hand and board are procedural primitives. Bevy loads glTF models directly, so the project plans to replace them part by part, with the animation already driving named joints.
- Faster dev builds. The workspace uses Bevy's recommended dev profile:
opt-level = 1for the project's own code andopt-level = 3for all dependencies, so the game's code compiles quickly while the engine runs fast.