Skip to content

Commit a895256

Browse files
Better documentation for explicit dependencies (#1428)
* More in-depth ambiguity checker docs * Updated ecs_guide example with explicit dependencies
1 parent d021a3c commit a895256

File tree

3 files changed

+54
-20
lines changed

3 files changed

+54
-20
lines changed

crates/bevy_ecs/src/schedule/stage.rs

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,24 @@ pub trait Stage: Downcast + Send + Sync {
2121

2222
impl_downcast!(Stage);
2323

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

3144
struct VirtualSystemSet {

crates/bevy_ecs/src/schedule/system_descriptor.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use std::borrow::Cow;
1212
/// been applied.
1313
///
1414
/// All systems can have a label attached to them; other systems in the same group can then specify
15-
/// that they have to run before or after the system with that label.
15+
/// that they have to run before or after the system with that label using the `before` and `after` methods.
1616
///
1717
/// # Example
1818
/// ```rust

examples/ecs/ecs_guide.rs

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -261,19 +261,29 @@ fn main() {
261261
//
262262
// SYSTEM EXECUTION ORDER
263263
//
264-
// By default, all systems run in parallel. This is efficient, but sometimes order matters.
264+
// Each system belongs to a `Stage`, which controls the execution strategy and broad order of the systems within each tick.
265+
// Startup stages (which startup systems are registered in) will always complete before ordinary stages begin,
266+
// and every system in a stage must complete before the next stage advances.
267+
// Once every stage has concluded, the main loop is complete and begins again.
268+
//
269+
// By default, all systems run in parallel, except when they require mutable access to a piece of data.
270+
// This is efficient, but sometimes order matters.
265271
// For example, we want our "game over" system to execute after all other systems to ensure we don't
266272
// accidentally run the game for an extra round.
267273
//
268-
// First, if a system writes a component or resource (ComMut / ResMut), it will force a synchronization.
269-
// Any systems that access the data type and were registered BEFORE the system will need to finish first.
270-
// Any systems that were registered _after_ the system will need to wait for it to finish. This is a great
271-
// default that makes everything "just work" as fast as possible without us needing to think about it ... provided
272-
// we don't care about execution order. If we do care, one option would be to use the rules above to force a synchronization
273-
// at the right time. But that is complicated and error prone!
274+
// Rather than splitting each of your systems into separate stages, you should force an explicit ordering between them
275+
// by giving the relevant systems a label with `.label`, then using the `.before` or `.after` methods.
276+
// Systems will not be scheduled until all of the systems that they have an "ordering dependency" on have completed.
277+
//
278+
// Doing that will, in just about all cases, lead to better performance compared to
279+
// splitting systems between stages, because it gives the scheduling algorithm more
280+
// opportunities to run systems in parallel.
281+
// Stages are still necessary, however: end of a stage is a hard sync point
282+
// (meaning, no systems are running) where `Commands` issued by systems are processed.
283+
// This is required because commands can perform operations that are incompatible with
284+
// having systems in flight, such as spawning or deleting entities,
285+
// adding or removing resources, etc.
274286
//
275-
// This is where "stages" come in. A "stage" is a group of systems that execute (in parallel). Stages are executed in order,
276-
// and the next stage won't start until all systems in the current stage have finished.
277287
// add_system(system) adds systems to the UPDATE stage by default
278288
// However we can manually specify the stage if we want to. The following is equivalent to add_system(score_system)
279289
.add_system_to_stage(stage::UPDATE, score_system.system())
@@ -285,11 +295,22 @@ fn main() {
285295
.add_stage_after(stage::UPDATE, "after_round", SystemStage::parallel())
286296
.add_system_to_stage("before_round", new_round_system.system())
287297
.add_system_to_stage("before_round", new_player_system.system())
288-
.add_system_to_stage("after_round", score_check_system.system())
289-
.add_system_to_stage("after_round", game_over_system.system())
290-
// score_check_system will run before game_over_system because score_check_system modifies GameState and game_over_system
291-
// reads GameState. This works, but it's a bit confusing. In practice, it would be clearer to create a new stage that runs
292-
// before "after_round"
298+
// We can ensure that game_over system runs after score_check_system using explicit ordering constraints
299+
// First, we label the system we want to refer to using `.label`
300+
// Then, we use either `.before` or `.after` to describe the order we want the relationship
301+
.add_system_to_stage(
302+
"after_round",
303+
score_check_system.system().label("score_check"),
304+
)
305+
.add_system_to_stage(
306+
"after_round",
307+
game_over_system.system().after("score_check"),
308+
)
309+
// We can check our systems for execution order ambiguities by examining the output produced in the console
310+
// by adding the following Resource to our App :)
311+
// Be aware that not everything reported by this checker is a potential problem, you'll have to make
312+
// that judgement yourself.
313+
.insert_resource(ReportExecutionOrderAmbiguities)
293314
// This call to run() starts the app we just built!
294315
.run();
295316
}

0 commit comments

Comments
 (0)