Ordering of systems is hard: the problem and thoughts about the solution #10205
Replies: 2 comments 2 replies
-
I have encountered some of your pain points and learned to work around them. I will try to put my thoughts into points but it will be mess: Resource initialization - I usually don't create system which initializes a resource. I impl System dependencies - There are only few ways a systems depend on each other: Events: for events I create a generic Resources: I don't order systems against resources but against state - semantically resource existing at x time but not at x-y time means something "important" happened and it should be put into it's own state. If resource exists but it's fields are Entities/Components: because commands execution is postponed until States: if you systems do "contradictionary" things - load_chunks, unload_chunks - then you should use states and anotate systems with relevant state run conditions. I think this cover majority of use cases of ordering - could be wrong. I don't think Also Regarding your new primitive I am not trying to rebuke your or something, this is just mine raving on this matter. |
Beta Was this translation helpful? Give feedback.
-
For the record, I have implemented a PR for making dependencies between systems declarative: #10618. Can be used like this: fn first(_before: Before<MyBarrier>) {}
fn second(_after: After<MyBarrier>) {} or like this with fn first(writer: MyOrderedEventWriter<Foo>) { ... }
fn second(reader: MyOrderedEventReader<Foo>) { ... } |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Update
Barriers are implemented in #10618.
Original post
Hey,
I've been using bevy for a couple weeks, and here is my feedback about systems, which are either hard to use, are not explained well (which also means, hard to use). This is the part I struggled with the most.
It is entirely possible I don't understand ECS model well (which is new concept to me).
Also I didn't try to search very hard for similar discussions, so feel free to ignore this one.
So
Bevy is parallel. Which is great. But also non-deterministic, not great. For example:
If I understood the documentation correctly, there are several main instruments to control ordering of systems:
.after
which is good because it allows ordering explicitly.in_set
which allows ordering of group of systems relative to other group of systems.pipe
which is nice because it allows passing data between system. Some kind of function call with dependency injectionNow the problem
Consider this system evaluation graph.
There are two ways to describe scheduling for this graph, both don't work well.
Either explicitly
This is fragile and not scalable: you miss one edge, and then spend hours debugging why it doesn't work as expected on another machine with different number of CPUs where systems are executed in different order.
Another option is put (A, B, C) in one system set, and (D, E, F) in another system set
... and put one system set before another. This is not good, because it reduces parallelism: it adds dependency between A and F for example.
Additionally, this approach requires making everything public
For example, I have two modules (or two crates) defining some systems. I have to make all the functions and all the types public to make it work. Abstraction leaking, harder code navigation, slower compilation, etc.
How it could work
Resources
Resources are often initialized at startup, but also often consumed at startup.
Create a separate type like
ResInit<FooBar>
. So every system which consumesRes<FooBar>
andResMut<FooBar>
must be schedule after any system which acceptsResInit<FooBar>
.For example:
Unwrap in resources
Additionally, would be nice if bevy tracked at graph construction time which resources are initialized. Basically:
Res<T>
should never be an "option", if you need it optional, make itRes<Option<T>>
Res<T>
, but no system withResInit<T>
So if I forgot to add resource initialized, fail early with clear message instead of on some rare case during gameplay.
Events
If there are systems some are event readers, and others are event writers for the same event type, they should be ordered, so that:
(Assuming these are in the same schedule, if they are not, it's fine as is.)
I.e.
Barrier
Add a new primitive, I'll call it barrier
So the graph builder add before/after dependencies between these systems.
I think currently it is possible to kind of achieve similar behavior by creating a dummy empty system and schedule things before/after that dummy system at
app
initialization, but it is less explicit (system definition and system initialization are far apart in code, so it is fragile).(Additionally, I'm scared of compiler optimizations. System identity seems to be a function pointer. And compiler at high optimization levels may collapse two identical functions into one, so if I use empty function as a barrier, something may not work as expected, but I didn't check.)I'm wrong, there's no identity.Piping
.pipe
is part I like, but not ideal:In my perfect view it would work like this:
How it would look like ideally, in my subjective opinion.
I think this can probably be emulated with
Changed
filter. Don't know. ButChanged
filter has the same issue: it does not enforce that whoever is subscribed to change will be scheduled after the update was made.That's it. Thanks for reading.
Beta Was this translation helpful? Give feedback.
All reactions