Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better documentation for explicit dependencies #1428

Merged
merged 7 commits into from
Feb 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 18 additions & 5 deletions crates/bevy_ecs/src/schedule/stage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,24 @@ pub trait Stage: Downcast + Send + Sync {

impl_downcast!(Stage);

/// When this resource is present in `Resources`, `SystemStage` will log a report containing
/// pairs of systems with ambiguous execution order - i.e., those systems might induce different
/// results depending on the order they're executed in, yet don't have an explicit execution order
/// constraint between them.
/// This is not necessarily a bad thing - you have to make that judgement yourself.
/// When this resource is present in the `AppBuilder`'s `Resources`,
/// each `SystemStage` will log a report containing
/// pairs of systems with ambiguous execution order.
///
/// Systems that access the same Component or Resource within the same stage
/// risk an ambiguous order that could result in logic bugs, unless they have an
/// explicit execution ordering constraint between them.
///
/// This occurs because, in the absence of explicit constraints, systems are executed in
/// an unstable, arbitrary order within each stage that may vary between runs and frames.
///
/// Some ambiguities reported by the ambiguity checker may be warranted (to allow two systems to run without blocking each other)
/// or spurious, as the exact combination of archetypes used may prevent them from ever conflicting during actual gameplay.
/// You can resolve the warnings produced by the ambiguity checker by adding `.before` or `.after` to one of the conflicting systems
/// referencing the other system to force a specific ordering.
alice-i-cecile marked this conversation as resolved.
Show resolved Hide resolved
///
/// The checker may report a system more times than the amount of constraints it would actually need to have
/// unambiguous order with regards to a group of already-constrained systems.
pub struct ReportExecutionOrderAmbiguities;

struct VirtualSystemSet {
Expand Down
2 changes: 1 addition & 1 deletion crates/bevy_ecs/src/schedule/system_descriptor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use std::borrow::Cow;
/// been applied.
///
/// All systems can have a label attached to them; other systems in the same group can then specify
/// that they have to run before or after the system with that label.
/// that they have to run before or after the system with that label using the `before` and `after` methods.
///
/// # Example
/// ```rust
Expand Down
49 changes: 35 additions & 14 deletions examples/ecs/ecs_guide.rs
Original file line number Diff line number Diff line change
Expand Up @@ -261,19 +261,29 @@ fn main() {
//
// SYSTEM EXECUTION ORDER
//
// By default, all systems run in parallel. This is efficient, but sometimes order matters.
// Each system belongs to a `Stage`, which controls the execution strategy and broad order of the systems within each tick.
// Startup stages (which startup systems are registered in) will always complete before ordinary stages begin,
// and every system in a stage must complete before the next stage advances.
// Once every stage has concluded, the main loop is complete and begins again.
//
// By default, all systems run in parallel, except when they require mutable access to a piece of data.
// This is efficient, but sometimes order matters.
// For example, we want our "game over" system to execute after all other systems to ensure we don't
// accidentally run the game for an extra round.
//
// First, if a system writes a component or resource (ComMut / ResMut), it will force a synchronization.
// Any systems that access the data type and were registered BEFORE the system will need to finish first.
// Any systems that were registered _after_ the system will need to wait for it to finish. This is a great
// default that makes everything "just work" as fast as possible without us needing to think about it ... provided
// we don't care about execution order. If we do care, one option would be to use the rules above to force a synchronization
// at the right time. But that is complicated and error prone!
// Rather than splitting each of your systems into separate stages, you should force an explicit ordering between them
// by giving the relevant systems a label with `.label`, then using the `.before` or `.after` methods.
// Systems will not be scheduled until all of the systems that they have an "ordering dependency" on have completed.
//
// Doing that will, in just about all cases, lead to better performance compared to
// splitting systems between stages, because it gives the scheduling algorithm more
// opportunities to run systems in parallel.
// Stages are still necessary, however: end of a stage is a hard sync point
// (meaning, no systems are running) where `Commands` issued by systems are processed.
// This is required because commands can perform operations that are incompatible with
// having systems in flight, such as spawning or deleting entities,
// adding or removing resources, etc.
//
// This is where "stages" come in. A "stage" is a group of systems that execute (in parallel). Stages are executed in order,
// and the next stage won't start until all systems in the current stage have finished.
// add_system(system) adds systems to the UPDATE stage by default
// However we can manually specify the stage if we want to. The following is equivalent to add_system(score_system)
.add_system_to_stage(stage::UPDATE, score_system.system())
Expand All @@ -285,11 +295,22 @@ fn main() {
.add_stage_after(stage::UPDATE, "after_round", SystemStage::parallel())
.add_system_to_stage("before_round", new_round_system.system())
.add_system_to_stage("before_round", new_player_system.system())
.add_system_to_stage("after_round", score_check_system.system())
.add_system_to_stage("after_round", game_over_system.system())
// score_check_system will run before game_over_system because score_check_system modifies GameState and game_over_system
// reads GameState. This works, but it's a bit confusing. In practice, it would be clearer to create a new stage that runs
// before "after_round"
// We can ensure that game_over system runs after score_check_system using explicit ordering constraints
// First, we label the system we want to refer to using `.label`
// Then, we use either `.before` or `.after` to describe the order we want the relationship
.add_system_to_stage(
"after_round",
score_check_system.system().label("score_check"),
)
.add_system_to_stage(
"after_round",
game_over_system.system().after("score_check"),
)
// We can check our systems for execution order ambiguities by examining the output produced in the console
// by adding the following Resource to our App :)
// Be aware that not everything reported by this checker is a potential problem, you'll have to make
// that judgement yourself.
.insert_resource(ReportExecutionOrderAmbiguities)
// This call to run() starts the app we just built!
.run();
}