-
Notifications
You must be signed in to change notification settings - Fork 66
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
Centralized run criteria #29
Conversation
rfcs/centralized_run_criteria.md
Outdated
}), | ||
); | ||
``` | ||
- Globally, owned by a schedule, usable by anything inside said schedule via a 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.
This schedule-based API is very useful for raw consumers of bevy_ecs
, but less familiar for users of bevy_ecs
, who may not clue in that you can just add this to the AppBuilder
as well.
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 want to say that this RFC is about features of bevy_ecs
, AppBuilder
may as well not exist.
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.
Absolutely, but bevy_app
is a downstream consumer, and will have to respond to these API changes.
Not essential here though, just something to bear in mind.
rfcs/centralized_run_criteria.md
Outdated
Run criteria are utility systems that control whether regular systems should run during a given schedule execution. They can be used by entire schedules and stages, or by individual systems. Any system that returns `ShouldRun` can be used as a run criteria. | ||
|
||
Criteria can be defined in two ways: | ||
- Inline, directly attached to their user: |
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.
It's not immediately clear to beginners that they can also be attached to system sets, which will be a common pattern.
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.
System sets should be taught as "a way to attach things to multiple systems at the same time". "Criteria can be attached to systems" should click with that.
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.
System sets should be taught as "a way to attach things to multiple systems at the same time".
Cool, I like that framing a lot. I'll try to do this in the new book.
rfcs/centralized_run_criteria.md
Outdated
.add_system(player_dmg_buff_system.with_run_criteria(PlayerControlCriteria::CanAttack)) | ||
``` | ||
|
||
Since run criteria aren't forbidden from having side effects, it might become necessary to define an explicit execution order between them. This is done almost exactly as with regular systems, the main difference is that the label must be unique (to facilitate reuse, and piping - covered elsewhere): |
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.
What precisely does "unique" mean here? Must the system only have one label? Must the label only have one system?
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.
Both, actually.
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've revised the design in 10ee296. Now criteria can have as many labels as they want(e.g. for execution order specification), but only the labels that map to just one criteria support criteria reusing (and piping, which is irrelevant for this RFC so I omitted its mention).
rfcs/centralized_run_criteria.md
Outdated
Since run criteria aren't forbidden from having side effects, it might become necessary to define an explicit execution order between them. This is done almost exactly as with regular systems, the main difference is that the label must be unique (to facilitate reuse, and piping - covered elsewhere): | ||
```rust | ||
schedule | ||
.add_run_criteria(run_if_player_can_attack.label(PlayerControlCriteria::CanAttack)) |
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.
If labels are required, it feels like the schedule (and appbuilder) methods like this should just take two arguments.
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'm torn on this one: two arguments is correct Rust, descriptors everywhere is consistent Bevy.
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 still prefer two arguments so it's a a) compile-time error and b) immediately obvious what needs to be done.
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 think I have a solution: the label passed here as the separate argument is the label that can be used for piping and reusing, labels attached to the descriptor (the criteria-bearing argument) are for execution order and whatever else. It even eliminates this restriction.
rfcs/centralized_run_criteria.md
Outdated
|
||
## Drawbacks | ||
|
||
Stages will become unusable individually, outside of a schedule. |
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.
IMO this is fine because you can just make a schedule that contains a single stage.
In the long term, stages themselves are due for deprecation in some form so...
rfcs/centralized_run_criteria.md
Outdated
|
||
- It's unclear what the better approach is: implement this feature followed by `stageless_api`, or both of them at once. | ||
- Most of the implementation details are unknowable without actually starting on the implementation. | ||
- Should there be any special considerations for e.g. schedules nested inside stages? |
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.
Oh right: schedules implement the Stage trait... Have we seen any practical applications of this yet? Having a sense of that will help guide our design here.
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.
Startup systems.
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 really like this: it's a sensible refactor that makes other critical user-facing work much easier. I have a few nits on API design and clarity (left as notes on the code), but you have my agreement in principle.
- Change stages' own criteria to work with criteria descriptors. | ||
- Rearrange criteria descriptor structs and related method bounds to make globally declared criteria require labels. | ||
|
||
## Drawbacks |
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 think we're missing a pretty big drawback: there is now no way to make run criteria react to state calculated this frame. This is possible in the current implementation. Ex:
- StageA System: run a system where CanAttack is set according to some condition
- StageB System: use run criteria to only run a system when CanAttack is set
This is still possible to express in the new system, but it won't behave as expected because the "can attack" criteria is calculated at the start of the frame instead of at the start of StageB.
Of course, this can be done "inside" systems. But it creates the question "what is run criteria for"? "reusable conditional state that is calculated at the start of a schedule run only" is useful, but also pretty arbitrary and limited. Guiding users to the right abstraction might be challenging and I expect some amount of frustration in the event that they hit the limits of the abstraction.
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.
We can still react to changes on the same frame, the same way we're doing it within a single stage: rerunning systems with the relevant criteria after the reevaluation at the end of the stage - "feedback". But this "feedforward" reaction mode will indeed be lost.
Main difference between "feedforward" and schedule-wide "feedback" is that the former doesn't run state-specific systems of stages prior to the state change. I think it's possible to crowbar that back in, but it seems like more trouble than it's worth. But I'll add that to the drawbacks, for sure.
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 think we should come up with a list of scenarios that we want run criteria to support, then work back from there. In my head, "feedback-only" criteria only works (well) for "looping" criteria like fixed timestep and state transitions. Maybe this is fine, but I think its good to be intentional about it. And if run criteria is only useful for "some number of things countable on one hand", maybe its better to optimize for those things individually (ex: bake FSM drivers directly into the schedule executor).
Also, if we really do plan on rewriting criteria and feedback again for stageless_api
and first_class_fsm_drivers
, why bother with this intermediate design at all? We all have limited time to design, implement, and review things. Shouldn't we focus on designing the "final implementation"?
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.
(ex: bake FSM drivers directly into the schedule executor)
That's exactly what first_class_fsm_drivers
is referring to.
[...] why bother with this intermediate design at all?
Three reasons: the intermediate is useable and useful on its own, these changes would have to be made with a full redesign anyway, and it's a smaller changeset than the full redesign would be. The latter reason further leads to it being easier and faster to design, implement, and review.
I don't think that this being a separate RFC mandates that it must be its own PR: once we (well, me, probably) put together the namedropped RFCs, the actual implementation can be done in one pass, without this stopgap solution ever finding its way into a release.
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.
Im fine with this being a separate RFC (and I think you made the right call on that). But I definitely wouldn't merge it without the wider context being established and agreed upon first. As-is this breaks too many scenarios and punts too much to "future work" which hasn't been designed yet.
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.
That's perfectly fine by me, in fact, I much prefer this staying in flight, rather than have it land only to get amended later if we run into a problem.
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.
player_has_control
is referred to as run_if_player_has_control
, when added to stage.
Co-authored-by: Federico Rinaldi <gisquerin@gmail.com>
Right now, we have two modes of same-frame state change handling: | ||
|
||
* "Feedforward": state was changed in a previous stage, so this stage will run only systems that aren't forbidden by the new state. | ||
* "Feedback": a state change was detected while reevaluating criteria at the end of stage, so the stage runs again, with only the systems that are specific to the new state. |
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.
How is this:
a state change was detected while reevaluating criteria at the end of stage, so the stage runs again, with only the systems that are specific to the new state.
Compatible with this part of the RFC?
Run criteria are evaluated once at the start of schedule, any criteria returning
ShouldRun::*AndCheckAgain
will be evaluated again at the end of schedule. At that point, depending on the new result, the systems governed by those criteria might be ran again, followed by another criteria evaluation; this repeats until no criteria returnShouldRun::Yes*
. Systems with no run criteria always run exactly once per schedule execution.
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.
It's not. Not entirely, at least: the feedback paragraph refers to how things are now (roughly), the other one describes how they will be with this feature.
|
||
It should be noted that criteria declared globally are required to have a label, because they would be useless for their purpose otherwise. Criteria declared inline can be labelled as well, but using that for anything other than defining execution order is detrimental to code readability. | ||
|
||
Run criteria are evaluated once at the start of schedule, any criteria returning `ShouldRun::*AndCheckAgain` will be evaluated again at the end of schedule. At that point, depending on the new result, the systems governed by those criteria might be ran again, followed by another criteria evaluation; this repeats until no criteria return `ShouldRun::Yes*`. Systems with no run criteria always run exactly once per schedule execution. |
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.
Does this mean that a schedule with the following stages:
UpdateStage: MoveSystem (fixed timestep criteria)
PostUpdateStage: TransformPropagateSystem
RenderStage: RenderSystem
Could execute like this for a single frame?
MoveSystem (Yes and Loop)
TransformPropagateSystem
RenderSystem
MoveSystem (No)
Isn't this, like, very wrong? MoveSystem is intended to be run X times for the fixed timestep prior to Transforms being updated and RenderSystem rendering the final transform state. Instead, only the first timestep is rendered, then subsequent timesteps happen after rendering and show up next frame (and note that the position would be out of sync with the TransformPropagate state at the start of the next frame). Am I confused?
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.
No, you're right, however: this feature is breaking; what you outlined is a prime example of the breakage. The schedule and criteria that's okay in current implementation will most likely become nonsense - I should probably put that in drawbacks.
The intended solution is to either involve everything in a FSM, or rearrange the schedule to have all systems like "RenderSystem" at the start of the schedule. I'm not absolutely confident that's a good solution (on its own, anyway), hence the RFC.
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.
The intended solution is to either involve everything in a FSM, or rearrange the schedule to have all systems like "RenderSystem" at the start of the schedule.
I don't find the "move render system to the front" approach very satisfying. Render shouldn't run until PostUpdate runs, otherwise it will attempt to draw stuff that hasn't been "fully processed" yet. Putting it first feels wrong. We would need to create new machinery to ensure that it doesn't actually run on the first run, which feels complicated to me, especially if there are multiple stages that all rely on each other (ex: a PostRender stage). And it would mean that rendering happens for "the previous update" instead of "the current update". And managing "dependencies" between multiple stages (instead of just the Update->Render dependency) when they aren't necessarily inserted in the order they should run sounds nasty.
FSM might be a solution, but it feels like we should wait for that design before committing to this impl.
Implemented as part of #45. |
Rendered.
Lift run criteria and related machinery out of individual stages, and centralize them in the schedule instead.