Skip to content
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

Lifecycle API #2432

Open
zarik5 opened this issue Jul 4, 2021 · 14 comments
Open

Lifecycle API #2432

zarik5 opened this issue Jul 4, 2021 · 14 comments
Labels
C-Enhancement A new feature S-Needs-Design-Doc This issue or PR is particularly complex, and needs an approved design doc before it can be merged

Comments

@zarik5
Copy link

zarik5 commented Jul 4, 2021

What problem does this solve or what need does it fill?

Android and XR apps often needs to keep track of the app lifecycle state and react differently to each of them. State-aware behavior is also often needed by backends an plugins implementations.

What solution would you like?

Some alternatives are:

  • .add_*_system(), where * is the name of a lifecycle state (or event). This follows the current .add_startup_system().
  • .add_system(my_system.when::<LifecycleState>())
  • .add_system_to_schedule(my_system, LifecycleState). .add_system() will be an alias for the running/steady state systems.

Possible names for the states:

  • Startup/OnCreate/Enter
  • Resume/OnResume/Resuming
  • Update/Running
  • Pause/OnPause/Pausing
  • OnDestroy/Exit

User resources are available in all states, but engine provided resources might not be available in every state.

@zarik5 zarik5 added C-Enhancement A new feature S-Needs-Triage This issue needs to be labelled labels Jul 4, 2021
@TheRawMeatball TheRawMeatball added S-Needs-Design-Doc This issue or PR is particularly complex, and needs an approved design doc before it can be merged and removed S-Needs-Triage This issue needs to be labelled labels Jul 4, 2021
@AlphaModder
Copy link

I wonder if this same mechanism could be leveraged in the editor for something like Unity's pause/play.

@zarik5
Copy link
Author

zarik5 commented Jul 5, 2021

@AlphaModder No, in-game pause is another thing. The app lifecycle is dictated by platform dependent events. Also when working with an editor, the code that runs the 3D view and other tools will be very different than the final game.

@zarik5
Copy link
Author

zarik5 commented Jul 5, 2021

I don't have much time for working on this, but if we decide the general shape of the API I can start writing the implementation and RFC. Personally I like more the .add_system_to_schedule() version (from @TheRawMeatball). This allows plugins with runners to define new lifecycle states (for example OpenXR has a particular state before or after a XR session is created, where the user can choose the parameters for creating a new session or bail out and close the app).

@AlphaModder
Copy link

@zarik5 In-game pause isn't really what I meant. As I understand it, the current state/run-condition mechanisms already support a pause mechanism intended for the player's use. I'm talking about something like the way the Unity editor can unilaterally pause execution of the game, possibly replace assets/code (though of course the latter is difficult with Rust), and then resume execution. Is that sort of event not similar to the platform requirements on Android or similar? It's an external, uncancelable request for the app to stop, persist its state, and reload it later.

@zarik5
Copy link
Author

zarik5 commented Jul 5, 2021

@AlphaModder The editor probably will own the runner (and replace or run the child game runner), so could do basically what it wants, even without this API. The app can react to the lifecycle event I'm describing here, but the type of pause you describe should not be detectable by the game.

@zarik5
Copy link
Author

zarik5 commented Jul 8, 2021

Screen Shot 2021-07-08 at 11 41 38

Here I propose to model the lifecycle API as stack-based, with states joined by an "enter" and "exit" transitions. It is important to distinguish between states and transitions, even just semantically. State systems could be run multiple times on a loop, while transition systems should be run only once between states. While states could be skipped, transitions should never be skipped, so for example Pause and Exit could run one after the other without hitting the Idle state. This should be true also for user-requested exiting of the app. These rules allow the user to organize the code so that key resources creation and destruction happen only during transitions, while state systems merely make use of these resources.

The OpenXR lifecycle is a bit of a mashup to accommodate both wired and standalone headsets. While having both Idle and WaitForDevice seems redundant (and SessionCreated and SessionEnd transitions are actually not that useful), they cannot be merged because Idle will provide the Session resource while WaitForDevice cannot. The OpenXR specification defines Hidden(Synchronized), Visible and Focused as separate states, but in practice the app should behave (mostly) in the same way in all three states. It is even discouraged to treat these three states differently, as it could throw off the runtime synchronization mechanism. The actual state will be provided as a resource to the Running state.

About the implementation, it was discussed to use the State API to implement this API, but ultimately it was decided it is not a good fit. Another possibility is that each state and transition is a separate schedule. While it could be nice to have a well defined API to construct lifecycles of different shapes, I think this is overkill, since we will have only a few engine-provided runners (winit + OpenXR + WebXR). I think the simplest solution is to let the runners manage the order of states and transitions runs.

After the lifecycle API is implemented, the Schedule can be simplified, removing the concept of "run only once" or "run multiple times". This will all be managed by the runners.

@Ratysz
Copy link
Contributor

Ratysz commented Jul 12, 2021

I see two dichotomies that need to be decided upon before we can start designing this proper.

Implementation:

  • lifecycle states are modeled by separate schedules
  • all systems live in the same schedule

API:

  • lifecycle states are equivalent to logic states
  • lifecycle states and logic states are on different levels

Unfortunately, implementation choice matters here: systems that live in separate schedules cannot interact with each other beyond sharing the world, i.e. they can't have explicit execution ordering, and they can't share run criteria, logic states, or systems.

If we don't split the schedule, we can still make lifecycle separate from logic in the API, but users won't have to duplicate (and then somehow commutate!) anything they want to share between lifecycle states.

This leaves us with three options:

  1. Split the schedule into lifecycle-specific subschedules.
  2. Keep everything in one schedule, and build lifecycle states using run criteria and/or logic states.
  3. Keep everything in one schedule, and make a separate, specialized API for lifecycle states.

I'm partial to option 3:

  • doesn't introduce new restrictions
  • exhibits the correct metaphor of distinct lifecycle states as the app overstructure
  • lets running the same copy of a system on different lifecycle events
  • lets sensibly sharing a logic state machine between different lifecycle states

@zarik5
Copy link
Author

zarik5 commented Jul 12, 2021

Unfortunately, implementation choice matters here: systems that live in separate schedules cannot interact with each other beyond sharing the world, i.e. they can't have explicit execution ordering, and they can't share run criteria, logic states, or systems.

I don't think this really matters. Every state and transition beside "Running" will be "utility" schedules. In every state and transition the user would want to do completely different things, I think the World will be enough to glue everything together. The only useful thing that is impossible by keeping separate schedules is sharing logic states, but this seems more of a limitation of the States API. But even then, the States API is a very high level abstraction, while the app lifecycle isn't, so there shouldn't be many useful cases where logic states are needed cross-lifecycle-states. Keep in mind that until now all bevy apps have been built on top of the "Running" state/schedule.

@Ratysz
Copy link
Contributor

Ratysz commented Jul 12, 2021

We had split schedules before, with the previous logic states implementation; we moved away from that for a reason. We arguably still have split schedules, in the form of stages, and it's pretty much unanimous that we want to get rid of them too ("stageless" thing that keeps coming up is all about that).

I don't think this really matters. [...] I think the World will be enough to glue everything together. The only useful thing that is impossible by keeping separate schedules is sharing logic states, [...]

It does. Logic states, run criteria, events, system labels, ambiguity sets - these aren't part of the world, and they all matter for expressing the systems graph and its behaviors; there will be more in the future, too (soft and hard sync points, as a definite example).

Splitting schedules will require duplicating this housekeeping in every subschedule by default, or make the user do that as needed - which will expose them to a lot of the guts that are neatly folded away in such a way that they don't have to think about it, ever (well, not entirely yet, but that's what we're working towards).

The only useful thing that is impossible by keeping separate schedules is sharing logic states, but this seems more of a limitation of the States API.

I would argue that inability to interoperate with logic states (and everything else, for that matter) is a limitation of the proposed lifecycle implementation, not the other way around.

In every state and transition the user would want to do completely different things, [...]

I would definitely want to be able to continue doing some of the things I do while running when the app goes idle. Keep a network connection alive, play music, react to system events...

[...] so there shouldn't be many useful cases where logic states are needed cross-lifecycle-states.

I want my game to automatically go into "Paused" logic state when the lifecycle switches to "Idle". While continuing to play music and react to system events and whatnot.

Regardless of all of this... Compared to my suggested option 3, what would the option 1 you argue for actually win us?

@zarik5
Copy link
Author

zarik5 commented Jul 12, 2021

My argument was we don't need to share logic states, run criteria, events, system etc, not that we have to avoid it, but yeah, you showed me some use cases.

What I don't want to do is making the lifecycle a special case of a schedule. I don't like that the top schedule wants to execute one stage after the other if I don't explicitly pick and execute each one in the right lifecycle order. But from the user perspective, this behavior of "running one stage after the other" is preferred. For this reason I think schedules should be child objects of the lifecycle object. Then in practice I'm ok with moving much of the Schedule intercommunication logic into the lifecycle object.

@Ratysz
Copy link
Contributor

Ratysz commented Jul 12, 2021

What I don't want to do is making the lifecycle a special case of a schedule. I don't like that the top schedule wants to execute one stage after the other if I don't explicitly pick and execute each one in the right lifecycle order.

I think there's a misunderstanding of what Schedule is in Bevy: it's not "one stage after the other" at all, it's "all of systems, their relations, and metasystems". It just so happens to take the form of a string of stages right now - which, as I've said, is what we'll be moving away from.

We don't need to ontologically nest a "schedule object" inside a "lifecycle object" to create the behavior we want. (Note: "we"; I'm with you in that the lifecycle should preclude all other user logic.)

Then in practice I'm ok with moving much of the Schedule intercommunication logic into the lifecycle object.

That would essentially make it into the Schedule of option 3, in all but name.

@zarik5
Copy link
Author

zarik5 commented Jul 12, 2021

Ok, I see now. It's just a matter of making the implementation ergonomic both for the users and for runners.

Then there is the question of how to handle transitions. My idea was to let runners have complete control, the top level schedule would just be an unordered collection of states. But I don't know if this goes against proper management of the schedule resources.

@Ratysz
Copy link
Contributor

Ratysz commented Jul 13, 2021

Spent some time thinking about it, all I can say is that I'm gonna need to think some more to answer that with any certainty. Lifecycle falls right in the middle of the (as of now) nebulous design space of stageless and potential logic states rework - and I fear we'll have to do all three of them at the same time, if we want to avoid having to lean onto halfway solutions.

So far I think we should put lifecycle in the system descriptor API (the builder thing, .add_system(my_system.when::<LifecycleState>())), and manage lifecycle states and transitions sort of like we do with the logic ones, but without exposing anything as run criteria. Commanding the app to change lifecycle state should be done through, well, commands - I'm not yet sure if it should be a dedicated queue, or if we could conscript the data operations (adding/removing entities/components etc) queue. This is not supposed to be actionable, I'm just spitballing.

kcking added a commit to kcking/bevy that referenced this issue Jan 6, 2022
now running into

thread '<unnamed>' panicked at 'Cannot get the native window, it's null and will always be null before Event::Resumed and after Event::Suspended. Make sure you only call this function between those events.', C:\Users\kevin\.cargo\registry\src\git.luolix.top-1ecc6299db9ec823\winit-0.26.1\src\platform_impl\android\mod.rs:598:13

perhaps this is related to bevyengine#2432
kcking added a commit to kcking/bevy that referenced this issue Jan 14, 2022
now running into

thread '<unnamed>' panicked at 'Cannot get the native window, it's null and will always be null before Event::Resumed and after Event::Suspended. Make sure you only call this function between those events.', C:\Users\kevin\.cargo\registry\src\git.luolix.top-1ecc6299db9ec823\winit-0.26.1\src\platform_impl\android\mod.rs:598:13

perhaps this is related to bevyengine#2432
kcking added a commit to kcking/bevy that referenced this issue Jan 23, 2022
now running into

thread '<unnamed>' panicked at 'Cannot get the native window, it's null and will always be null before Event::Resumed and after Event::Suspended. Make sure you only call this function between those events.', C:\Users\kevin\.cargo\registry\src\git.luolix.top-1ecc6299db9ec823\winit-0.26.1\src\platform_impl\android\mod.rs:598:13

perhaps this is related to bevyengine#2432
kcking added a commit to kcking/bevy that referenced this issue Jan 29, 2022
now running into

thread '<unnamed>' panicked at 'Cannot get the native window, it's null and will always be null before Event::Resumed and after Event::Suspended. Make sure you only call this function between those events.', C:\Users\kevin\.cargo\registry\src\git.luolix.top-1ecc6299db9ec823\winit-0.26.1\src\platform_impl\android\mod.rs:598:13

perhaps this is related to bevyengine#2432
@chotchki
Copy link

chotchki commented Mar 23, 2023

With the new stageless design in Bevy 0.10, would this be possible to use as run criteria for systems?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
C-Enhancement A new feature S-Needs-Design-Doc This issue or PR is particularly complex, and needs an approved design doc before it can be merged
Projects
Status: Tangentially Related
Development

No branches or pull requests

5 participants