-
-
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
Improve usability of StateStage and cut down on "magic" #1059
Conversation
Pinging @Ratysz @alice-i-cecile @sapir @alec-deason for feedback |
Seems like StateStage should be in prelude since this requires access to it in the normal case. |
So, to reiterate my understanding of the way this works.
Questions
|
I think this has to be true because it may not be possible to run all a given state's systems in the same stage. Some may need to be before UPDATE and some after, etc.
I have an example from my game. The player may be in a level (which is a state) and also have the ship editor active (which is a different state). If they exit the level at that point they must transition out of both the level state and the editor state. I've been handling that by having a central block of functions which handle complex transitions by flipping lower level states on or off. So the end_level function would change the level state and also the editor state if it was active. I haven't yet explored how I'll handle the issue with this new state API but it will likely be similar.
Hmm. That ambiguity didn't bother to me at all, or even occur to me. I just assumed it meant every tick while the state is active. |
I know that this isn't at all how the system works and I think it's already been discussed elsewhere but this is what I really want the state system to do: I want to be able to run a system in any arbitrary stage, but only when a state is active. So my states would mostly be mixed all together in UPDATE but they would run conditionally. I don't think any of the new state or conditional stage stuff actually handles that and maybe it just isn't practical. I think I can get something close to that by having multiple stages associated with a given state but it's not quite the same. Maybe that's the problem the SystemSets are meant to solve. I still haven't caught up with the discussion. |
You can, with the caveat that State "transitions" are resolved within the "currently executing StateStage". If you have a STATE_UPDATE
This is the pattern that makes the most sense to me for "state stage nesting" enum PauseState {
Paused,
Running,
}
enum AppState {
Menu,
InGame,
}
// This app will stop running the AppState stage whenever PauseState is "Paused"
app
.add_stage("pause", StateStage::<PauseState>::default())
.stage("pause", |pause_stage: &mut StateStage<PauseState>|
pause_stage
.set_update_stage(PauseState::Running, StateStage::<AppState>::default())
.update_stage(PauseState::Running, |app_stage: &mut StateStage<AppState>|
app_stage
.on_state_update(AppStat::Menu, some_menu_system)
.on_state_update(AppStat::InGame, some_game_system)
)
)
I can't think of anything I like more. Maybe
Currently it definitely isn't the same as slapping @alec-deason it definitely sounds like you want SystemSets. Thats exactly what they're for 😄 |
@alice-i-cecile if you really want "state intersection" logic (rather than the nesting logic), then with_run_criteria should work fine for that. |
Cool. I look forward to that feature then. But I have been able to express all my state logic with this version so far and I don't foresee any major problems as I move the rest over. Seems good. |
To be honest, I dislike cross-hierarchy methods like these. I understand they're there to make the simple cases easy to express, but I can't help but feel that there's a better way to facilitate this than duplicating the API of a lower-level construct on a higher-level one and forwarding the calls to some present-by-default instance. Auto-wrapping low-level bricks into appropriate high-level ones (when the user doesn't need to interact with said high level of the hierarchy directly) doesn't have the code duplication problem, but is indeed "magical" and occasionally ambiguous. Maybe making this wrapping explicit would be a good middle ground? Sort of related, I'm not quite sure how exactly system sets are going to fit together with states. |
Thats a fair take. I agree (to an extent). Ideally we have one api that works for everything. But for me this is a case of "Wanting to have my cake and eat it too". I think the "dynamicness" of the underlying api forces a certain amount of boilerplate on us for the "low level" api. Removing the "high level" api would mean pushing boilerplate on users for the common case (ex: the
I'm not quite sure what you are suggesting here. Could you provide an example?
I don't have all of the details sorted out, but the general idea is to add a new SystemSet for each lifecycle handler (Ex: enter + MyState::A, exit + MyState::B, etc). Then have each SystemSet criteria look something like this: // For the "MyState::A + exit" SystemSet
if state.just_exited() == Some(MyState::A) {
ShouldRun::YesAndLoop
} else if self.finished_evaluating(&state) {
ShouldRun::No
} else {
ShouldRun::NoAndLoop
} That doesn't solve the "how do we actually transition states" problem though. There could be a new system that drives the transitions, but we'd need a way to force it to run last. |
Oh, nothing advanced: it's like when we had
If all else fails the transitions can be handled by the executor explicitly, but I'm reasonably sure the implementation I'm working on will allow expressing "I want this to run at the very very end and it needs exclusive access to everything" as a system. Too early to go into specifics, though. |
I do like the "into" pattern you mentioned, but I still don't see how that would notably solve the "add system to StateStage on_enter lifecycle stage, assuming it is a SystemStage" problem, other than punting those semantics to the add_system() function (and a SystemDescriptor of some kind). I don't consider that to be a fundamental improvement in api design. Worth considering, but we'd still be adding a second "high level" api. I'm always down to consider other apis, but the one in this pr strikes a decent balance between flexibility and usability (the best balance I've come up with yet). I think I'm cool with merging this. We can always change it later if a better design comes along. At this stage in the project, we don't need to be perfect, just "better than we were before".
Yup thats where my head is at too. We can always hard-code logic if we can't build a suitable generic abstraction. |
The Problem
The "right" way to use StateStage was a bit unclear and there was too much "implicit" behavior. Additionally it was cumbersome for the "common case", which is "appending systems to a state's lifecycle event stage". This pr aims to improve the situation:
This PR
remove IntoStage trait
Builder functions that need Stages now just take stages directly instead of taking IntoStage. IntoStage made it easy to use either a Stage or a System when stages are needed, but it caused more confusion than it was worth.
StateStages now have default SystemStage handlers for their lifecycle events
This makes the internal implementation simpler / reduces branching. It also cuts down on user facing boilerplate.
on_state_X functions now take Systems
on_state_X take systems instead of Stages. These methods assume that the lifecycle stage is a SystemStage (which is true by default).
removed app.add_state()
This was too "magical". It created a new stage after UPDATE with an arbitrary name. This has been replaced with the less ergonomic, but much clearer:
Usage
Related: #1053 #1021