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

Add an example for doing movement in fixed timesteps #14223

Merged
merged 18 commits into from
Jul 9, 2024

Conversation

janhohenheim
Copy link
Member

@janhohenheim janhohenheim commented Jul 8, 2024

copy-pasted from my doc comment in the code

Objective

This example shows how to properly handle player input, advance a physics simulation in a fixed timestep, and display the results.

The classic source for how and why this is done is Glenn Fiedler's article Fix Your Timestep!.

Motivation

The naive way of moving a player is to just update their position like so:

transform.translation += velocity;

The issue here is that the player's movement speed will be tied to the frame rate.
Faster machines will move the player faster, and slower machines will move the player slower.
In fact, you can observe this today when running some old games that did it this way on modern hardware!
The player will move at a breakneck pace.

The more sophisticated way is to update the player's position based on the time that has passed:

transform.translation += velocity * time.delta_seconds();

This way, velocity represents a speed in units per second, and the player will move at the same speed regardless of the frame rate.

However, this can still be problematic if the frame rate is very low or very high. If the frame rate is very low, the player will move in large jumps. This may lead to a player moving in such large jumps that they pass through walls or other obstacles. In general, you cannot expect a physics simulation to behave nicely with any delta time. Ideally, we want to have some stability in what kinds of delta times we feed into our physics simulation.

The solution is using a fixed timestep. This means that we advance the physics simulation by a fixed amount at a time. If the real time that passed between two frames is less than the fixed timestep, we simply don't advance the physics simulation at all.
If it is more, we advance the physics simulation multiple times until we catch up. You can read more about how Bevy implements this in the documentation for bevy::time::Fixed.

This leaves us with a last problem, however. If our physics simulation may advance zero or multiple times per frame, there may be frames in which the player's position did not need to be updated at all, and some where it is updated by a large amount that resulted from running the physics simulation multiple times. This is physically correct, but visually jarring. Imagine a player moving in a straight line, but depending on the frame rate, they may sometimes advance by a large amount and sometimes not at all. Visually, we want the player to move smoothly. This is why we need to separate the player's position in the physics simulation from the player's position in the visual representation. The visual representation can then be interpolated smoothly based on the last and current actual player position in the physics simulation.

This is a tradeoff: every visual frame is now slightly lagging behind the actual physical frame, but in return, the player's movement will appear smooth. There are other ways to compute the visual representation of the player, such as extrapolation. See the documentation of the lightyear crate for a nice overview of the different methods and their tradeoffs.

Implementation

  • The player's velocity is stored in a Velocity component. This is the speed in units per second.
  • The player's current position in the physics simulation is stored in a PhysicalTranslation component.
  • The player's previous position in the physics simulation is stored in a PreviousPhysicalTranslation component.
  • The player's visual representation is stored in Bevy's regular Transform component.
  • Every frame, we go through the following steps:
    • Advance the physics simulation by one fixed timestep in the advance_physics system.
      This is run in the FixedUpdate schedule, which runs before the Update schedule.
    • Update the player's visual representation in the update_displayed_transform system.
      This interpolates between the player's previous and current position in the physics simulation.
    • Update the player's velocity based on the player's input in the handle_input system.

Relevant Issues

Related to #1259.
I'm also fairly sure I've seen an issue somewhere made by @alice-i-cecile about showing how to move a character correctly in a fixed timestep, but I cannot find it.

@janhohenheim janhohenheim added C-Examples An addition or correction to our examples D-Modest A "normal" level of difficulty; suitable for simple features or challenging fixes S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Jul 8, 2024
@janhohenheim janhohenheim changed the title Add an example for how to do movement in fixed timesteps Add an example for doing movement in fixed timesteps Jul 8, 2024
@alice-i-cecile
Copy link
Member

As a follow-up (or in this PR), we should stop using fixed timestep in the breakout example, and use delta time instead.

@janhohenheim
Copy link
Member Author

janhohenheim commented Jul 8, 2024

@alice-i-cecile I'll leave that for a follow-up if that's alright :)
Also, what are you doing looking at work stuff‽ Go relax, you've earned it!

@janhohenheim janhohenheim requested a review from MiniaczQ July 8, 2024 17:37
Copy link
Contributor

@cBournhonesque cBournhonesque left a comment

Choose a reason for hiding this comment

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

I think this example is fine to be merged as it currently is, but it would be nice to see some future improvements:

  • maybe have a toggle where we can turn on the interpolation and turn it off to see the difference (or even a split-screen view)
  • maybe have an option to set or update the FixedTimeStep (so that we can see this helps the visuals in lower timestep than framerate, and higher timestep than framerate)
  • in general i've noticed that this kind of interpolation becomes extremely important when the camera is set on the moving entity. In those cases the issue becomes every obvious because suddenly everything else (other entities) seems to be jittering. I might try to add this in a future PR. I've seen tons of questions on discord related to jittery cameras that could boil down to this

@cBournhonesque
Copy link
Contributor

As a follow-up (or in this PR), we should stop using fixed timestep in the breakout example, and use delta time instead.

Why would we want to switch to delta-time? To be able to do 'partial-timesteps' using delta_time * overstep_fraction? Or is it for clarity?

@janhohenheim
Copy link
Member Author

janhohenheim commented Jul 8, 2024

I generally agree with your suggestions and have also thought about that. Here are my thoughts:

  • I decided to not have a direct comparison in here because I felt it would bloat the already fairly large example code-wise. But yeah, it would certainly help with the learning effect.
  • Changing the fixed update would be a nice and simple follow-up, agreed
  • I have played with the idea of merging this example with camera/2d_top_down_camera.rs to paint a more complete picture. I agree that such a bigger example would be a good thing to have laying around.

All of this is follow-up work.

@inodentry
Copy link
Contributor

Looks fine to me. From a quick read through the PR, I didn't see anything wrong with it.

I don't want to give an opinionated review (about how the concept should be taught), because I like it when different people teach the same concepts in their own way. I have already taught this topic in Bevy Cheatbook the way I like to teach it (incl. with a discussion and comparison of interpolation vs. extrapolation).

So I limit my review to checking for correctness. And correct it seems to be :)

@janhohenheim janhohenheim added S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it and removed S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Jul 8, 2024
@alice-i-cecile alice-i-cecile modified the milestone: 0.15 Jul 8, 2024
Copy link
Contributor

@MiniaczQ MiniaczQ left a comment

Choose a reason for hiding this comment

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

lgtm

@alice-i-cecile alice-i-cecile added this pull request to the merge queue Jul 9, 2024
Merged via the queue into bevyengine:main with commit d0e606b Jul 9, 2024
32 checks passed
@janhohenheim janhohenheim deleted the fixed-time-movement branch July 9, 2024 15:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
C-Examples An addition or correction to our examples D-Modest A "normal" level of difficulty; suitable for simple features or challenging fixes S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants