Managing Execution Flow in Bevy with System Sets
Stop fighting one-frame lag in Bevy. Learn how to use System Sets to group logic, manage execution order, and apply global run conditions to your game systems.
12 Nov 2025, 00:49 UTC

The Scheduling Chaos Problem
In a growing Bevy project, you quickly encounter a synchronization problem: systems that depend on each other. For example, you cannot calculate a character's final position if the input handling system hasn't yet updated the velocity component. If you rely on the default parallel scheduler, these systems may run in any order, leading to "one-frame lag" where the game feels unresponsive or jittery.
The solution is System Sets. Rather than manually ordering every single function, System Sets allow you to group logic into logical buckets and define the relationship between those buckets. This shifts your focus from "System A must run before System B" to "The Input Phase must complete before the Physics Phase begins."
Grouping Logic with System Sets
A System Set is a label applied to one or more systems. In Bevy, these are typically defined as unique types or enums. Once a system is added to a set, any constraint applied to that set—such as a run condition or an ordering requirement—automatically applies to every system within it.
This approach eliminates boilerplate. Instead of calling .after(system_a) on ten different movement systems, you simply tell the entire MovementSet to run after the InputSet.
Practical Implementation: Input to Movement
To implement this, you define your sets and use the .configure_sets method during app initialization. The following example assumes Bevy 0.13+ syntax.
use bevy::prelude::*;
#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
enum GamePhase {
Input,
Movement,
}
fn handle_keyboard(mut query: Query<(&mut Velocity, &KeyboardInput)>) {
// Logic to update velocity based on keys
}
fn apply_velocity(mut query: Query<(&mut Transform, &Velocity)>) {
// Logic to move transform based on velocity
}
fn main() {
App::new()
.add_plugins(DefaultPlugins)
// 1. Define the order of the sets
.configure_sets(Update,
GamePhase::Movement.after(GamePhase::Input)
)
// 2. Assign systems to those sets
.add_systems(Update, (
handle_keyboard.in_set(GamePhase::Input),
apply_velocity.in_set(GamePhase::Movement),
))
.run();
}
Execution Details
- Where to run: These configurations occur in the
main()function during theAppbuild phase. - Permissions: Standard Rust project permissions; no special OS privileges required.
- Expected Result: The scheduler guarantees that
handle_keyboardcompletes across all entities beforeapply_velocitybegins. - Risk: If you accidentally create a circular dependency (e.g., Set A after Set B, and Set B after Set A), Bevy will panic at startup.
Conditional Execution for Entire Sets
One of the most powerful features of sets is the ability to apply a Run Condition to the entire group. A run condition is a system that returns a boolean; if it returns false, the scheduler skips every system in the associated set.
For instance, you can create a GameState::Playing condition. By applying this to your GamePhase::Movement set, you ensure that no physics or movement logic runs while the game is paused or in a main menu, without having to add if paused { return; } to every single function.
Trade-offs and Scheduling Bottlenecks
While sets simplify organization, they can introduce performance bottlenecks if overused. Bevy's strength is its ability to run systems in parallel across all available CPU cores. When you force a strict linear sequence (Set A → Set B → Set C), you limit the scheduler's ability to fill those cores.
To maintain performance, keep your sets broad. Avoid creating a unique set for every single system. Instead, group systems that truly share a data dependency and allow systems within the same set to run concurrently whenever possible.
Verification and Testing
To verify your scheduling is working as intended, you can use a simple print-statement check. Place a println!("Input Phase") in your input system and println!("Movement Phase") in your movement system. If the logs consistently show Input before Movement across multiple frames, your set configuration is correct.
If you need to remove a set constraint, simply remove the .after() or .before() call in configure_sets to return the systems to the default parallel execution mode.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.