structure-bevy-app
Use when adding a feature to a Bevy game or untangling one — plugin-per-feature layout, app states with DespawnOnExit, system sets and ordering, messages vs observers, change detection, query conflict
Files2
View on GitHubSKILL.mdskills/game/structure-bevy-app/SKILL.md
GitHubStructure Bevy App
Bevy 0.20 APIs. Rationale and anti-examples: architecture-patterns.md. Versions: stack-versions.
1. Audit current state (change nothing)
cat .claude/stack-profile.md 2>/dev/null # rust lint/test layout
ls src/ src/*/ 2>/dev/null; wc -l src/main.rs src/lib.rs 2>/dev/null
grep -rnE "EventReader|EventWriter|add_event::|StateScoped|Trigger<|On<Add,|\.set_if_neq|send_event" src/ # pre-0.17/0.20 names
grep -rnE "add_systems\(" src/ | grep -v "impl Plugin" | head # systems registered outside a plugin
grep -rnE "derive\(.*Resource.*Component|derive\(.*Component.*Resource" src/ # both on one type: compile error since 0.19
grep -rn "States" src/ | head; grep -rnE "\.single\(\)\.unwrap|query\.single\(\)" src/
cargo clippy --all-targets -- -D warnings 2>&1 | tail -5
If .claude/stack-profile.md says tests.layout: colocated, put unit tests next to the plugin and keep integration tests in tests/ anyway (Cargo requires it).
2. Decide what to do
- New game, empty
src/→ createcore.rs(states, sets) + the first feature plugin. - Existing code → fix the highest-value finding only: logic in
main.rs, systems outside plugins, old API names, missing state scoping. One concern per change. - Plugins, sets, and state cleanup already in place → "already in place", stop.
3. Detect track
- 2D or 3D, UI-heavy or world-heavy: same structure; only the presentation plugin differs.
- Simulation that needs determinism (physics, lockstep netcode): put it in
FixedUpdate; see the rule in the patterns file. - Old Bevy names found: use the migration table in the patterns file; do not mix old and new in one change.
4. Install only what's missing
Nothing to install. Third-party crates only when the audit shows a need (physics, input mapping). Check each one's bevy requirement first (stack-versions).
5. Generate the seams
src/lib.rs GamePlugin: the only list of plugins
src/core.rs Screen state, GameSystems sets, shared configure_sets
src/<feature>.rs one Plugin: its components, messages, observers, systems, OnEnter/OnExit
src/<feature>/ only when a feature outgrows one file: mod.rs (the plugin) + components.rs + systems.rs
Canonical plugin:
pub struct PlayerPlugin;
impl Plugin for PlayerPlugin {
fn build(&self, app: &mut App) {
app.add_message::<Damage>()
.add_observer(on_died)
.add_systems(OnEnter(Screen::Playing), spawn_player)
.add_systems(
Update,
(
apply_velocity.in_set(GameSystems::Movement),
apply_damage.in_set(GameSystems::Combat),
),
);
}
}
Canonical state + sets (in core.rs):
#[derive(States, Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum Screen { #[default] Loading, Menu, Playing }
#[derive(SystemSet, Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum GameSystems { Input, Movement, Combat, Cleanup }
app.init_state::<Screen>().configure_sets(
Update,
(GameSystems::Input, GameSystems::Movement, GameSystems::Combat, GameSystems::Cleanup)
.chain()
.run_if(in_state(Screen::Playing)),
);
State-scoped spawn: commands.spawn((Player, DespawnOnExit(Screen::Playing))); — no manual cleanup system.
6. Wire it
- Add the plugin to
GamePlugininlib.rs; add.add_message::<M>()for each#[derive(Message)]type. - Spawn everything that belongs to one screen with
DespawnOnExit(<state>); useDespawnOnEnterto clear leftovers on the way in. - Order systems with the shared sets, never with ad hoc
.before()/.after()across plugins. - Fallible systems return
Resultand use?(query.single()?);Single<..>for "exactly one, else skip".
7. Verify
cargo clippy --all-targets -- -D warnings && cargo nextest run
grep -rnE "EventReader|EventWriter|StateScoped|On<Add," src/ ; echo "exit $?"
Expected: clippy and tests pass; the grep prints nothing and exit 1 (no old names). Every feature has a headless test (test-bevy-systems).
Next: manage-bevy-assets, optimize-bevy-game. Contracts: engineering principles.
Findability
| I need to find... | It lives in |
|---|---|
| The list of everything the game runs | src/lib.rs (GamePlugin) |
| Game states and their transitions | src/core.rs |
System ordering inside Update |
GameSystems in src/core.rs |
| A feature's components and systems | src/<feature>.rs (or src/<feature>/) |
| What happens when a screen starts or ends | OnEnter/OnExit calls in the owning plugin; cleanup is DespawnOnExit at the spawn site |
| Cross-feature communication | Message types in the plugin that produces them; observers for one-off reactions |
| Sprites, meshes, animation for a gameplay entity | the presentation plugin (src/view.rs or src/<feature>/view.rs), never the logic plugin |
| Asset handles | src/assets.rs (manage-bevy-assets) |
| Tests for a feature | tests/<feature>.rs |