Add time scaling and simplify fixed timestep use#3002
Add time scaling and simplify fixed timestep use#3002maniwani wants to merge 3 commits intobevyengine:mainfrom
Conversation
754d57f to
7d77bb6
Compare
6a57c25 to
2dddfc6
Compare
djeedai
left a comment
There was a problem hiding this comment.
This change seem to introduce time scaling. I'd be against doing so, if not at all, at least mixed in this PR with other changes, and instead have a proper discussion about it (apology if I missed it). Time scaling is a very delicate and complex topic, that is likely to break a lot of things, especially any game physics and animation.
djeedai
left a comment
There was a problem hiding this comment.
I can't seem to find any accumulated time / fixed time. Is there a reason for that? Unity has Time.time and Time.fixedTime, which are helpful sometimes, as not all logic can reason in terms of deltas. Can we add time() (which is I think last_update() maybe?) and fixed_time() as the accumulated time since startup of those 2 clocks? I expect if not, users will use their own accumulator, and this will end up being duplicated in several systems.
No, I just forgot.
That's Btw, |
7accde0 to
f9da443
Compare
|
Okay, I made edits to the fn comments, got rid of the "live" getters for the raw, unscaled time, and added a field to track the "current time" of the fixed loop. Now I'm wondering if separating the fixed stuff into a AFAIK you wouldn't need both time resources in a single system, so having that split can still have clean ergonomics. Well, UI systems that produce stuff consumed by the fixed-step logic might need both, idk. Something to think about. |
|
Nice! |
|
I think splitting makes sense, especially since accessing both is a code smell. The overall clock will be the latest but the fixed clock could be arbitrarily behind depending on the situation. |
5e82ddf to
a04d805
Compare
4f0e7da to
e0335c2
Compare
|
Decided to go ahead and split things into a separate (Also I messed up merging changes from main and had to rebase. Sorry if anyone was depending on my branch.) |
|
bors try |
Ah, okay. Since you had mentioned bpm, I just assumed it was headed in the direction of "let's use In that case, I think that how the fixed timestep works is pretty orthogonal to all that, since regardless of how many there are, they would all derive from
Yes, this is my rule of thumb as well. |
e09970a to
1441afd
Compare
|
|
||
| #[derive(Component, Debug)] | ||
| struct MyComponent(f64); | ||
| struct MyComponent(f32); |
There was a problem hiding this comment.
This change seems unrelated.
There was a problem hiding this comment.
It was either change this to be an f32 or use time.seconds_since_startup_f64() below. Since this is a user example, I went with the choice that looked better.
alice-i-cecile
left a comment
There was a problem hiding this comment.
@maniwani could you split out the relatively uncontroversial changes from this into a smaller PR?
I think the changes to Time (except for relative_speed) can stand alone and will be easy to merge.
# Objective Reduce the catch-all grab-bag of functionality in bevy_core by minimally splitting off time functionality into bevy_time. Functionality like that provided by #3002 would increase the complexity of bevy_time, so this is a good candidate for pulling into its own unit. A step in addressing #2931 and splitting bevy_core into more specific locations. ## Solution Pull the time module of bevy_core into a new crate, bevy_time. # Migration guide - Time related types (e.g. `Time`, `Timer`, `Stopwatch`, `FixedTimestep`, etc.) should be imported from `bevy::time::*` rather than `bevy::core::*`. - If you were adding `CorePlugin` manually, you'll also want to add `TimePlugin` from `bevy::time`. - The `bevy::core::CorePlugin::Time` system label is replaced with `bevy::time::TimeSystem`. Co-authored-by: Carter Anderson <mcanders1@gmail.com>
# Objective Reduce the catch-all grab-bag of functionality in bevy_core by minimally splitting off time functionality into bevy_time. Functionality like that provided by bevyengine#3002 would increase the complexity of bevy_time, so this is a good candidate for pulling into its own unit. A step in addressing bevyengine#2931 and splitting bevy_core into more specific locations. ## Solution Pull the time module of bevy_core into a new crate, bevy_time. # Migration guide - Time related types (e.g. `Time`, `Timer`, `Stopwatch`, `FixedTimestep`, etc.) should be imported from `bevy::time::*` rather than `bevy::core::*`. - If you were adding `CorePlugin` manually, you'll also want to add `TimePlugin` from `bevy::time`. - The `bevy::core::CorePlugin::Time` system label is replaced with `bevy::time::TimeSystem`. Co-authored-by: Carter Anderson <mcanders1@gmail.com>
3f8cdda to
527da04
Compare
527da04 to
0d83288
Compare
# Objective - Make `Time` API more consistent. - Support time accel/decel/pause. ## Solution This is just the `Time` half of #3002. I was told that part isn't controversial. - Give the "delta time" and "total elapsed time" methods `f32`, `f64`, and `Duration` variants with consistent naming. - Implement accelerating / decelerating the passage of time. - Implement stopping time. --- ## Changelog - Changed `time_since_startup` to `elapsed` because `time.time_*` is just silly. - Added `relative_speed` and `set_relative_speed` methods. - Added `is_paused`, `pause`, `unpause` , and methods. (I'd prefer `resume`, but `unpause` matches `Timer` API.) - Added `raw_*` variants of the "delta time" and "total elapsed time" methods. - Added `first_update` method because there's a non-zero duration between startup and the first update. ## Migration Guide - `time.time_since_startup()` -> `time.elapsed()` - `time.seconds_since_startup()` -> `time.elapsed_seconds_f64()` - `time.seconds_since_startup_wrapped_f32()` -> `time.elapsed_seconds_wrapped()` If you aren't sure which to use, most systems should continue to use "scaled" time (e.g. `time.delta_seconds()`). The realtime "unscaled" time measurements (e.g. `time.raw_delta_seconds()`) are mostly for debugging and profiling.
|
|
# Objective - Make `Time` API more consistent. - Support time accel/decel/pause. ## Solution This is just the `Time` half of bevyengine#3002. I was told that part isn't controversial. - Give the "delta time" and "total elapsed time" methods `f32`, `f64`, and `Duration` variants with consistent naming. - Implement accelerating / decelerating the passage of time. - Implement stopping time. --- ## Changelog - Changed `time_since_startup` to `elapsed` because `time.time_*` is just silly. - Added `relative_speed` and `set_relative_speed` methods. - Added `is_paused`, `pause`, `unpause` , and methods. (I'd prefer `resume`, but `unpause` matches `Timer` API.) - Added `raw_*` variants of the "delta time" and "total elapsed time" methods. - Added `first_update` method because there's a non-zero duration between startup and the first update. ## Migration Guide - `time.time_since_startup()` -> `time.elapsed()` - `time.seconds_since_startup()` -> `time.elapsed_seconds_f64()` - `time.seconds_since_startup_wrapped_f32()` -> `time.elapsed_seconds_wrapped()` If you aren't sure which to use, most systems should continue to use "scaled" time (e.g. `time.delta_seconds()`). The realtime "unscaled" time measurements (e.g. `time.raw_delta_seconds()`) are mostly for debugging and profiling.
# Objective - Make `Time` API more consistent. - Support time accel/decel/pause. ## Solution This is just the `Time` half of bevyengine#3002. I was told that part isn't controversial. - Give the "delta time" and "total elapsed time" methods `f32`, `f64`, and `Duration` variants with consistent naming. - Implement accelerating / decelerating the passage of time. - Implement stopping time. --- ## Changelog - Changed `time_since_startup` to `elapsed` because `time.time_*` is just silly. - Added `relative_speed` and `set_relative_speed` methods. - Added `is_paused`, `pause`, `unpause` , and methods. (I'd prefer `resume`, but `unpause` matches `Timer` API.) - Added `raw_*` variants of the "delta time" and "total elapsed time" methods. - Added `first_update` method because there's a non-zero duration between startup and the first update. ## Migration Guide - `time.time_since_startup()` -> `time.elapsed()` - `time.seconds_since_startup()` -> `time.elapsed_seconds_f64()` - `time.seconds_since_startup_wrapped_f32()` -> `time.elapsed_seconds_wrapped()` If you aren't sure which to use, most systems should continue to use "scaled" time (e.g. `time.delta_seconds()`). The realtime "unscaled" time measurements (e.g. `time.raw_delta_seconds()`) are mostly for debugging and profiling.
# Objective - Make `Time` API more consistent. - Support time accel/decel/pause. ## Solution This is just the `Time` half of bevyengine#3002. I was told that part isn't controversial. - Give the "delta time" and "total elapsed time" methods `f32`, `f64`, and `Duration` variants with consistent naming. - Implement accelerating / decelerating the passage of time. - Implement stopping time. --- ## Changelog - Changed `time_since_startup` to `elapsed` because `time.time_*` is just silly. - Added `relative_speed` and `set_relative_speed` methods. - Added `is_paused`, `pause`, `unpause` , and methods. (I'd prefer `resume`, but `unpause` matches `Timer` API.) - Added `raw_*` variants of the "delta time" and "total elapsed time" methods. - Added `first_update` method because there's a non-zero duration between startup and the first update. ## Migration Guide - `time.time_since_startup()` -> `time.elapsed()` - `time.seconds_since_startup()` -> `time.elapsed_seconds_f64()` - `time.seconds_since_startup_wrapped_f32()` -> `time.elapsed_seconds_wrapped()` If you aren't sure which to use, most systems should continue to use "scaled" time (e.g. `time.delta_seconds()`). The realtime "unscaled" time measurements (e.g. `time.raw_delta_seconds()`) are mostly for debugging and profiling.
# Objective Reduce the catch-all grab-bag of functionality in bevy_core by minimally splitting off time functionality into bevy_time. Functionality like that provided by bevyengine#3002 would increase the complexity of bevy_time, so this is a good candidate for pulling into its own unit. A step in addressing bevyengine#2931 and splitting bevy_core into more specific locations. ## Solution Pull the time module of bevy_core into a new crate, bevy_time. # Migration guide - Time related types (e.g. `Time`, `Timer`, `Stopwatch`, `FixedTimestep`, etc.) should be imported from `bevy::time::*` rather than `bevy::core::*`. - If you were adding `CorePlugin` manually, you'll also want to add `TimePlugin` from `bevy::time`. - The `bevy::core::CorePlugin::Time` system label is replaced with `bevy::time::TimeSystem`. Co-authored-by: Carter Anderson <mcanders1@gmail.com>
Objective
TimeAPI.FixedTimestepless prone to user error.Solution
Timeall havef32,f64, andDurationvariants.f32andf64values are cached, but derived from theDurationvalue to minimize drift from rounding errors.FixedTimecounterpart toTimefor people to use in their fixed timestep systems.FixedTimestepsresource (HashMap<String, FixedTimestepState>).FixedTimestepStatenow (only need to read it when you want to interpolate something).Setting the step rate isn't quite as ergonomic as before but it's still pretty easy to do.
Changelist
Time::time_since_startuptoTime::elapsed_since_startup.Instantof the firstTimeupdate (there's some delay after startup).Time::first_updateTime::relative_speedTime::set_relative_speedTime::raw_deltaTime::raw_elapsed_since_startupFixedTimeresource with similar API.FixedTime::startup(should give same value asTime::startup)FixedTime::first_updateFixedTime::last_updateFixedTime::deltaFixedTime::set_deltaFixedTime::steps_per_secondFixedTime::set_steps_per_secondFixedTime::elasped_since_startupFixedTimestepStateto be step-size agnostic so it works seamlessly with time scaling.FixedTimestepsresource. (explained below)FixedTimestep::step. Set the base step size/rate throughFixedTime.Why only one built-in fixed timestep?
tl;dr
FixedTimestepis for repeatable, deterministic behaviorFixedTimestepwon't work for thatA fixed timestep is "context" that wraps a block of systems and sort-of-but-not-really-decouples it from the main frame rate. (It's still inside the frame loop, so nothing is really decoupled.) The systems inside the block use your hardcoded
dtvalue, andFixedTimestepjust makes it so the average time between runs isdt. It'll regularly loop several times in a single frame to achieve that.So your systems always see a constant
dtwhile the actualdtbetween runs varies wildly. Great for getting consistent game physics, but useless if you wanted an actual fixed frequency (if you need that, your only option is a dedicated thread).Anyway,
FixedTimesteponly works correctly when it wraps a single system/stage/sub-schedule. You can't use a bunch of them in different places without running into some subtle side effects.For example, if you had two of
FixedTimestepinstances, their steps wouldn't run in a consistent order. E.g. if you had a 50ms timestepAand a 100ms timestepB, you'd probably expect them to interleave like this......which might often be the case. However, if the app stutters even a little (e.g. one frame takes half a second),
AandBwould get several steps queued up, and then their order on the next frame would be completely screwed up (because of how they catch up):So a single long frame could cause very subtle bugs.
Note: You can subdivide one timestep into longer ones by counting. For example, you can get a 1 second timestep from a 100ms timestep just by counting to 10 and having run criteria detect that. That gives variable—but still coarse—rate limiting without messing up the system order.
I still left
FixedTimestepStatepubso people can build their ownHashMap<Key, FixedTimestepState>resource if they need it for something exotic.