About bevy-ecs-systems
bevy-ecs-systems is a Bevy 0.19 skill covering ECS systems, parameters, sets, run conditions, and state schedules. It is for developers building Bevy applications, with focused guidance on ordering systems and handling runtime removal and ambiguity errors.
Key features
- 01Bundle state with SystemParam
- 02Group and order systems with SystemSet
- 03Gate systems with run conditions
- 04Hook systems to state transitions
- 05Remove systems from sets at runtime
- 06Diagnose schedule ambiguity errors
Use cases
- 01Organize related systems into sets
- 02Run systems only when conditions hold
- 03Handle application state transitions
- 04Remove debug systems at runtime
Install
Copy the command and run it in a terminal.
$
npx skills add https:/ / github.com/ chrisgliddon/ bevy-skills --skill bevy-ecs-systemsAfter installing, type/bevy-ecs-systemsin Claude Code to confirm it loaded.
Bundled files
References and scripts installed along with SKILL.md.
references/ordering.mdrun-conditions.mdruntime-removal.mdstate-schedules.mdsystem-params.mdsystem-sets.mdDetails
From the project READMEBevy 0.19 — ECS Systems (params, sets, run conditions)
When to use this skill
- Bundling related state into a single system parameter via
#[derive(SystemParam)]. - Grouping systems into a
SystemSetso callers can order against the whole group. - Gating systems with run conditions (
run_if,on_message,in_state,resource_exists,any_with_component). - Hooking into state transitions with
OnEnter(S)/OnExit(S)/OnTransition { from, to }. - Hot-removing systems at runtime (e.g. tearing down a debug overlay) with
remove_systems_in_set. - Hitting
ScheduleBuildErrorambiguity panics — the executor no longer guesses.
Canonical pattern
use bevy::ecs::system::SystemParam;
use bevy::prelude::*;
#[derive(SystemSet, Hash, PartialEq, Eq, Clone, Debug)]
enum GameLoop { Input, Simulate, Render }
#[derive(States, Default, Hash, PartialEq, Eq, Clone, Debug)]
enum AppState { #[default] Loading, Playing }
#[derive(Resource, Default)]
struct Score(u32);
#[derive(Message)]
struct GoalScored { team: u8 }
// Composite param: pass one argument, get four.
// 'w = world borrow; 's = system-local state borrow.
#[derive(SystemParam)]
struct GameCtx<'w, 's> {
time: Res<'w, Time>,
score: ResMut<'w, Score>,
goals: MessageReader<'w, 's, GoalScored>,
}
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.init_resource::<Score>()
.add_message::<GoalScored>()
.init_state::<AppState>()
.configure_sets(Update, (GameLoop::Input, GameLoop::Simulate, GameLoop::Render).chain())
// State schedule: fires once when entering Playing.
.add_systems(OnEnter(AppState::Playing), spawn_level)
// State schedule: fires once when leaving Playing.
.add_systems(OnExit(AppState::Playing), despawn_level)
.add_systems(Update, read_input.in_set(GameLoop::Input))
.add_systems(
Update,
tally_goals
.in_set(GameLoop::Simulate)
.run_if(on_message::<GoalScored>),
)
.add_systems(Update, draw_hud.in_set(GameLoop::Render))
.run();
}
#[derive(Component)]
struct LevelEntity;
fn spawn_level(mut commands: Commands) {
commands.spawn((LevelEntity, Name::new("level")));
}
fn despawn_level(mut commands: Commands, query: Query<Entity, With<LevelEntity>>) {
for e in &query { commands.entity(e).despawn(); }
}
fn read_input(mut writer: MessageWriter<GoalScored>) {
writer.write(GoalScored { team: 0 });
}
fn tally_goals(mut ctx: GameCtx) {
let _ = ctx.time.delta_secs();
for goal in ctx.goals.read() {
ctx.score.0 += 1;
info!("team {} scored, total {}", goal.team, ctx.score.0);
}
}
fn draw_hud(score: Res<Score>) { let _ = score.0; }
Run condition cheat sheet
| Built-in | Triggers when |
|---|---|
on_message::<M> | The Messages<M> buffer has unread items. |
resource_exists::<R> | Resource R is in the World. |
resource_changed::<R> | R was mutated this tick. |
in_state(MyState::X) | Current State<MyState> equals X. |
state_changed::<MyState> | The state changed this tick. |
any_with_component::<C> | At least one entity has C. |
Combine with .and() / .or(): run_if(in_state(GameState::Playing).and(resource_exists::<NetSession>())).
Topics
| Topic | Reference |
|---|---|
#[derive(SystemParam)], 'w/'s lifetimes, Local<T>, Commands deferred apply | references/system-params.md |
#[derive(SystemSet)] requirements, .in_set, .configure_sets, .chain() | references/system-sets.md |
Every built-in condition, .and()/.or()/not(), custom conditions, cost rule | references/run-conditions.md |
OnEnter(S) / OnExit(S) / OnTransition, NextState<S>, set_if_neq | references/state-schedules.md |
.before, .after, .chain(), .ambiguous_with, debugging ambiguity errors | references/ordering.md |
remove_systems_in_set, cleanup policies, cross-schedule runtime removal, side-effect limits | references/runtime-removal.md |
Bevy 0.19 gotchas
- Shared access does not define order. Conflicting mutable systems are serialized,
but their relative order is unspecified. Enable ambiguity diagnostics and add
.before,.after, or.chain()when behavior depends on order. See references/ordering.md. MessageReader/MessageWriter, notEventReader/EventWriter. Renamed in 0.17. Trait derive is#[derive(Message)]; registrar isapp.add_message::<M>().next_state.set(S)always firesOnExit/OnEnterin 0.18. Useset_if_neqfor the old behaviour. See references/state-schedules.md.- Do not remove the currently running schedule through
Schedules. It is temporarily absent from that resource. RemoveUpdatesystems from a later/different schedule such asLast. See references/runtime-removal.md. Resource: Componentin 0.19. Generic system parameters still useRes<T>/ResMut<T>, but aT: Resourcebound no longer proves thatTcannot appear in a component query.
See also
bevy-core-concepts— which schedule (Startup,Update,FixedUpdate) to add a system to.bevy-ecs-queries— query is one of manySystemParams;Changed<T>/Added<T>filters.bevy-ecs-components— defining theComponenttypes your systems act on.bevy-migration-0-17-to-0-18— full Message/Event rename,MaterialPluginchanges.bevy-migration-0-18-to-0-19— resource bounds and executor configuration.
Comments
Markdown supported