-
-
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
Add time scaling and simplify fixed timestep use #3002
Conversation
754d57f
to
7d77bb6
Compare
6a57c25
to
2dddfc6
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.
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.
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 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.) |
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.
Alright, this isn't a complete review. I only looked at the time.rs file, but here's a few things I noticed.
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
@@ -13,7 +13,7 @@ fn main() { | |||
} | |||
|
|||
#[derive(Component, Debug)] | |||
struct MyComponent(f64); | |||
struct MyComponent(f32); |
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.
This change seems unrelated.
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.
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.
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.
@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 <[email protected]>
# 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 <[email protected]>
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 <[email protected]>
Objective
Time
API.FixedTimestep
less prone to user error.Solution
Time
all havef32
,f64
, andDuration
variants.f32
andf64
values are cached, but derived from theDuration
value to minimize drift from rounding errors.FixedTime
counterpart toTime
for people to use in their fixed timestep systems.FixedTimesteps
resource (HashMap<String, FixedTimestepState>
).FixedTimestepState
now (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_startup
toTime::elapsed_since_startup
.Instant
of the firstTime
update (there's some delay after startup).Time::first_update
Time::relative_speed
Time::set_relative_speed
Time::raw_delta
Time::raw_elapsed_since_startup
FixedTime
resource with similar API.FixedTime::startup
(should give same value asTime::startup
)FixedTime::first_update
FixedTime::last_update
FixedTime::delta
FixedTime::set_delta
FixedTime::steps_per_second
FixedTime::set_steps_per_second
FixedTime::elasped_since_startup
FixedTimestepState
to be step-size agnostic so it works seamlessly with time scaling.FixedTimesteps
resource. (explained below)FixedTimestep::step
. Set the base step size/rate throughFixedTime
.Why only one built-in fixed timestep?
tl;dr
FixedTimestep
is for repeatable, deterministic behaviorFixedTimestep
won'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
dt
value, andFixedTimestep
just 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
dt
while the actualdt
between 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,
FixedTimestep
only 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
FixedTimestep
instances, their steps wouldn't run in a consistent order. E.g. if you had a 50ms timestepA
and 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),
A
andB
would 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
FixedTimestepState
pub
so people can build their ownHashMap<Key, FixedTimestepState>
resource if they need it for something exotic.