From d0e606b87cc9ce68d16b9c31b852b5e7a02adaf3 Mon Sep 17 00:00:00 2001 From: Jan Hohenheim Date: Tue, 9 Jul 2024 16:23:10 +0200 Subject: [PATCH] Add an example for doing movement in fixed timesteps (#14223) _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!](https://gafferongames.com/post/fix_your_timestep/). ## Motivation The naive way of moving a player is to just update their position like so: ```rust 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: ```rust 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`](https://docs.rs/bevy/latest/bevy/time/struct.Fixed.html). 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](https://cbournhonesque.github.io/lightyear/book/concepts/advanced_replication/visual_interpolation.html) 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. --- Cargo.toml | 11 + examples/README.md | 7 + examples/camera/2d_top_down_camera.rs | 12 +- .../movement/physics_in_fixed_timestep.rs | 213 ++++++++++++++++++ 4 files changed, 239 insertions(+), 4 deletions(-) create mode 100644 examples/movement/physics_in_fixed_timestep.rs diff --git a/Cargo.toml b/Cargo.toml index b3c056a763..f6a30b4892 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3258,6 +3258,17 @@ description = "Demonstrates how to enqueue custom draw commands in a render phas category = "Shaders" wasm = true +[[example]] +name = "physics_in_fixed_timestep" +path = "examples/movement/physics_in_fixed_timestep.rs" +doc-scrape-examples = true + +[package.metadata.example.physics_in_fixed_timestep] +name = "Run physics in a fixed timestep" +description = "Handles input, physics, and rendering in an industry-standard way by using a fixed timestep" +category = "Movement" +wasm = true + [profile.wasm-release] inherits = "release" opt-level = "z" diff --git a/examples/README.md b/examples/README.md index 55e148e236..d48f614787 100644 --- a/examples/README.md +++ b/examples/README.md @@ -53,6 +53,7 @@ git checkout v0.4.0 - [Gizmos](#gizmos) - [Input](#input) - [Math](#math) + - [Movement](#movement) - [Reflection](#reflection) - [Scene](#scene) - [Shaders](#shaders) @@ -337,6 +338,12 @@ Example | Description [Sampling Primitives](../examples/math/sampling_primitives.rs) | Demonstrates all the primitives which can be sampled. [Smooth Follow](../examples/math/smooth_follow.rs) | Demonstrates how to make an entity smoothly follow another using interpolation +## Movement + +Example | Description +--- | --- +[Run physics in a fixed timestep](../examples/movement/physics_in_fixed_timestep.rs) | Handles input, physics, and rendering in an industry-standard way by using a fixed timestep + ## Reflection Example | Description diff --git a/examples/camera/2d_top_down_camera.rs b/examples/camera/2d_top_down_camera.rs index ad565b36f5..9dde1463ba 100644 --- a/examples/camera/2d_top_down_camera.rs +++ b/examples/camera/2d_top_down_camera.rs @@ -4,9 +4,9 @@ //! //! | Key Binding | Action | //! |:---------------------|:--------------| -//! | `Z`(azerty), `W`(US) | Move forward | -//! | `S` | Move backward | -//! | `Q`(azerty), `A`(US) | Move left | +//! | `W` | Move up | +//! | `S` | Move down | +//! | `A` | Move left | //! | `D` | Move right | use bevy::core_pipeline::bloom::BloomSettings; @@ -61,7 +61,7 @@ fn setup_scene( fn setup_instructions(mut commands: Commands) { commands.spawn( TextBundle::from_section( - "Move the light with ZQSD or WASD.\nThe camera will smoothly track the light.", + "Move the light with WASD.\nThe camera will smoothly track the light.", TextStyle::default(), ) .with_style(Style { @@ -111,6 +111,10 @@ fn update_camera( } /// Update the player position with keyboard inputs. +/// Note that the approach used here is for demonstration purposes only, +/// as the point of this example is to showcase the camera tracking feature. +/// +/// A more robust solution for player movement can be found in `examples/movement/physics_in_fixed_timestep.rs`. fn move_player( mut player: Query<&mut Transform, With>, time: Res