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

Discrete timer and stopwatch #2683

Closed
wants to merge 5 commits into from

Conversation

alice-i-cecile
Copy link
Member

Objective

  • Fixes Native Tick Timers #1633.
  • As expressed in the linked issue, tick timers are a fundamental tool for controlling game-logic in certain game genres (factory builders, tower defenses) where we want to slow down gameplay when frame rate drops and ensure perfect synchronization.
  • Using absolute time can be too complex and too inexact for many gameplay uses.

Solution

  • Create a DiscreteStopwatch and DiscreteTimer structs to closely mirror the existing Timer and Stopwatch API.
  • This allows users to trivially swap between the APIs and reduces learning and maintenance burden.

Directly mirrors existing Stopwatch, but for discrete time step use cases
Mirrors API and impl of Timer
@alice-i-cecile alice-i-cecile added A-Core Common functionality for all bevy apps C-Feature A new feature, making something new possible labels Aug 18, 2021
@github-actions github-actions bot added the S-Needs-Triage This issue needs to be labelled label Aug 18, 2021
@alice-i-cecile
Copy link
Member Author

Code duplication could be reduced somewhat by refactoring these types to use a shared trait. I decided not to for simplicity (there are only two variants, and no obvious extensions), but I'm happy to change that if others feel that I should.

I'm not sure where you would want to accept either a DiscreteTimer or a Timer, but permitting that would be another benefit of a trait-based impl.

@DJMcNab
Copy link
Member

DJMcNab commented Aug 18, 2021

Is any of this code taken from @maplant's repo? If so, it would be good to make sure we record that properly

(I don't expect that we'll need to get in touch with all contributors again, but I don't want to miss any if we do)

/// assert_eq!(stopwatch.elapsed(), 1);
/// ```
#[inline]
pub fn unpause(&mut self) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about continue in favor of unpause?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd probably call that out-of-scope for this PR; I'm just following the existing API :)

(I also slightly prefer unpause, and think resume is also better than continue if we want to change.)

Copy link
Contributor

@maniwani maniwani left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple doctests weren't updated from the original timer, but other than that LGTM.

crates/bevy_core/src/time/timer.rs Outdated Show resolved Hide resolved
crates/bevy_core/src/time/timer.rs Outdated Show resolved Hide resolved
@alice-i-cecile
Copy link
Member Author

Is any of this code taken from @maplant's repo? If so, it would be good to make sure we record that properly

Nope, this is written completely from scratch.

@mockersf
Copy link
Member

Is this really a fundamental feature? I couldn't find something similar in Unity or Godot after a quick look.

As you mentioned, if the goal is to keep the API close to Timer and Stopwatch, they should share a trait with those to ensure that. Otherwise this just adds maintenance complexity...

I would prefer the names (DiscreteStopwatch and DiscreteTimer) to not be related to time at all as it's not using time. Maybe Countdown and Ticker?

@maplant
Copy link

maplant commented Aug 18, 2021

Is this really a fundamental feature? I couldn't find something similar in Unity or Godot after a quick look.

As you mentioned, if the goal is to keep the API close to Timer and Stopwatch, they should share a trait with those to ensure that. Otherwise this just adds maintenance complexity...

I would prefer the names (DiscreteStopwatch and DiscreteTimer) to not be related to time at all as it's not using time. Maybe Countdown and Ticker?

It is definitely an important, fundamental feature. Unity does not have this because components have an Update method and corresponding Time.deltaTime value. If you search "implement timer in Unity", you will find tick timers. Case in point: https://gamedevbeginner.com/how-to-make-countdown-timer-in-unity-minutes-seconds/

@alice-i-cecile
Copy link
Member Author

Is this really a fundamental feature? I couldn't find something similar in Unity or Godot after a quick look.

I think this deserves to be in the engine to provide a consistent, useful API and point beginners in the right direction (rather than forcing everyone to write their own data structures for this constantly).

As you mentioned, if the goal is to keep the API close to Timer and Stopwatch, they should share a trait with those to ensure that. Otherwise this just adds maintenance complexity...

I can do that :) I think it's the right call too; I just didn't want to dive right in before I'd gotten some initial feedback.

I would prefer the names (DiscreteStopwatch and DiscreteTimer) to not be related to time at all as it's not using time. Maybe Countdown and Ticker?

IMO these will almost always be used as direct substitutes of the existing Timer and Countdown. We should try to reflect that in the API, ideally through both traits and naming.

@mockersf
Copy link
Member

mockersf commented Aug 18, 2021

It is definitely an important, fundamental feature. Unity does not have this because components have an Update method and corresponding Time.deltaTime value. If you search "implement timer in Unity", you will find tick timers. Case in point: https://gamedevbeginner.com/how-to-make-countdown-timer-in-unity-minutes-seconds/

This link describes the Timer that already exists in Bevy I think?

IMO these will almost always be used as direct substitutes of the existing Timer and Countdown. We should try to reflect that in the API, ideally through both traits and naming.

I disagree, real time timers will always be needed, if only for any kind of animation or sound

@maplant
Copy link

maplant commented Aug 18, 2021

It is definitely an important, fundamental feature. Unity does not have this because components have an Update method and corresponding Time.deltaTime value. If you search "implement timer in Unity", you will find tick timers. Case in point: https://gamedevbeginner.com/how-to-make-countdown-timer-in-unity-minutes-seconds/

This link describes the Timer that already exists in Bevy I think?

My mistake, what I am looking for is a FixedUpdate, found here: https://docs.unity3d.com/ScriptReference/MonoBehaviour.FixedUpdate.html This is a tick timer.

IMO these will almost always be used as direct substitutes of the existing Timer and Countdown. We should try to reflect that in the API, ideally through both traits and naming.

I disagree, real time timers will always be needed, if only for any kind of animation or sound

They may be necessary for display and sound given the way your update loop is structured (although I believe http://gameprogrammingpatterns.com/game-loop.html shows that you can do it in one place, and then have a fixed conversion between ticks and real time), however it's not very good to use system timers for gameplay.

If you have a fixed time step, then tick timers are absolutely a direct substitute for system timers. You're never going to update faster than a tick anywhere, so you don't get higher resolution without them.

@maniwani
Copy link
Contributor

maniwani commented Aug 18, 2021

IMO these will almost always be used as direct substitutes of the existing Timer and Stopwatch. We should try to reflect that in the API, ideally through both traits and naming.

I disagree, real time timers will always be needed, if only for any kind of animation or sound.

I wouldn't say Timer and Stopwatch can be replaced in most instances by these generic/unitless variants, but I think keeping those names in the suffix would help users more easily understand their use. In my mind, these discrete primitives fill the niche of "I want a generic interval" (e.g. run every 3 fixed update steps, every 10 frames, every 4 events, etc.). But I would also be okay with Countdown and Counter/Ticker.

(edit)

My mistake, what I am looking for is a FixedUpdate, found here: docs.unity3d.com/ScriptReference/MonoBehaviour.FixedUpdate.html This is a tick timer.

Also just wanted to say that a fixed timestep is not a timer. They're technically opposites. (I suppose this means that there could also be an integer variant of FixedTimestep, but that seems pretty esoteric.)

FixedTimestep

  • runs N steps per second
  • each step represents a fixed amount of time, but the actual time between steps varies

Timer

  • runs steps with at least N seconds in between
  • actual steps per second varies, because it does not "catch up" like FixedTimestep does

@alice-i-cecile
Copy link
Member Author

IMO these will almost always be used as direct substitutes of the existing Timer and Countdown. We should try to reflect that in the API, ideally through both traits and naming.

I disagree, real time timers will always be needed, if only for any kind of animation or sound

Sorry, my sentence structure was not clear there. The point I was trying to make is that "users will often want to swap back and forth between these types directly", not "one is strictly superior": they both have their niches but fundamentally fill a similar role which their API should reflect.

@mockersf mockersf removed the S-Needs-Triage This issue needs to be labelled label Aug 19, 2021
@alice-i-cecile
Copy link
Member Author

So @mockersf I've now refactored this PR to use traits :) I'm not thrilled with the end result, so I'd appreciate some mentorship. Here's my thought process:

  1. There's still a great deal of near-duplicated, non-trivial code.
  2. Some of this is due to getter-setter patterns, and the inability to directly store data on traits in Rust.
  3. We can't store most of the complex, tricky logic within the shared impl of the methods, because it requires APIs such as set_finished that we don't want to expose to the end user (because they'll end up putting their timers in invalid states).
  4. We can't just make these methods internal only, since this trait needs to be pub in order for others to add their own timers and stopwatches. See this discussion or this SO post.
  5. With the addition of some more trait bounds / clever work, I suspect we could use a derive macro for Stopwatch and Timer. This would reduce code duplication, but further increase complexity and make maintenance and optimization harder in the future. And again, I'm not sure that there's a large number of end users clamoring to make their own timers (that still behave fundamentally similarly and would benefit from the macro) once these two exist.

I've also had to rename Timer and Stopwatch to DurationTimer and DurationStopwatch, as Timer and Stopwatch are the obviously correct trait names. Which are clearer, but very heavy on boilerplate.

Upon writing this up this morning, I'm tempted to:

  1. Make the Timer and Stopwatch trait private.
  2. Change the names to something less ergonomic, and keep Timer and Stopwatch as they are on 0.5.
  3. Add more private methods that can break the promises of the public API.
  4. Use those private methods to reduce code duplication further.

Allowing end users to be able to extend the trait and refer to it in their code is nice, but it comes at a high cost.

@alice-i-cecile alice-i-cecile added the S-Needs-Design This issue requires design work to think about how it would best be accomplished label Sep 22, 2021
@alice-i-cecile
Copy link
Member Author

Closing out for now. The whole Timer API needs a cohesive redesign.

bors bot pushed a commit that referenced this pull request Oct 17, 2022
As mentioned in #2926, it's better to have an explicit type that clearly communicates the intent of the timer mode rather than an opaque boolean, which can be only understood when knowing the signature or having to look up the documentation.

This also opens up a way to merge different timers, such as `Stopwatch`, and possibly future ones, such as `DiscreteStopwatch` and `DiscreteTimer` from #2683, into one struct.

Signed-off-by: Lena Milizé <me@lvmn.org>

# Objective

Fixes #2926.

## Solution

Introduce `TimerMode` which replaces the `bool` argument of `Timer` constructors. A `Default` value for `TimerMode` is `Once`.

---

## Changelog

### Added

- `TimerMode` enum, along with variants `TimerMode::Once` and `TimerMode::Repeating`

### Changed

- Replace `bool` argument of `Timer::new` and `Timer::from_seconds` with `TimerMode`
- Change `repeating: bool` field of `Timer` with `mode: TimerMode`

## Migration Guide

- Replace `Timer::new(duration, false)` with `Timer::new(duration, TimerMode::Once)`.
- Replace `Timer::new(duration, true)` with `Timer::new(duration, TimerMode::Repeating)`.
- Replace `Timer::from_seconds(seconds, false)` with `Timer::from_seconds(seconds, TimerMode::Once)`.
- Replace `Timer::from_seconds(seconds, true)` with `Timer::from_seconds(seconds, TimerMode::Repeating)`.
- Change `timer.repeating()` to `timer.mode() == TimerMode::Repeating`.
james7132 pushed a commit to james7132/bevy that referenced this pull request Oct 19, 2022
)

As mentioned in bevyengine#2926, it's better to have an explicit type that clearly communicates the intent of the timer mode rather than an opaque boolean, which can be only understood when knowing the signature or having to look up the documentation.

This also opens up a way to merge different timers, such as `Stopwatch`, and possibly future ones, such as `DiscreteStopwatch` and `DiscreteTimer` from bevyengine#2683, into one struct.

Signed-off-by: Lena Milizé <me@lvmn.org>

# Objective

Fixes bevyengine#2926.

## Solution

Introduce `TimerMode` which replaces the `bool` argument of `Timer` constructors. A `Default` value for `TimerMode` is `Once`.

---

## Changelog

### Added

- `TimerMode` enum, along with variants `TimerMode::Once` and `TimerMode::Repeating`

### Changed

- Replace `bool` argument of `Timer::new` and `Timer::from_seconds` with `TimerMode`
- Change `repeating: bool` field of `Timer` with `mode: TimerMode`

## Migration Guide

- Replace `Timer::new(duration, false)` with `Timer::new(duration, TimerMode::Once)`.
- Replace `Timer::new(duration, true)` with `Timer::new(duration, TimerMode::Repeating)`.
- Replace `Timer::from_seconds(seconds, false)` with `Timer::from_seconds(seconds, TimerMode::Once)`.
- Replace `Timer::from_seconds(seconds, true)` with `Timer::from_seconds(seconds, TimerMode::Repeating)`.
- Change `timer.repeating()` to `timer.mode() == TimerMode::Repeating`.
james7132 pushed a commit to james7132/bevy that referenced this pull request Oct 28, 2022
)

As mentioned in bevyengine#2926, it's better to have an explicit type that clearly communicates the intent of the timer mode rather than an opaque boolean, which can be only understood when knowing the signature or having to look up the documentation.

This also opens up a way to merge different timers, such as `Stopwatch`, and possibly future ones, such as `DiscreteStopwatch` and `DiscreteTimer` from bevyengine#2683, into one struct.

Signed-off-by: Lena Milizé <me@lvmn.org>

# Objective

Fixes bevyengine#2926.

## Solution

Introduce `TimerMode` which replaces the `bool` argument of `Timer` constructors. A `Default` value for `TimerMode` is `Once`.

---

## Changelog

### Added

- `TimerMode` enum, along with variants `TimerMode::Once` and `TimerMode::Repeating`

### Changed

- Replace `bool` argument of `Timer::new` and `Timer::from_seconds` with `TimerMode`
- Change `repeating: bool` field of `Timer` with `mode: TimerMode`

## Migration Guide

- Replace `Timer::new(duration, false)` with `Timer::new(duration, TimerMode::Once)`.
- Replace `Timer::new(duration, true)` with `Timer::new(duration, TimerMode::Repeating)`.
- Replace `Timer::from_seconds(seconds, false)` with `Timer::from_seconds(seconds, TimerMode::Once)`.
- Replace `Timer::from_seconds(seconds, true)` with `Timer::from_seconds(seconds, TimerMode::Repeating)`.
- Change `timer.repeating()` to `timer.mode() == TimerMode::Repeating`.
Pietrek14 pushed a commit to Pietrek14/bevy that referenced this pull request Dec 17, 2022
)

As mentioned in bevyengine#2926, it's better to have an explicit type that clearly communicates the intent of the timer mode rather than an opaque boolean, which can be only understood when knowing the signature or having to look up the documentation.

This also opens up a way to merge different timers, such as `Stopwatch`, and possibly future ones, such as `DiscreteStopwatch` and `DiscreteTimer` from bevyengine#2683, into one struct.

Signed-off-by: Lena Milizé <me@lvmn.org>

# Objective

Fixes bevyengine#2926.

## Solution

Introduce `TimerMode` which replaces the `bool` argument of `Timer` constructors. A `Default` value for `TimerMode` is `Once`.

---

## Changelog

### Added

- `TimerMode` enum, along with variants `TimerMode::Once` and `TimerMode::Repeating`

### Changed

- Replace `bool` argument of `Timer::new` and `Timer::from_seconds` with `TimerMode`
- Change `repeating: bool` field of `Timer` with `mode: TimerMode`

## Migration Guide

- Replace `Timer::new(duration, false)` with `Timer::new(duration, TimerMode::Once)`.
- Replace `Timer::new(duration, true)` with `Timer::new(duration, TimerMode::Repeating)`.
- Replace `Timer::from_seconds(seconds, false)` with `Timer::from_seconds(seconds, TimerMode::Once)`.
- Replace `Timer::from_seconds(seconds, true)` with `Timer::from_seconds(seconds, TimerMode::Repeating)`.
- Change `timer.repeating()` to `timer.mode() == TimerMode::Repeating`.
ItsDoot pushed a commit to ItsDoot/bevy that referenced this pull request Feb 1, 2023
)

As mentioned in bevyengine#2926, it's better to have an explicit type that clearly communicates the intent of the timer mode rather than an opaque boolean, which can be only understood when knowing the signature or having to look up the documentation.

This also opens up a way to merge different timers, such as `Stopwatch`, and possibly future ones, such as `DiscreteStopwatch` and `DiscreteTimer` from bevyengine#2683, into one struct.

Signed-off-by: Lena Milizé <me@lvmn.org>

# Objective

Fixes bevyengine#2926.

## Solution

Introduce `TimerMode` which replaces the `bool` argument of `Timer` constructors. A `Default` value for `TimerMode` is `Once`.

---

## Changelog

### Added

- `TimerMode` enum, along with variants `TimerMode::Once` and `TimerMode::Repeating`

### Changed

- Replace `bool` argument of `Timer::new` and `Timer::from_seconds` with `TimerMode`
- Change `repeating: bool` field of `Timer` with `mode: TimerMode`

## Migration Guide

- Replace `Timer::new(duration, false)` with `Timer::new(duration, TimerMode::Once)`.
- Replace `Timer::new(duration, true)` with `Timer::new(duration, TimerMode::Repeating)`.
- Replace `Timer::from_seconds(seconds, false)` with `Timer::from_seconds(seconds, TimerMode::Once)`.
- Replace `Timer::from_seconds(seconds, true)` with `Timer::from_seconds(seconds, TimerMode::Repeating)`.
- Change `timer.repeating()` to `timer.mode() == TimerMode::Repeating`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-Core Common functionality for all bevy apps C-Feature A new feature, making something new possible S-Needs-Design This issue requires design work to think about how it would best be accomplished
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Native Tick Timers
6 participants