-
-
Notifications
You must be signed in to change notification settings - Fork 3.6k
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
Unify FixedTime
and Time
while fixing several problems
#8964
Unify FixedTime
and Time
while fixing several problems
#8964
Conversation
6f74262
to
03d2c1b
Compare
Example |
2 similar comments
Example |
Example |
ad80620
to
7f22e4a
Compare
FixedTime
and Time
while fixing several problems
1b38ee0
to
288aeb9
Compare
You added a new example but didn't update the readme. Please run |
2 similar comments
You added a new example but didn't update the readme. Please run |
You added a new example but didn't update the readme. Please run |
6f40e09
to
c909405
Compare
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 quite like these changes! The Time
overriding behavior is a bit implicit. But I do agree that making systems usable across contexts is useful. And it means 99% of users can just consume Time::delta
in any context and it will "just work". This does make the system slightly harder to "fully grasp" as a result. I think some users will think they are consuming the exact same value in both contexts. But I think the tradeoff is worth it. For users that need to care, you wrote really nice docs explaining everything. I cannot conceive of a better system.
Great work!
(I just have one comment on the default fixed timestep value)
@nakedible let me know if you want a hand with those docs. Once those are written, we'll do a quick pass of review and then I can merge this in for you :) |
Nah, I'll handle the doc change later today, thanks though! |
25e8660
to
da07cfa
Compare
da07cfa
to
1f2aee7
Compare
7e9980b
to
4a1db6b
Compare
Some extra doc lint checks were added in the meantime, so cleaned up those too so that tests pass again. Should be merge ready from my side. |
…e#8964) # Objective Current `FixedTime` and `Time` have several problems. This pull aims to fix many of them at once. - If there is a longer pause between app updates, time will jump forward a lot at once and fixed time will iterate on `FixedUpdate` for a large number of steps. If the pause is merely seconds, then this will just mean jerkiness and possible unexpected behaviour in gameplay. If the pause is hours/days as with OS suspend, the game will appear to freeze until it has caught up with real time. - If calculating a fixed step takes longer than specified fixed step period, the game will enter a death spiral where rendering each frame takes longer and longer due to more and more fixed step updates being run per frame and the game appears to freeze. - There is no way to see current fixed step elapsed time inside fixed steps. In order to track this, the game designer needs to add a custom system inside `FixedUpdate` that calculates elapsed or step count in a resource. - Access to delta time inside fixed step is `FixedStep::period` rather than `Time::delta`. This, coupled with the issue that `Time::elapsed` isn't available at all for fixed steps, makes it that time requiring systems are either implemented to be run in `FixedUpdate` or `Update`, but rarely work in both. - Fixes bevyengine#8800 - Fixes bevyengine#8543 - Fixes bevyengine#7439 - Fixes bevyengine#5692 ## Solution - Create a generic `Time<T>` clock that has no processing logic but which can be instantiated for multiple usages. This is also exposed for users to add custom clocks. - Create three standard clocks, `Time<Real>`, `Time<Virtual>` and `Time<Fixed>`, all of which contain their individual logic. - Create one "default" clock, which is just `Time` (or `Time<()>`), which will be overwritten from `Time<Virtual>` on each update, and `Time<Fixed>` inside `FixedUpdate` schedule. This way systems that do not care specifically which time they track can work both in `Update` and `FixedUpdate` without changes and the behaviour is intuitive. - Add `max_delta` to virtual time update, which limits how much can be added to virtual time by a single update. This fixes both the behaviour after a long freeze, and also the death spiral by limiting how many fixed timestep iterations there can be per update. Possible future work could be adding `max_accumulator` to add a sort of "leaky bucket" time processing to possibly smooth out jumps in time while keeping frame rate stable. - Many minor tweaks and clarifications to the time functions and their documentation. ## Changelog - `Time::raw_delta()`, `Time::raw_elapsed()` and related methods are moved to `Time<Real>::delta()` and `Time<Real>::elapsed()` and now match `Time` API - `FixedTime` is now `Time<Fixed>` and matches `Time` API. - `Time<Fixed>` default timestep is now 64 Hz, or 15625 microseconds. - `Time` inside `FixedUpdate` now reflects fixed timestep time, making systems portable between `Update ` and `FixedUpdate`. - `Time::pause()`, `Time::set_relative_speed()` and related methods must now be called as `Time<Virtual>::pause()` etc. - There is a new `max_delta` setting in `Time<Virtual>` that limits how much the clock can jump by a single update. The default value is 0.25 seconds. - Removed `on_fixed_timer()` condition as `on_timer()` does the right thing inside `FixedUpdate` now. ## Migration Guide - Change all `Res<Time>` instances that access `raw_delta()`, `raw_elapsed()` and related methods to `Res<Time<Real>>` and `delta()`, `elapsed()`, etc. - Change access to `period` from `Res<FixedTime>` to `Res<Time<Fixed>>` and use `delta()`. - The default timestep has been changed from 60 Hz to 64 Hz. If you wish to restore the old behaviour, use `app.insert_resource(Time::<Fixed>::from_hz(60.0))`. - Change `app.insert_resource(FixedTime::new(duration))` to `app.insert_resource(Time::<Fixed>::from_duration(duration))` - Change `app.insert_resource(FixedTime::new_from_secs(secs))` to `app.insert_resource(Time::<Fixed>::from_seconds(secs))` - Change `system.on_fixed_timer(duration)` to `system.on_timer(duration)`. Timers in systems placed in `FixedUpdate` schedule automatically use the fixed time clock. - Change `ResMut<Time>` calls to `pause()`, `is_paused()`, `set_relative_speed()` and related methods to `ResMut<Time<Virtual>>` calls. The API is the same, with the exception that `relative_speed()` will return the actual last ste relative speed, while `effective_relative_speed()` returns 0.0 if the time is paused and corresponds to the speed that was set when the update for the current frame started. ## Todo - [x] Update pull name and description - [x] Top level documentation on usage - [x] Fix examples - [x] Decide on default `max_delta` value - [x] Decide naming of the three clocks: is `Real`, `Virtual`, `Fixed` good? - [x] Decide if the three clock inner structures should be in prelude - [x] Decide on best way to configure values at startup: is manually inserting a new clock instance okay, or should there be config struct separately? - [x] Fix links in docs - [x] Decide what should be public and what not - [x] Decide how `wrap_period` should be handled when it is changed - [x] ~~Add toggles to disable setting the clock as default?~~ No, separate pull if needed. - [x] Add tests - [x] Reformat, ensure adheres to conventions etc. - [x] Build documentation and see that it looks correct ## Contributors Huge thanks to @alice-i-cecile and @maniwani while building this pull. It was a shared effort! --------- Co-authored-by: Alice Cecile <[email protected]> Co-authored-by: Cameron <[email protected]> Co-authored-by: Jerome Humbert <[email protected]>
Can you give an advice on how I can create a custom In my own crate, I can't add error[E0116]: cannot define inherent `impl` for a type outside of the crate where the type is defined
|
3 | impl Time<MyTime> {
| ^^^^^^^^^^^^^^^^^ impl for type defined outside of crate.
|
= note: define and implement a trait or new type instead But I'd like users to have the same interface (e.g. Is a custom trait (that people have to explicitly import) the only option here? |
@nakedible, why did you decide to go with: pub struct Time<T> { ... }
pub struct Fixed;
fn system(time: Res<Time<Fixed>>) {} as opposed to this?: pub struct Time {...}
#[derive(Deref, DerefMut)]
pub struct FixedTime(Time);
fn system(time: Res<FixedTime>) {} I.e. why |
I think because this allows |
In the design phase I got some opposition to doing this with
Honestly could've gone either way. |
It's a little clunky, but pub trait MyTrait {
/* ... */
}
impl MyTrait for Time<MyTime> {
/* ... */
} |
There seems to be a slight mistake in the migration guide:
Should instead say:
This mistake also appears in the migration guide |
In snake the snake only moves one square every so often. In Bevy we can emulate this with fixed time steps using [`Time`](https://docs.rs/bevy/0.12.0/bevy/prelude/struct.Time.html); specifically a variant of `Time` called [`Fixed`](https://docs.rs/bevy/0.12.0/bevy/time/struct.Fixed.html). In `src/main.rs`, we start off by inserting a new `Resource` using [`Time<Fixed>`](https://docs.rs/bevy/0.12.0/bevy/time/struct.Time.html#impl-Time%3CFixed%3E) to handle counting our time steps. In this case I've chosen to make the step run every 0.1 seconds. ```rust .insert_resource(Time::<Fixed>::from_seconds(0.1)) ``` <details><summary>more about `Time` and `Fixed`</summary> There are a lot of different ways to deal with time. In Bevy, there's ways of accessing [wall-clock time](https://docs.rs/bevy/0.12.0/bevy/time/struct.Real.html), [virtual time](https://docs.rs/bevy/0.12.0/bevy/time/struct.Virtual.html), and [fixed time](https://docs.rs/bevy/0.12.0/bevy/time/struct.Fixed.html). Each of these ways of accessing time share a large set of functions for dealing with time, so it makes sense that we'd have a way to represent all of that shared functionality. Here is the [Pull Request](bevyengine/bevy#8964) that implemented this unified interface. [`Time`](https://docs.rs/bevy/0.12.0/bevy/prelude/struct.Time.html) then, is a type that accepts a generic type argument (in this case we're using [`Fixed`](https://docs.rs/bevy/0.12.0/bevy/time/struct.Fixed.html)). There are additional functions implemented on [`Time<Fixed>`](https://docs.rs/bevy/0.12.0/bevy/time/struct.Time.html#impl-Time%3CFixed%3E) that don't exist on other specializations of `Time`. One such function is `from_seconds` which is used to instantiate a new `Time<Fixed>` value that we can insert as a `Resource`. The syntax for accessing the `from_seconds` function associated with the `Time<Fixed>` type is not written as `Time<Fixed>::from_seconds` but rather is `Time::<Fixed>::from_seconds`. This is mostly because if we didn't use the turbofish syntax then it could be ambiguous as to whether the programmer meant to write "greater than" and "less than" or "a generic type". </details> After creating our `Time<Fixed>` `Resource`, we need to add a system that gets executed on the interval we defined. We'll use the [`FixedUpdate`](https://docs.rs/bevy/0.12.0/bevy/app/struct.FixedUpdate.html) schedule and a new system we haven't created yet called `tick`. ```rust .add_systems(FixedUpdate, tick) ``` The `tick` system will be defined in `src/lib.rs`, so we can write the use item now to bring it into scope. ```rust use snake::{ board::{spawn_board, Board}, snake::{spawn_snake, Snake}, tick, }; ``` In `src/lib.rs` , we can drop in a new function that logs out the word `"tick!"`. This system will run once every 0.1 seconds, as we specified in our `src/main.rs`. ```rust pub mod board; pub mod colors; pub mod snake; use bevy::prelude::*; pub fn tick() { info!("tick!"); } ``` `info!` comes from the [tracing](https://docs.rs/tracing/0.1.40/tracing/index.html) crate, which Bevy includes and sets up for us. > [!NOTE] tracing in Bevy > Bevy handles setting up logging in different ways depending on the environment we're running in using the [`LogPlugin`](https://docs.rs/bevy/0.12.0/bevy/log/struct.LogPlugin.html), which is part of the [`DefaultPlugins`](https://docs.rs/bevy/0.12.0/bevy/struct.DefaultPlugins.html) After running `cargo run`, we can see our new system logging out `"tick"` on the interval we specified. The output will include the timestamp of the log, the log level (`INFO` in this case), any span information, and finally the message we logged out. ``` 2023-11-13T17:54:07.073260Z INFO snake: tick! 2023-11-13T17:54:07.174248Z INFO snake: tick! 2023-11-13T17:54:07.273097Z INFO snake: tick! 2023-11-13T17:54:07.373646Z INFO snake: tick! 2023-11-13T17:54:07.473044Z INFO snake: tick! ``` In the next lesson we'll combine this system with our VecDeque to handle our snake's movement.
…e#8964) # Objective Current `FixedTime` and `Time` have several problems. This pull aims to fix many of them at once. - If there is a longer pause between app updates, time will jump forward a lot at once and fixed time will iterate on `FixedUpdate` for a large number of steps. If the pause is merely seconds, then this will just mean jerkiness and possible unexpected behaviour in gameplay. If the pause is hours/days as with OS suspend, the game will appear to freeze until it has caught up with real time. - If calculating a fixed step takes longer than specified fixed step period, the game will enter a death spiral where rendering each frame takes longer and longer due to more and more fixed step updates being run per frame and the game appears to freeze. - There is no way to see current fixed step elapsed time inside fixed steps. In order to track this, the game designer needs to add a custom system inside `FixedUpdate` that calculates elapsed or step count in a resource. - Access to delta time inside fixed step is `FixedStep::period` rather than `Time::delta`. This, coupled with the issue that `Time::elapsed` isn't available at all for fixed steps, makes it that time requiring systems are either implemented to be run in `FixedUpdate` or `Update`, but rarely work in both. - Fixes bevyengine#8800 - Fixes bevyengine#8543 - Fixes bevyengine#7439 - Fixes bevyengine#5692 ## Solution - Create a generic `Time<T>` clock that has no processing logic but which can be instantiated for multiple usages. This is also exposed for users to add custom clocks. - Create three standard clocks, `Time<Real>`, `Time<Virtual>` and `Time<Fixed>`, all of which contain their individual logic. - Create one "default" clock, which is just `Time` (or `Time<()>`), which will be overwritten from `Time<Virtual>` on each update, and `Time<Fixed>` inside `FixedUpdate` schedule. This way systems that do not care specifically which time they track can work both in `Update` and `FixedUpdate` without changes and the behaviour is intuitive. - Add `max_delta` to virtual time update, which limits how much can be added to virtual time by a single update. This fixes both the behaviour after a long freeze, and also the death spiral by limiting how many fixed timestep iterations there can be per update. Possible future work could be adding `max_accumulator` to add a sort of "leaky bucket" time processing to possibly smooth out jumps in time while keeping frame rate stable. - Many minor tweaks and clarifications to the time functions and their documentation. ## Changelog - `Time::raw_delta()`, `Time::raw_elapsed()` and related methods are moved to `Time<Real>::delta()` and `Time<Real>::elapsed()` and now match `Time` API - `FixedTime` is now `Time<Fixed>` and matches `Time` API. - `Time<Fixed>` default timestep is now 64 Hz, or 15625 microseconds. - `Time` inside `FixedUpdate` now reflects fixed timestep time, making systems portable between `Update ` and `FixedUpdate`. - `Time::pause()`, `Time::set_relative_speed()` and related methods must now be called as `Time<Virtual>::pause()` etc. - There is a new `max_delta` setting in `Time<Virtual>` that limits how much the clock can jump by a single update. The default value is 0.25 seconds. - Removed `on_fixed_timer()` condition as `on_timer()` does the right thing inside `FixedUpdate` now. ## Migration Guide - Change all `Res<Time>` instances that access `raw_delta()`, `raw_elapsed()` and related methods to `Res<Time<Real>>` and `delta()`, `elapsed()`, etc. - Change access to `period` from `Res<FixedTime>` to `Res<Time<Fixed>>` and use `delta()`. - The default timestep has been changed from 60 Hz to 64 Hz. If you wish to restore the old behaviour, use `app.insert_resource(Time::<Fixed>::from_hz(60.0))`. - Change `app.insert_resource(FixedTime::new(duration))` to `app.insert_resource(Time::<Fixed>::from_duration(duration))` - Change `app.insert_resource(FixedTime::new_from_secs(secs))` to `app.insert_resource(Time::<Fixed>::from_seconds(secs))` - Change `system.on_fixed_timer(duration)` to `system.on_timer(duration)`. Timers in systems placed in `FixedUpdate` schedule automatically use the fixed time clock. - Change `ResMut<Time>` calls to `pause()`, `is_paused()`, `set_relative_speed()` and related methods to `ResMut<Time<Virtual>>` calls. The API is the same, with the exception that `relative_speed()` will return the actual last ste relative speed, while `effective_relative_speed()` returns 0.0 if the time is paused and corresponds to the speed that was set when the update for the current frame started. ## Todo - [x] Update pull name and description - [x] Top level documentation on usage - [x] Fix examples - [x] Decide on default `max_delta` value - [x] Decide naming of the three clocks: is `Real`, `Virtual`, `Fixed` good? - [x] Decide if the three clock inner structures should be in prelude - [x] Decide on best way to configure values at startup: is manually inserting a new clock instance okay, or should there be config struct separately? - [x] Fix links in docs - [x] Decide what should be public and what not - [x] Decide how `wrap_period` should be handled when it is changed - [x] ~~Add toggles to disable setting the clock as default?~~ No, separate pull if needed. - [x] Add tests - [x] Reformat, ensure adheres to conventions etc. - [x] Build documentation and see that it looks correct ## Contributors Huge thanks to @alice-i-cecile and @maniwani while building this pull. It was a shared effort! --------- Co-authored-by: Alice Cecile <[email protected]> Co-authored-by: Cameron <[email protected]> Co-authored-by: Jerome Humbert <[email protected]>
Objective
Current
FixedTime
andTime
have several problems. This pull aims to fix many of them at once.FixedUpdate
for a large number of steps. If the pause is merely seconds, then this will just mean jerkiness and possible unexpected behaviour in gameplay. If the pause is hours/days as with OS suspend, the game will appear to freeze until it has caught up with real time.FixedUpdate
that calculates elapsed or step count in a resource.FixedStep::period
rather thanTime::delta
. This, coupled with the issue thatTime::elapsed
isn't available at all for fixed steps, makes it that time requiring systems are either implemented to be run inFixedUpdate
orUpdate
, but rarely work in both.time.delta()
not compatible with.run_if()
and timer conditions #8800FixedUpdate
schedules that take too long enter a cascading failure mode #8543FixedTime
API should matchTime
directly #7439Solution
Time<T>
clock that has no processing logic but which can be instantiated for multiple usages. This is also exposed for users to add custom clocks.Time<Real>
,Time<Virtual>
andTime<Fixed>
, all of which contain their individual logic.Time
(orTime<()>
), which will be overwritten fromTime<Virtual>
on each update, andTime<Fixed>
insideFixedUpdate
schedule. This way systems that do not care specifically which time they track can work both inUpdate
andFixedUpdate
without changes and the behaviour is intuitive.max_delta
to virtual time update, which limits how much can be added to virtual time by a single update. This fixes both the behaviour after a long freeze, and also the death spiral by limiting how many fixed timestep iterations there can be per update. Possible future work could be addingmax_accumulator
to add a sort of "leaky bucket" time processing to possibly smooth out jumps in time while keeping frame rate stable.Changelog
Time::raw_delta()
,Time::raw_elapsed()
and related methods are moved toTime<Real>::delta()
andTime<Real>::elapsed()
and now matchTime
APIFixedTime
is nowTime<Fixed>
and matchesTime
API.Time<Fixed>
default timestep is now 64 Hz, or 15625 microseconds.Time
insideFixedUpdate
now reflects fixed timestep time, making systems portable betweenUpdate
andFixedUpdate
.Time::pause()
,Time::set_relative_speed()
and related methods must now be called asTime<Virtual>::pause()
etc.max_delta
setting inTime<Virtual>
that limits how much the clock can jump by a single update. The default value is 0.25 seconds.on_fixed_timer()
condition ason_timer()
does the right thing insideFixedUpdate
now.Migration Guide
Res<Time>
instances that accessraw_delta()
,raw_elapsed()
and related methods toRes<Time<Real>>
anddelta()
,elapsed()
, etc.period
fromRes<FixedTime>
toRes<Time<Fixed>>
and usedelta()
.app.insert_resource(Time::<Fixed>::from_hz(60.0))
.app.insert_resource(FixedTime::new(duration))
toapp.insert_resource(Time::<Fixed>::from_duration(duration))
app.insert_resource(FixedTime::new_from_secs(secs))
toapp.insert_resource(Time::<Fixed>::from_seconds(secs))
system.on_fixed_timer(duration)
tosystem.on_timer(duration)
. Timers in systems placed inFixedUpdate
schedule automatically use the fixed time clock.ResMut<Time>
calls topause()
,is_paused()
,set_relative_speed()
and related methods toResMut<Time<Virtual>>
calls. The API is the same, with the exception thatrelative_speed()
will return the actual last ste relative speed, whileeffective_relative_speed()
returns 0.0 if the time is paused and corresponds to the speed that was set when the update for the current frame started.Todo
max_delta
valueReal
,Virtual
,Fixed
good?wrap_period
should be handled when it is changedAdd toggles to disable setting the clock as default?No, separate pull if needed.Contributors
Huge thanks to @alice-i-cecile and @maniwani while building this pull. It was a shared effort!