Adding a Conditional, Ordered System to a Custom Bevy Plugin
A practical guide to wrapping a Bevy system in a plugin, attaching a boolean run condition, and constraining execution order with system sets or explicit before/after pairs.
03 Jul 2025, 12:52 UTC

Desired Outcome
You want a Bevy system to run only when a simulation is active, and you want it to execute after a preparation phase without hard-coding update loops. The useful takeaway: wrap registration in a plugin, attach a boolean run condition with run_if, and constrain order with an explicit system set or .after/.before pair. This keeps scheduling concerns declarative and testable.
Prerequisites and Version Check
You need a Rust toolchain compatible with the Bevy version in your Cargo.toml. The bevy dependency must include the features your app uses, such as bevy_render and bevy_winit for a windowed build, or a headless plugin group for tests. Because Bevy's API surface has shifted across releases, confirm trait paths and feature names against your declared version before copying snippets.
Step 1: Define the Condition and Gated System
In Bevy's ECS, a system is a plain Rust function whose parameters are injected by the scheduler. A run condition is another system that returns a bool; it is evaluated before the target system. Define a resource to hold the pause state, a condition system that reads it, and the gated system that mutates entities.
use bevy::prelude::*;
#[derive(Resource, Default)]
struct SimulationClock {
paused: bool,
}
fn clock_is_running(clock: Res<SimulationClock>) -> bool {
!clock.paused
}
fn advance_simulation(mut query: Query<&mut Transform>) {
for mut transform in query.iter_mut() {
transform.translation.x += 1.0;
}
}
Step 2: Encapsulate Registration in a Plugin
A plugin is a struct that implements the Plugin trait. Its build method receives the App and registers systems. Use a system set to group preparation work, then attach the condition and ordering constraint to the gated system.
#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
struct PreparationSet;
fn prepare_entities(query: Query<Entity, Added<Transform>>) {
// setup work for newly added transforms
}
struct SimulationPlugin;
impl Plugin for SimulationPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<SimulationClock>();
app.add_systems(Update, prepare_entities.in_set(PreparationSet));
app.add_systems(
Update,
advance_simulation
.run_if(clock_is_running)
.after(PreparationSet),
);
}
}
The .after(PreparationSet) constraint tells the scheduler that advance_simulation must not run until prepare_entities has completed. The .run_if(clock_is_running) call gates execution on the boolean condition.
Step 3: Register the Plugin
Add the plugin to your App before calling run. If you use DefaultPlugins, register your plugin after them so your systems are added to the same schedules.
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugins(SimulationPlugin)
.run();
}
Expected Checks
Verify the plugin compiles and behaves as intended before relying on it.
- In your project root, run
cargo checkwith the same feature set your app uses. Resolve any missing-resource or invalid-query panics that appear at startup. - Run
cargo testwith a headless plugin group if you need automated confirmation that the condition system is reachable. - Add a temporary
info!orprintln!insideadvance_simulationand launch the app. Confirm the message appears only whenSimulationClock.pausedisfalse. - Use Bevy's schedule introspection or logging output to confirm
advance_simulationappears in theUpdateschedule afterPreparationSet. The exact tracing feature or log target varies by release, so check the version-specific documentation.
Recovery Options When Scheduling Assumptions Break
If Bevy reports ambiguous ordering or the execution order is not what you expected, adjust the constraints rather than adding branching logic inside system bodies.
- Ambiguity errors: Replace system sets with explicit
.before(other_system)and.after(other_system)pairs. Broad sets can create ambiguity; explicit pairs remove it. - Control flow sprawl: If many systems need to pause together, use Bevy's
Statesor a dedicated run condition instead of mutating shared resources inside each system. - Unexpected order: Inspect the schedule graph via Bevy's tracing or logging facilities. Overly broad ordering can serialize systems that would otherwise run in parallel, which hides data races rather than fixing them.
Limitations
Run conditions are evaluated on every schedule execution, so extremely fine-grained gating can add overhead. Bevy's default plugin composition and API names have changed across versions; treat the snippets above as a representative pattern and verify them in a sandboxed branch before applying them to a production project.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.