-
-
Notifications
You must be signed in to change notification settings - Fork 3.7k
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
Schedule v2 #1021
Schedule v2 #1021
Conversation
Behaves - yes. :-) Great work. It lacks a crucial feature though. |
Is there a way of accessing the current Scheduler from a running system to obtain its state? fn main() {
let fixed_timestep_scheduler = SystemStage::parallel()
.with_run_criteria(FixedTimestep::step(2.0))
.with_system(fixed_update);
App::build()
.add_plugins(DefaultPlugins)
// this system will run once every update (it should match your screen's refresh rate)
.add_system(update)
// add a new stage that runs every two seconds
.add_stage("fixed_update", fixed_timestep_scheduler)
.add_resource(fixed_timestep_scheduler)
.run();
} but that does not build (as expected)
|
Hmm yeah passing both
(3) feels the nicest to me |
I like (3) too. |
To give some feedback on the state part of this new scheduling system, I have updated my Kataster mini-game, replacing its custom state management system with the new bevy state of this PR. First I'd like to say that I have been able to get rid of my home-made state system, and replace it with bevy state without any hacks. This gain ~5% lines of code and is globally clearer/cleaner. Now for the feedbacks, I see 4 points of discussion :
Again, as is, I have found the state implementation to be fully usable. |
`ForState` is an interesting idea, but I worry that it might be a bit
too specific for engine code. That, or it should probably be `ForStates`
and take a `Vec`. I'm not porting my currently mostly done large game to
Bevy because, well, it's currently mostly done. :) But if I were, I'd
have lots of entities I only wanted to exist during the in-game and
paused states. I'd want them to go away for the start-menu state,
though. I.e. in an Asteroids game, keep targets around while the game is
running or paused, but despawn them when someone lands in the initial
game menu state and is about to start a new round. Though maybe there
isn't a clean way to capture that idea in a reusable pattern--not sure.
Like what I'm seeing here!
|
In fact, my ForState takes a vec!, as some entity lifespan can extend multiple states. It was named ForStates in my custom version, but as I migrated gradually and needed the two version at the same time, the one for bevy state, I named ForState :). But should be ForStates when I'll do it in a plugin. |
@Bobox214
Hmm yeah thats a good point. I do think we generally want state change order to be well defined. We could:
Yeah I think that sort of "cross-state coherency" is probably best left as an exercise for the user. If common patterns emerge we could consider adding built in features for them, but it seems too early to consider that.
I also think this falls in to the "too early to commit to something" category. Theres a ton of different ways to track entities (especially across states), and it would be hard to build something that meets everyone's constraints. I'd rather just wait and see if common patterns emerge. I also hope that scenes (or just entities spawned under a root entity) will occupy this niche: ex: spawn in a complex scene for a given state, then despawn it when you're done.
Thats a really good idea. I'm sure there will be cases where knowing what state is being transitioned from/to would be very useful. |
For 1), I personnaly would prefer 3. And between 2 and 1 I don't know , 2 avoids the 'glitch' of entering/exiting a state instantaneously, without update, but this can make it difficult to debug if the overwrite was not intended. For 2), 3), fully aggree, these features can be proposed as plugins. 4). The fact that get() returns the next state when calling the exit system, is a bit strange to me, so a complete/clearer API would be better and useful. |
I would really like to see the stages used in e.g. Reasons to make this change:
Possible approaches for implementation:
#[derive(BevyStages)]
enum MyStages {
Lib1Stage(lib1::Stage),
Lib2Stage(lib2::Stage),
/* ... */
} Internally, make your enum of stages something like enum StageKey<T> {
Foo,
Bar,
Baz,
State(TypeId),
User(T),
} allowing you to use type signatures like:
1 is a partial solution, but basically all upside. 2 is apparently the most idiomatic but I worry about the end user burden. 3 would be ideal and gets all of the benefits without any extra boilerplate, but I'm not experienced enough to be sure that this is feasible throughout the code base. |
As an extension to 1 above, you could extend the Stage enum above to namespace third-party library stages by their crate as well, which would help avoid conflicts tremendously. If we nest the built-in stages as their own enums, this gives us: enum Stage
Startup(StartupStage),
Core(CoreStage),
User(String),
Lib(String, String) This would be much simpler to implement than either 2 or 3, and should capture most of their benefits. The syntax for user defined stages stays nice and simple, and the third-party stages are disambiguated nicely without too much boilerplate :) Of the benefits, we get 3 and 4, and half of 1 and 2. My largest complaint is that you might still get runtime failures, but that'll occur if you attempt to add things to a valid stage that hasn't been added no matter what we do. I think that this is a pretty clear win, and should make the internal code easier to follow and refactor too. |
This looks awesome, I was hoping you'd do something this expressive. Are we able to change the run criteria for stages during runtime? I'd like to be able to do things like change the fixed timestep for a given state on the fly. Think of something like Sim City or The Sims where the user can slow down or speed up the simulation speed. It's not entirely clear to me what nested stages do. If I have two stages, A and B, and B is nested inside of A, does this mean that B runs during stage A? If so I imagine this is really only useful when combined with run criteria to optionally run some logic? How does parallel vs sequential execution work in regard to nesting? On a final note, this is likely approaching the realm of unreasonable but I'd also like to be able to compose states from other states. For a game I'm currently working on I have an API which is somewhat similar to this, and I'm able to call .withProgramState(ProgramStateId); This is nice because it allows me to do things like have the core simulation (minus player input) in one state, and then use this state in various other contexts. So I have another state which is the core_simulation + input + gui (for the actual game), and then another state which is the main_menu + core_simulation + input_playback where with input_playback I'm feeding in pre-recorded input into the simulation. In other words, behind the main menu a pre-recorded gameplay demo takes place. The concept is similar to how mario 3 on the original NES played out a pre-recorded segment on the start screen. It looks like with the current setup you can already accomplish this sort of composability by segregating out the state build calls into separate functions, and then just reusing those function with maybe passing in a different state name for use in the build call? Thanks for the work you're doing, bevy is shaping up to be amazing. |
@alice-i-cecile: Thanks for exploring the "stage label" design space a bit. Enough people are interested in non-string stage labels that I do thinks its worth discussing (although unless we reach an immediate consensus, I won't block this pr on this topic as it is largely orthogonal to most of the changes in this PR). My criteriaFirst lets enumerate my personal requirements for stage labels:
The current state of thingsI consider the pattern we use today to reasonably cover 1-5: // bevy internally sets up some stages
mod bevy {
mod stage {
const UPDATE: &str = "update";
const RENDER: &str = "render";
}
app
.add_stage(stage::UPDATE, SystemStage::parallel())
.add_stage(stage::RENDER, SystemStage::parallel())
}
// user then builds on top
mod app_stage {
const CUSTOM: &str = "custom";
}
app
.add_stage_after(bevy::stage::UPDATE, app_stage::CUSTOM, SystemStage::parallel())
.add_system_to_stage(bevy::stage::UPDATE, system_a)
.add_system_to_stage(app_stage::CUSTOM, system_b)
Your reasons to make the changeYou might notice that my criteria lines up pretty similarly to your reasons to change.
I honestly don't think there is a huge problem here (in the context of following the pattern defined above). IDEs already autocomplete stage names. Stage names are nicely scoped and discoverable in api docs. I think there are almost no relevant functional differences between enums and the "mod/const" pattern. Of course users aren't forced to follow the mod/const pattern.
This is also not a problem provided you follow the mod/const pattern.
This is a good point, and imo one of the strongest arguments to adopt a strict However I'd like to point out that this strength is also the biggest weakness: each stage enum is (1) completely immutable by default and (2) only extensible manually via author-defined "custom" variants, which requires throwing away the static strictness in that context.
This is really the "big one". Its currently easy for plugin developers to step on each other's toes. Bevy is intended to be modular to a high degree, so protecting against cases like this is important. The designs suggestedFirst, to satisfy my "criteria 3" (and to perform the baseline functionality), each of these designs would need the following derives: #[derive(Debug, Eq, Hash, Clone)]
Notably this boilerplate produces functionality that we get "for free" by using strings.
This is a partial solve to the problem, but it comes at the cost of consistency. I personally don't like that "downstream" user defined stages are defined as Custom(String), whereas "upstream" stages have their own enum variants
The nesting and proc-macro feel like way too much abstraction for what we get (according to my own taste of course). I don't think people should need to go through that much "pomp and circumstance" just for stage labels. Unless I'm missing something, it also seems like it would force the App developer to fully construct the global schedule instead of incrementally constructing it via plugins, which puts too much burden on the user to understand the mechanics of their upstream dependencies.
This feels like the most viable alternative to me. It preserves the overall Bevy plugin structure and allows for consistent declarations of upstream vs downstream sibling stages. This is also the solution @Ratysz suggested (and provided sample code for here). This approach does notably solve the "plugins stepping on each other" problem (when using user-defined enums) because it includes the TypeId in the hash. Unfortunately it has some major downsides:
This by itself is enough to make me hesitant to use it. We lose the "consistency" and "resilient to human error" criteria because it supports literally every possible value. By convention, we could encourage enums, but if we're back to "safety by convention", why not just use the "mod/const" convention? We could constrain the values by only supporting types that implement a given trait (ex: StageKey), but that comes at the cost of additional abstraction / more derives the user needs to know about.
Any value/type can be added anywhere
We're adding another 160 lines to bevy for what I consider to be marginal gains. Collecting my thoughtsI have yet to encounter a perfect answer to this problem. Strings are simple to define and consume, but they come at the cost of "safety by convention", collision risk, and inducing a general "ew strings" response from seasoned developers (which is a healthy initial reaction, even if I consider it to be a bit of a false positive in this case). Every enum-based solution either:
In the face of decisions like this, I generally bias toward the simpler solution (which in my mind is strings). If we roll with an alternative, the only one that seems viable to me (so far) is @Ratysz's "any key" implementation (with a StageLabel trait used as a constraint to protect against people using things like ints). Which would end up looking like this: // bevy internally sets up some stages
mod bevy {
#[derive(Debug, Hash, Eq, PartialEq, Clone, StageLabel)]
enum Stage {
Update,
Render,
}
app
.add_stage(bevy::Stage::Update, SystemStage::parallel())
.add_stage(bevy::Stage::Render, SystemStage::parallel())
}
// user then builds on top
#[derive(Debug, Hash, Eq, PartialEq, Clone, StageLabel)]
enum AppStage {
Custom
}
app
.add_stage_after(bevy::Stage::Update, AppStage::Custom, SystemStage::parallel())
.add_system_to_stage(bevy::Stage::Update, system_a)
.add_system_to_stage(AppStage::Custom, system_b) From a "typo safety" perspective I consider this to be almost identical to strings. There is still nothing stopping users that don't know/care from doing this: #[derive(Debug, Hash, Eq, PartialEq, Clone, StageLabel)]
struct Label(String) But in practice thats harder than lazily dropping in a string. From a "plugin devs stepping on each other" perspective this is strictly better than strings because of TypeId hashing. From a UX perspective I might actually give it to "any key". It requires more typing (30% more non-whitespace characters), but the stage names themselves are easier to read and the So I guess the biggest cost is internal complexity cost, which is important, but takes a secondary role to UX. Dang I guess "any key + type constraints + derives" is now in first place in my head. Thats not where my head was at when I started writing this. |
Not yet, but in the future I'd like to add the ability to queue up app changes somehow (see the "next steps" section in the description)
To be clear, stages don't nest by default. You can build a stage that supports nesting and define the execution behavior arbitrarily. Currently the only Stage types that support nesting are:
Yup that should work!
❤️ |
@cart I agree with your analysis: Option 1 is verbose and doesn't offer enough benefits, while option 2 is a huge amount of overhead. I'm thrilled to see convergent thinking on @Ratysz's "any key + type constraints + derives" implementation, especially with some work fleshing out that it can work under the hood. I agree that this work is largely orthogonal to this PR: I think it would be best to spin up a new issue and then PR to implement those changes and focus on the other elements of this PR here. |
... I may have forgotten why exactly did I want to push for this when we talked on Discord earlier. Downstream string collisions was the chief reason; I'm glad this was brought up! I have to clarify: while I did have the idea to try and use "any key" thing here, I didn't come up with the original implementation - I found it in this reddit comment. I've tinkered with it some more, pulling in more snippets from other half-baked prototypes, and landed on an implementation that looks like this in use. Here is the trait itself; it should be trivial to add a derive macro for it, and the whole The methods of schedule mockup used in the example have delightfully simple bounds now: just |
I am concerned about the performance implications of fragmenting bevy apps into so many stages. This new API encourages users to have lots of stages. System parallelism / parallel execution can only happen within a schedule. By fragmenting into more stages (smaller schedules with fewer systems in each), bevy apps would become more and more serialized. This is contrary to one of the main benefits/motivations for using ECS, which is performance and scalability to easily run in parallel, to take advantage of modern CPUs with many cores. I have noticed that Bevy's parallelism tends to be fairly limited in practice. In my experience, there is usually one thread running at 100% cpu and the others are in the single digits at best. I blame this on bevy's stage-focused design, as stages effectively form "global barriers" for parallel execution. I am worried that this new API could make this even worse.
This really does not make me happy. It means that, to make use of states, game logic must become fragmented into many small schedules, which means they will have to run sequentially and only have parallelism within them. I see a lot of potential for using states (specifically, multiple state types) to control the execution of different systems. It would be a shame if doing this comes with the caveat of big performance implications. Why can't states just enable/disable systems within one big parallel schedule? P.S: BTW, this is also why (some time ago) I was proposing adding "system dependencies" as an alternative to stages, to move away from relying on stages as much as possible and keep stages coarse-grained. It needs to be possible and ergonomic to get the right sequential execution where it matters, without ruining parallelism for everything else. In order to have as much parallel execution as possible (with the current paradigms), means having as many systems as possible in the same schedule/stage. I understand why it's nice to have pre-update/update/post-update stages, to separate, say, rendering from update logic. But i don't like the trend of encouraging users to split up their update logic into finer-grained stages. |
I wouldn't say so. Stages are largely optional, to be utilized by users when they actually need them - when they have a group of systems that has to be executed before or after another group of systems. Fleshing out the API to be more expressive and flexible is not encouragement to use the API to solve every problem. Although, how Bevy's own systems are split between stages is another matter.
Again, this is optional. If someone does that that's on them.
Not really. ECS itself has nothing to do with schedules or stages or, amusingly, systems (in the way they're implemented in Bevy). It's purely a data storage structure with certain efficient access patterns - Bevy's structuring of logic via systems, stages, and schedules is orthogonal to that. ECS benefits of compositional design, columnar access, and access parallelization are still fully utilized within a stage, where systems actually live.
This is not the cause (at least not the primary cause) - it's the overzealous-by-default system dependency inferrence mechanism of the executor (which I assume migrated to this new API intact?). Implementing more stages, with different scheduling algorithms and ways to specify/infer dependencies, and then hand-tuning the default schedule by shifting systems around said stages, will most definitely result in better utilization. This would've been impractical to do with the old API, and is something that should be done in a follow-up PR rather than here.
That's unavoidable if the user's state transitions require execution of code. It's a concept identical to that of the startup schedule from the old API. Having a neat and centralized way to do that, instead of multitudes of conditional systems, is good.
I would actually prefer some version of that. A state should be able to add or remove systems to and from existing stages, not just specify its own fully-formed update stage. (Maybe there's a way to do that with this API already that I haven't seen yet.)
You're not the only one. I even wrote an entire article on the options. And, again, this should be a follow-up PR - this one seems focused getting the schedule's API right. |
Yeah I agree with you that stages are optional and it is up to users to decide how much they want to use them. Also, that we should have nice and ergonomic APIs (of course). The core of my argument really was my reaction to this part of the API proposal:
I don't like that the proposed API requires you to fragment your systems into schedules in order to use states. Why should my state-specific systems have to run separately from my state-agnostic systems? That's what I was talking about when I said that the new API encourages (even requires) unnecessary fragmentation, hurting parallelism. As you said, fragmenting the app into multiple schedules should be the user's choice. So it is important that the design of engine features (such as the new states) does not require users to do it. To provide another example: imagine how awful it would have been if you had to separate the sending and receiving systems into separate stages in order to use events.
So we agree that this should be addressed.
Yes, ECS is, first and foremost, an efficient, elegant, and expressive way to structure data and logic. However, easy parallelism is also one of the major advantages that makes it so attractive.
Of course, this is exactly why it is important to not fragment more than necessary (and why I was motivated to make my post). Having many small schedules takes away from those benefits. |
I think @jamadazi makes an excellent point. I think we can decouple this behaviour from stages entirely. Sample use cases for conditional systems
In each of these cases, there's no reason to wait for all of the other systems to catch up, and serious parallelization issues with doing so. This really seems to be a separate feature, and should go into a distinct pull request. Alternate proposal for conditional systems The idea of But we can make this fully orthogonal to the idea of stages. Instead of a group of At the beginning of each frame, we can check whether the Benefits:
|
It sounds like in general we are all in agreement: Ooh nice. The patterns you're suggesting all seem like a good fit. I'm down to use the "vec for schedules" approach. In practice it might not actually matter, but in theory it does (and encodes that intent).
Haha we "kind of" did too in bevy_reflect: bevy/crates/bevy_reflect/src/reflect.rs Line 31 in 3d386a7
@jamadazi I agree with @Ratysz's responses. To quickly parrot: stages are both "optional" and a useful (and often required) part of building a scheduler. They are "hard sync points" inserted to enable users to make logical guarantees (but should generally be kept to a minimum) and for "stage" logic to ensure it has unique access (which enables it to parallelize safely). I absolutely intend to include manual system dependencies (and some form of @Ratysz's improved parallel scheduling approach) which should largely resolve the issues you are bringing up. However this pr intentionally doesn't solve the "parallel scheduling algorithm" problem. Instead it aims to encapsulate it and enable people to build custom strategies / combine multiple strategies within the same "schedule". It kind of sounds like you're proposing a "global parallel scheduling framework" that allows custom logic to be inserted inside. That sounds desirable, but it still fits into the system in this pr (it just needs to be implemented as a Stage). Parallel State Execution (and other such things)@jamadazi brought up (and @alice-i-cecile offered solutions to) the issue that "Schedule V2 + parallel scheduler with manual system dependencies" doesn't inherently solve the "running the systems of multiple States in parallel within a given stage" problem. First: its a reasonable ask. Unrelated states should be able to run their logic in parallel. I'm not totally convinced we need to solve that now because:
That being said, its worth trying to plan out some next steps here (which might inform whether or not we drop the current State implementation in this pr before merging). I think @alice-i-cecile is on the right track here. However ideally states aren't 'special cased' when we solve the problem: the "sometimes a group of systems should run within a given stage and sometimes it shouldn't" problem should be solved generically. I can think of a few ways we could take it (roughly inspired by @alice-i-cecile's thoughts):
// this system would run at some arbitrary point in the schedule. Its purpose is to select if / when a system should be scheduled
fn my_state_system(schedule_queue: Res<ScheduleQueue>, state: Res<State<MyState>>) {
let stage = schedule_queue.get_stage::<ParallelStage>("update").unwrap();
match *state {
MyState::A => stage.queue_systems(a_update_systems)
MyState::B => stage.queue_systems(b_update_systems)
}
// also do the same sort of thing for "enter" and "exit" systems
}
app.add_stage(Stage::Update, SystemStage::parallel()
// MyState::A update
.add_system_set(SystemSet::new()
.with_run_criteria(|state: Res<State<MyState>>| state.should_run_update(MyState::A))
.add_system(a_update_system_1)
.add_system(a_update_system_2)
)
// MyState::B enter
.add_system_set(SystemSet::new()
.with_run_criteria(|state: Res<State<MyState>>| state.should_run_enter(MyState::B))
.add_system(b_enter_system)
)
)
|
I like the idea of having systems and schedules statically-defined. Adding dynamism here seems like a major potential source of complexity and cognitive overhead, with questionable benefit. One of the big reasons why we have stages, schedules, states, etc. is to help clearly reason about the logic. Therefore, I strongly prefer proposal (2) over proposal (1). (3) would also be fine, if you would prefer to keep things easier. |
Also BTW, I want to bring up another point: it seems like it would be very useful to have multiple state types for controlling different parts of an application. For example, UI state, network state, asset loading state, game state. It might seem obvious, but I just wanted to point out this use case, just to make sure we are all aware of it. This is one reason why I was concerned about fragmentation and reduction in parallelism. It would quickly compound in a large application using multiple state types with different sets of systems. |
My preference is definitely towards proposal 2, system sets. The syntax is clean, and I really like the way that it encourages bundling of systems, rather than the chaos of proposal 1. You keep the nice "with_run_criteria", and can keep schedules static, like you said. In games that need it, you could instead treat the schedule as dynamic.
Partly for this reason, I don't think that it makes sense to have a special State type (or trait). Storing system state in a resource seems much more natural to me, because it gives us that global behavior without needing to special case things and integrates well with run criteria. But the need to properly handle exiting and entering different states is important. I don't understand
well enough to see how the original proposal addressed it smoothly. What do we lose out on if we just add a system set in FIRST to check if we should enter the state, and then a second system set in LAST to check if we should leave the state? Use something like I can see that the solution in the PR allows you to transition between states more quickly, after each stage. I don't see the practical benefit of doing so, do you expect sub-frame responsiveness to be critical for some use cases? This also adds more burden to their use as you then need to ensure that each of your state-dependent systems works properly when started at any point in the frame, rather than relying on the fact that we enabled the state at the start of the frame, and that each of the systems in previous stages that depend on the state have already run. |
I also prefer (2) by a pretty wide margin. The only issue is making it work well for multiple state transitions.
Yes exactly that. The "frames required to evaluate state changes" shouldn't get bigger as the size of the state transition queue increases. I think many people would consider "deferring updates to the next frame" like this to be hugely wasteful (I certainly do). Consider the If someone encounters that problem (and cares about that latency), the solution shouldn't be "merge states" or "dont use states".
|
I think the move toward states & stages is a great one and the syntax is very elegant and easy to understand. Proposal 2 certainly makes the most sense to me for expressing a system set as it is not only wrap your head around but it also encourages organizing your project into separate system sets. I took a small crack at implementing EventStage. How it functions is essentially equivalent to this: SystemStage::parallel()
.with_run_criteria(run_criteria::<T>)
.with_system(next_event.chain(event_handler));
fn run_criteria<T>(mut reader: Local<EventReader<T>>, events: Res<Events<T>>) -> ShouldRun {
if reader.earliest(&events).is_some() {
ShouldRun::YesAndLoop
} else {
ShouldRun::No
}
}
fn next_event<T>(mut reader: Local<EventReader<T>>, events: Res<Events<T>>) -> T {
reader.earliest(&events).unwrap().clone()
} I apologize I am not the most experienced Rust developer, however here are a few of my thoughts on the implementation:
|
I think I've addressed the relevant feedback so I'm transitioning out of draft mode. |
.with_run_criteria( | ||
FixedTimestep::step(2.0) | ||
// labels are optional. they provide a way to access the current FixedTimestep state from within a system | ||
.with_label(LABEL), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a duplication here? We have to invent a name for the Stage and a label for SystemStage.
Can these be unified?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we should unify them:
- A StateStage has different "substages" for each State lifecycle handler. That means there can be a 1->Many relationship between stage name and run criteria.
- Not all stages have names (only stages that live inside a Schedule)
- The upcoming SystemSets will also have run criteria, and they definitely won't have an infer-able name.
It is reproducible if you change fn update(mut last_time: Local<f64>, time: Res<Time>, fixed_timesteps: Res<FixedTimesteps>) {
let fixed_timestep = fixed_timesteps.get(LABEL).unwrap(); Of course I can add It used to be that bevy/crates/bevy_winit/src/lib.rs Line 169 in aecc0bb
which makes me worry whether it introduces #916 bug again. |
We could fix this by adding a Stage::initialize function, then calling Schedule::initialize recursively before calling the root Schedule::run. The downsides:
I guess thats reasonable. I'll start adding it (but feel free to discuss alternatives)
Fortunately its not a problem because we still handle window creation events before running the startup systems (because they are now part of the main schedule). I just tested it on this branch and we can access windows from startup systems. It even works if we remove the line in question from the bevy_winit lib.rs! (which i think we should do) |
@smokku : alrighty I made the changes mentioned above. The changes you made to the "fixed timestep" now work. |
It increases code complexity more than I'd like, but it does cut down on user-facing surprises, so I guess its worth it. |
I tested my |
Alrighty I'm going to merge this so we can get some testing before the 0.4 release. Thanks everyone! |
Just realized, that there also was #690 that needed separate |
While testing this I've run across behavior that I find surprising and don't see mentioned in the discussion (though I may have missed it). It seems like we could add a second family of functions like |
I did the latter in #1053 and then implemented the |
Hmm yeah that is definitely problematic. Given that the other "add system" apis are all additive, the The main issue is that I'll need to think about this for a bit 😄 |
Extends bevyengine#1021 Fixes bevyengine#1117 This also allows avoiding the Clone bound on state Possible future work: - Make state use Eq instead
Draft Note
This is a draft because I'm looking for feedback on this api. Please let me know if you can think of improvements or gaps.
Bevy Schedule V2
Bevy's old
Schedule
was simple, easy to read, and easy to use. But it also had significant limitations:V2 of Bevy Schedule aims to solve these problems while still maintaining the ergonomics we all love:
Stage Trait
Stage is now a trait. You can implement your own stage types!
There are now multiple built in Stage types:
SystemStage
StateStage<T>
Bevy now supports states. More on this below!
Schedule
You read that right! Schedules are also stages, which means you can nest Schedules
States
By popular demand, we now support
State
s!The new
state.rs
example is the best illustrator of this feature. It shows how to transition between a Menu state and an InGame state. Thetexture_atlas.rs
example has been adapt to use states to transition between a Loading state and a Finished state.This enables much more elaborate app models:
Run Criteria
Criteria driven stages (and schedules): only run stages or schedules when a certain criteria is met.
Fixed Timestep:
Schedule Building
Adding stages now takes a Stage value:
Typed stage building with nesting:
Unified Schedule
No more separate "startup" schedule! It has been moved into the main schedule with a RunOnce criteria
startup_stage::STARTUP (and variants) have been removed in favor of this:
this is a non-breaking change. you can continue using the AppBuilder .add_startup_system() shorthand
Discussion Topics
stage.with_run_criteria()
)Next steps: