I've been developing a deterministic lockstep multiplayer game which requires multiple non-standard game loops:
Player input runs on its own semi-fixed update cadence; It can run more or less often based on packet loss (aka time dilation)
Simulation runs on a separate fixed update cadence, with a limited budget per rendered frame to prevent lockups
Render runs as often as possible up to some optional frame cap
What problem does this solve or what need does it fill?
On the one hand, there are tons of amazing plugins to use! On the other hand, Plugin strongly couples systems (what is done) to specific schedule (when it's done) through the build() API. Unfortunately, for anyone using non-standard game loops, this almost immediately makes most plugins useless out of the box.
Take for example something as core as the bevy provided InputPlugin:
Here, we see that InputPlugin adds event maintenance to the First schedule, which is part of the render loop. In this case, the coupling extends all the way down into app.rs. If I wanted to reuse this logic for my own input polling loop, I can't use InputPlugin. I can't even use add_event. At best, I must copy/paste large chunks of code from across bevy into my project using my schedules instead. This is less than idea because it's otherwise unnecessary work, but more so because I now have to keep that code in sync with upstream changes and improvements to bevy's. This process then repeats for each useful plugin I would like to leverage.
My personal feeling is the progress being made on FixedUpdate isn't actually all that valuable so far primarily because of this coupling issue. Improvementskeepcoming, but we can't quite manage to close issues like, Inputs can be missed (or duplicated) when using a fixed time step. Plugins are tightly coupled specifically to the bevy versions of the Update or FixedUpdate loops, making anything even slightly different immediately very tricky and/or painful.
What solution would you like?
I would love to somehow decouple what plugins do from when they do it. This doesn't inherently require any breaking changes, but I'm not set on a specific solution. Ultimately, those two aspects just need to be exposed in an (ideally easily) consumable way.
One potential solution I've been playing around with is to simply expose system sets for each logical chuck of systems, plus an "init". For example, this the the interface to my copy/pasted version of bevy's InputPlugin:
Consumers are then free to add the exposed system sets to whatever schedules that meet their needs. One could even theoretically use the same logic on multiple schedules at the same time this way (e.g. local UI input vs deterministic player input). Granted, you would need to work around resources being World global.
// similar behavior to bevy's InputPlugin
app
.init_tick_input()
.register_tick_input()
.add_systems(First, systems_tick_input_collect())
.add_systems(Last, systems_tick_input_gc());
// or, custom behavior uses the exact same logic
app
.init_tick_input()
.register_tick_input()
// keep collecting input events every time Update schedule runs
.add_systems(First, systems_tick_input_collect())
// clean them up if the tick schedule actually ran
.add_systems(TickLast, systems_tick_input_gc());
Plugin could trivially be implemented on top of this for the same, nice out-of-the-box experience provided today.
Also notice how I don't have to guess how long to keep events around for anymore, because the consumer specifies that information. Additionally, if systems_tick_input_collect is never used, no memory leak occurs.
Summary
Anyway, like I said, I'm not particularly attached to a specific implementation. I would love to hear other ideas. Mostly, I hope to get people thinking about ways "mechanism" can be decoupled in bevy, because I think it will make bevy all the better.
Thanks for reading, and thanks for working on bevy!
I agree with your points here! I think that this is best considered as part of #2160 :) This has been a problem for a long time, and your detailed write-up of your use case is really helpful.
Background
I've been developing a deterministic lockstep multiplayer game which requires multiple non-standard game loops:
What problem does this solve or what need does it fill?
On the one hand, there are tons of amazing plugins to use! On the other hand,
Plugin
strongly couples systems (what is done) to specific schedule (when it's done) through thebuild()
API. Unfortunately, for anyone using non-standard game loops, this almost immediately makes most plugins useless out of the box.Take for example something as core as the bevy provided
InputPlugin
:Here, we see that
InputPlugin
adds event maintenance to theFirst
schedule, which is part of the render loop. In this case, the coupling extends all the way down intoapp.rs
. If I wanted to reuse this logic for my own input polling loop, I can't useInputPlugin
. I can't even useadd_event
. At best, I must copy/paste large chunks of code from across bevy into my project using my schedules instead. This is less than idea because it's otherwise unnecessary work, but more so because I now have to keep that code in sync with upstream changes and improvements to bevy's. This process then repeats for each useful plugin I would like to leverage.My personal feeling is the progress being made on
FixedUpdate
isn't actually all that valuable so far primarily because of this coupling issue. Improvements keep coming, but we can't quite manage to close issues like, Inputs can be missed (or duplicated) when using a fixed time step. Plugins are tightly coupled specifically to the bevy versions of theUpdate
orFixedUpdate
loops, making anything even slightly different immediately very tricky and/or painful.What solution would you like?
I would love to somehow decouple what plugins do from when they do it. This doesn't inherently require any breaking changes, but I'm not set on a specific solution. Ultimately, those two aspects just need to be exposed in an (ideally easily) consumable way.
One potential solution I've been playing around with is to simply expose system sets for each logical chuck of systems, plus an "init". For example, this the the interface to my copy/pasted version of bevy's
InputPlugin
:Consumers are then free to add the exposed system sets to whatever schedules that meet their needs. One could even theoretically use the same logic on multiple schedules at the same time this way (e.g. local UI input vs deterministic player input). Granted, you would need to work around resources being
World
global.Plugin
could trivially be implemented on top of this for the same, nice out-of-the-box experience provided today.Also notice how I don't have to guess how long to keep events around for anymore, because the consumer specifies that information. Additionally, if
systems_tick_input_collect
is never used, no memory leak occurs.Summary
Anyway, like I said, I'm not particularly attached to a specific implementation. I would love to hear other ideas. Mostly, I hope to get people thinking about ways "mechanism" can be decoupled in bevy, because I think it will make bevy all the better.
Thanks for reading, and thanks for working on bevy!
Edit: fixed logic error in schedule example