Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/examples/movement/physics_in_fixed_timestep.rs
6595 views
1
//! This example shows how to properly handle player input,
2
//! advance a physics simulation in a fixed timestep, and display the results.
3
//!
4
//! The classic source for how and why this is done is Glenn Fiedler's article
5
//! [Fix Your Timestep!](https://gafferongames.com/post/fix_your_timestep/).
6
//! For a more Bevy-centric source, see
7
//! [this cheatbook entry](https://bevy-cheatbook.github.io/fundamentals/fixed-timestep.html).
8
//!
9
//! ## Motivation
10
//!
11
//! The naive way of moving a player is to just update their position like so:
12
//! ```no_run
13
//! transform.translation += velocity;
14
//! ```
15
//! The issue here is that the player's movement speed will be tied to the frame rate.
16
//! Faster machines will move the player faster, and slower machines will move the player slower.
17
//! In fact, you can observe this today when running some old games that did it this way on modern hardware!
18
//! The player will move at a breakneck pace.
19
//!
20
//! The more sophisticated way is to update the player's position based on the time that has passed:
21
//! ```no_run
22
//! transform.translation += velocity * time.delta_secs();
23
//! ```
24
//! This way, velocity represents a speed in units per second, and the player will move at the same speed
25
//! regardless of the frame rate.
26
//!
27
//! However, this can still be problematic if the frame rate is very low or very high.
28
//! If the frame rate is very low, the player will move in large jumps. This may lead to
29
//! a player moving in such large jumps that they pass through walls or other obstacles.
30
//! In general, you cannot expect a physics simulation to behave nicely with *any* delta time.
31
//! Ideally, we want to have some stability in what kinds of delta times we feed into our physics simulation.
32
//!
33
//! The solution is using a fixed timestep. This means that we advance the physics simulation by a fixed amount
34
//! at a time. If the real time that passed between two frames is less than the fixed timestep, we simply
35
//! don't advance the physics simulation at all.
36
//! If it is more, we advance the physics simulation multiple times until we catch up.
37
//! You can read more about how Bevy implements this in the documentation for
38
//! [`bevy::time::Fixed`](https://docs.rs/bevy/latest/bevy/time/struct.Fixed.html).
39
//!
40
//! This leaves us with a last problem, however. If our physics simulation may advance zero or multiple times
41
//! per frame, there may be frames in which the player's position did not need to be updated at all,
42
//! and some where it is updated by a large amount that resulted from running the physics simulation multiple times.
43
//! This is physically correct, but visually jarring. Imagine a player moving in a straight line, but depending on the frame rate,
44
//! they may sometimes advance by a large amount and sometimes not at all. Visually, we want the player to move smoothly.
45
//! This is why we need to separate the player's position in the physics simulation from the player's position in the visual representation.
46
//! The visual representation can then be interpolated smoothly based on the previous and current actual player position in the physics simulation.
47
//!
48
//! This is a tradeoff: every visual frame is now slightly lagging behind the actual physical frame,
49
//! but in return, the player's movement will appear smooth.
50
//! There are other ways to compute the visual representation of the player, such as extrapolation.
51
//! See the [documentation of the lightyear crate](https://cbournhonesque.github.io/lightyear/book/concepts/advanced_replication/visual_interpolation.html)
52
//! for a nice overview of the different methods and their respective tradeoffs.
53
//!
54
//! If we decide to use a fixed timestep, our game logic should mostly go in the `FixedUpdate` schedule.
55
//! One notable exception is the camera. Cameras should update as often as possible, or the player will very quickly
56
//! notice choppy movement if it's only updated at the same rate as the physics simulation. So, we use a variable timestep for the camera,
57
//! updating its transform every frame. The question now is which schedule to use. That depends on whether the camera data is required
58
//! for the physics simulation to run or not.
59
//! For example, in 3D games, the camera rotation often determines which direction the player moves when pressing "W",
60
//! so we need to rotate the camera *before* the fixed timestep. In contrast, the translation of the camera depends on what the physics simulation
61
//! has calculated for the player's position. Therefore, we need to update the camera's translation *after* the fixed timestep. Fortunately,
62
//! we can get smooth movement by simply using the interpolated player translation for the camera as well.
63
//!
64
//! ## Implementation
65
//!
66
//! - The player's inputs since the last physics update are stored in the `AccumulatedInput` component.
67
//! - The player's velocity is stored in a `Velocity` component. This is the speed in units per second.
68
//! - The player's current position in the physics simulation is stored in a `PhysicalTranslation` component.
69
//! - The player's previous position in the physics simulation is stored in a `PreviousPhysicalTranslation` component.
70
//! - The player's visual representation is stored in Bevy's regular `Transform` component.
71
//! - Every frame, we go through the following steps:
72
//! - Accumulate the player's input and set the current speed in the `handle_input` system.
73
//! This is run in the `RunFixedMainLoop` schedule, ordered in `RunFixedMainLoopSystems::BeforeFixedMainLoop`,
74
//! which runs before the fixed timestep loop. This is run every frame.
75
//! - Rotate the camera based on the player's input. This is also run in `RunFixedMainLoopSystems::BeforeFixedMainLoop`.
76
//! - Advance the physics simulation by one fixed timestep in the `advance_physics` system.
77
//! Accumulated input is consumed here.
78
//! This is run in the `FixedUpdate` schedule, which runs zero or multiple times per frame.
79
//! - Update the player's visual representation in the `interpolate_rendered_transform` system.
80
//! This interpolates between the player's previous and current position in the physics simulation.
81
//! It is run in the `RunFixedMainLoop` schedule, ordered in `RunFixedMainLoopSystems::AfterFixedMainLoop`,
82
//! which runs after the fixed timestep loop. This is run every frame.
83
//! - Update the camera's translation to the player's interpolated translation. This is also run in `RunFixedMainLoopSystems::AfterFixedMainLoop`.
84
//!
85
//!
86
//! ## Controls
87
//!
88
//! | Key Binding | Action |
89
//! |:---------------------|:--------------|
90
//! | `W` | Move up |
91
//! | `S` | Move down |
92
//! | `A` | Move left |
93
//! | `D` | Move right |
94
//! | Mouse | Rotate camera |
95
96
use std::f32::consts::FRAC_PI_2;
97
98
use bevy::{color::palettes::tailwind, input::mouse::AccumulatedMouseMotion, prelude::*};
99
100
fn main() {
101
App::new()
102
.add_plugins(DefaultPlugins)
103
.init_resource::<DidFixedTimestepRunThisFrame>()
104
.add_systems(Startup, (spawn_text, spawn_player, spawn_environment))
105
// At the beginning of each frame, clear the flag that indicates whether the fixed timestep has run this frame.
106
.add_systems(PreUpdate, clear_fixed_timestep_flag)
107
// At the beginning of each fixed timestep, set the flag that indicates whether the fixed timestep has run this frame.
108
.add_systems(FixedPreUpdate, set_fixed_time_step_flag)
109
// Advance the physics simulation using a fixed timestep.
110
.add_systems(FixedUpdate, advance_physics)
111
.add_systems(
112
// The `RunFixedMainLoop` schedule allows us to schedule systems to run before and after the fixed timestep loop.
113
RunFixedMainLoop,
114
(
115
(
116
// The camera needs to be rotated before the physics simulation is advanced in before the fixed timestep loop,
117
// so that the physics simulation can use the current rotation.
118
// Note that if we ran it in `Update`, it would be too late, as the physics simulation would already have been advanced.
119
// If we ran this in `FixedUpdate`, it would sometimes not register player input, as that schedule may run zero times per frame.
120
rotate_camera,
121
// Accumulate our input before the fixed timestep loop to tell the physics simulation what it should do during the fixed timestep.
122
accumulate_input,
123
)
124
.chain()
125
.in_set(RunFixedMainLoopSystems::BeforeFixedMainLoop),
126
(
127
// Clear our accumulated input after it was processed during the fixed timestep.
128
// By clearing the input *after* the fixed timestep, we can still use `AccumulatedInput` inside `FixedUpdate` if we need it.
129
clear_input.run_if(did_fixed_timestep_run_this_frame),
130
// The player's visual representation needs to be updated after the physics simulation has been advanced.
131
// This could be run in `Update`, but if we run it here instead, the systems in `Update`
132
// will be working with the `Transform` that will actually be shown on screen.
133
interpolate_rendered_transform,
134
// The camera can then use the interpolated transform to position itself correctly.
135
translate_camera,
136
)
137
.chain()
138
.in_set(RunFixedMainLoopSystems::AfterFixedMainLoop),
139
),
140
)
141
.run();
142
}
143
144
/// A vector representing the player's input, accumulated over all frames that ran
145
/// since the last time the physics simulation was advanced.
146
#[derive(Debug, Component, Clone, Copy, PartialEq, Default, Deref, DerefMut)]
147
struct AccumulatedInput {
148
// The player's movement input (WASD).
149
movement: Vec2,
150
// Other input that could make sense would be e.g.
151
// boost: bool
152
}
153
154
/// A vector representing the player's velocity in the physics simulation.
155
#[derive(Debug, Component, Clone, Copy, PartialEq, Default, Deref, DerefMut)]
156
struct Velocity(Vec3);
157
158
/// The actual position of the player in the physics simulation.
159
/// This is separate from the `Transform`, which is merely a visual representation.
160
///
161
/// If you want to make sure that this component is always initialized
162
/// with the same value as the `Transform`'s translation, you can
163
/// use a [component lifecycle hook](https://docs.rs/bevy/0.14.0/bevy/ecs/component/struct.ComponentHooks.html)
164
#[derive(Debug, Component, Clone, Copy, PartialEq, Default, Deref, DerefMut)]
165
struct PhysicalTranslation(Vec3);
166
167
/// The value [`PhysicalTranslation`] had in the last fixed timestep.
168
/// Used for interpolation in the `interpolate_rendered_transform` system.
169
#[derive(Debug, Component, Clone, Copy, PartialEq, Default, Deref, DerefMut)]
170
struct PreviousPhysicalTranslation(Vec3);
171
172
/// Spawn the player and a 3D camera. We could also spawn the camera as a child of the player,
173
/// but in practice, they are usually spawned separately so that the player's rotation does not
174
/// influence the camera's rotation.
175
fn spawn_player(mut commands: Commands) {
176
commands.spawn((Camera3d::default(), CameraSensitivity::default()));
177
commands.spawn((
178
Name::new("Player"),
179
Transform::from_scale(Vec3::splat(0.3)),
180
AccumulatedInput::default(),
181
Velocity::default(),
182
PhysicalTranslation::default(),
183
PreviousPhysicalTranslation::default(),
184
));
185
}
186
187
/// Spawn a field of floating spheres to fly around in
188
fn spawn_environment(
189
mut commands: Commands,
190
mut meshes: ResMut<Assets<Mesh>>,
191
mut materials: ResMut<Assets<StandardMaterial>>,
192
) {
193
let sphere_material = materials.add(Color::from(tailwind::SKY_200));
194
let sphere_mesh = meshes.add(Sphere::new(0.3));
195
let spheres_in_x = 6;
196
let spheres_in_y = 4;
197
let spheres_in_z = 10;
198
let distance = 3.0;
199
for x in 0..spheres_in_x {
200
for y in 0..spheres_in_y {
201
for z in 0..spheres_in_z {
202
let translation = Vec3::new(
203
x as f32 * distance - (spheres_in_x as f32 - 1.0) * distance / 2.0,
204
y as f32 * distance - (spheres_in_y as f32 - 1.0) * distance / 2.0,
205
z as f32 * distance - (spheres_in_z as f32 - 1.0) * distance / 2.0,
206
);
207
commands.spawn((
208
Name::new("Sphere"),
209
Transform::from_translation(translation),
210
Mesh3d(sphere_mesh.clone()),
211
MeshMaterial3d(sphere_material.clone()),
212
));
213
}
214
}
215
}
216
217
commands.spawn((
218
DirectionalLight::default(),
219
Transform::default().looking_to(Vec3::new(-1.0, -3.0, 0.5), Vec3::Y),
220
));
221
}
222
223
/// Spawn a bit of UI text to explain how to move the player.
224
fn spawn_text(mut commands: Commands) {
225
let font = TextFont {
226
font_size: 25.0,
227
..default()
228
};
229
commands.spawn((
230
Node {
231
position_type: PositionType::Absolute,
232
bottom: px(12),
233
left: px(12),
234
flex_direction: FlexDirection::Column,
235
..default()
236
},
237
children![
238
(Text::new("Move the player with WASD"), font.clone()),
239
(Text::new("Rotate the camera with the mouse"), font)
240
],
241
));
242
}
243
244
fn rotate_camera(
245
accumulated_mouse_motion: Res<AccumulatedMouseMotion>,
246
player: Single<(&mut Transform, &CameraSensitivity), With<Camera>>,
247
) {
248
let (mut transform, camera_sensitivity) = player.into_inner();
249
250
let delta = accumulated_mouse_motion.delta;
251
252
if delta != Vec2::ZERO {
253
// Note that we are not multiplying by delta time here.
254
// The reason is that for mouse movement, we already get the full movement that happened since the last frame.
255
// This means that if we multiply by delta time, we will get a smaller rotation than intended by the user.
256
let delta_yaw = -delta.x * camera_sensitivity.x;
257
let delta_pitch = -delta.y * camera_sensitivity.y;
258
259
let (yaw, pitch, roll) = transform.rotation.to_euler(EulerRot::YXZ);
260
let yaw = yaw + delta_yaw;
261
262
// If the pitch was ±¹⁄₂ π, the camera would look straight up or down.
263
// When the user wants to move the camera back to the horizon, which way should the camera face?
264
// The camera has no way of knowing what direction was "forward" before landing in that extreme position,
265
// so the direction picked will for all intents and purposes be arbitrary.
266
// Another issue is that for mathematical reasons, the yaw will effectively be flipped when the pitch is at the extremes.
267
// To not run into these issues, we clamp the pitch to a safe range.
268
const PITCH_LIMIT: f32 = FRAC_PI_2 - 0.01;
269
let pitch = (pitch + delta_pitch).clamp(-PITCH_LIMIT, PITCH_LIMIT);
270
271
transform.rotation = Quat::from_euler(EulerRot::YXZ, yaw, pitch, roll);
272
}
273
}
274
275
#[derive(Debug, Component, Deref, DerefMut)]
276
struct CameraSensitivity(Vec2);
277
278
impl Default for CameraSensitivity {
279
fn default() -> Self {
280
Self(
281
// These factors are just arbitrary mouse sensitivity values.
282
// It's often nicer to have a faster horizontal sensitivity than vertical.
283
// We use a component for them so that we can make them user-configurable at runtime
284
// for accessibility reasons.
285
// It also allows you to inspect them in an editor if you `Reflect` the component.
286
Vec2::new(0.003, 0.002),
287
)
288
}
289
}
290
291
/// Handle keyboard input and accumulate it in the `AccumulatedInput` component.
292
///
293
/// There are many strategies for how to handle all the input that happened since the last fixed timestep.
294
/// This is a very simple one: we just use the last available input.
295
/// That strategy works fine for us since the user continuously presses the input keys in this example.
296
/// If we had some kind of instantaneous action like activating a boost ability, we would need to remember that that input
297
/// was pressed at some point since the last fixed timestep.
298
fn accumulate_input(
299
keyboard_input: Res<ButtonInput<KeyCode>>,
300
player: Single<(&mut AccumulatedInput, &mut Velocity)>,
301
camera: Single<&Transform, With<Camera>>,
302
) {
303
/// Since Bevy's 3D renderer assumes SI units, this has the unit of meters per second.
304
/// Note that about 1.5 is the average walking speed of a human.
305
const SPEED: f32 = 4.0;
306
let (mut input, mut velocity) = player.into_inner();
307
// Reset the input to zero before reading the new input. As mentioned above, we can only do this
308
// because this is continuously pressed by the user. Do not reset e.g. whether the user wants to boost.
309
input.movement = Vec2::ZERO;
310
if keyboard_input.pressed(KeyCode::KeyW) {
311
input.movement.y += 1.0;
312
}
313
if keyboard_input.pressed(KeyCode::KeyS) {
314
input.movement.y -= 1.0;
315
}
316
if keyboard_input.pressed(KeyCode::KeyA) {
317
input.movement.x -= 1.0;
318
}
319
if keyboard_input.pressed(KeyCode::KeyD) {
320
input.movement.x += 1.0;
321
}
322
323
// Remap the 2D input to Bevy's 3D coordinate system.
324
// Pressing W makes `input.y` go up. Since Bevy assumes that -Z is forward, we make our new Z equal to -input.y
325
let input_3d = Vec3 {
326
x: input.movement.x,
327
y: 0.0,
328
z: -input.movement.y,
329
};
330
331
// Rotate the input so that forward is aligned with the camera's forward direction.
332
let rotated_input = camera.rotation * input_3d;
333
334
// We need to normalize and scale because otherwise
335
// diagonal movement would be faster than horizontal or vertical movement.
336
// We use `clamp_length_max` instead of `.normalize_or_zero()` because gamepad input
337
// may be smaller than 1.0 when the player is pushing the stick just a little bit.
338
velocity.0 = rotated_input.clamp_length_max(1.0) * SPEED;
339
}
340
341
/// A simple resource that tells us whether the fixed timestep ran this frame.
342
#[derive(Resource, Debug, Deref, DerefMut, Default)]
343
pub struct DidFixedTimestepRunThisFrame(bool);
344
345
/// Reset the flag at the start of every frame.
346
fn clear_fixed_timestep_flag(
347
mut did_fixed_timestep_run_this_frame: ResMut<DidFixedTimestepRunThisFrame>,
348
) {
349
did_fixed_timestep_run_this_frame.0 = false;
350
}
351
352
/// Set the flag during each fixed timestep.
353
fn set_fixed_time_step_flag(
354
mut did_fixed_timestep_run_this_frame: ResMut<DidFixedTimestepRunThisFrame>,
355
) {
356
did_fixed_timestep_run_this_frame.0 = true;
357
}
358
359
fn did_fixed_timestep_run_this_frame(
360
did_fixed_timestep_run_this_frame: Res<DidFixedTimestepRunThisFrame>,
361
) -> bool {
362
did_fixed_timestep_run_this_frame.0
363
}
364
365
// Clear the input after it was processed in the fixed timestep.
366
fn clear_input(mut input: Single<&mut AccumulatedInput>) {
367
**input = AccumulatedInput::default();
368
}
369
370
/// Advance the physics simulation by one fixed timestep. This may run zero or multiple times per frame.
371
///
372
/// Note that since this runs in `FixedUpdate`, `Res<Time>` would be `Res<Time<Fixed>>` automatically.
373
/// We are being explicit here for clarity.
374
fn advance_physics(
375
fixed_time: Res<Time<Fixed>>,
376
mut query: Query<(
377
&mut PhysicalTranslation,
378
&mut PreviousPhysicalTranslation,
379
&Velocity,
380
)>,
381
) {
382
for (mut current_physical_translation, mut previous_physical_translation, velocity) in
383
query.iter_mut()
384
{
385
previous_physical_translation.0 = current_physical_translation.0;
386
current_physical_translation.0 += velocity.0 * fixed_time.delta_secs();
387
}
388
}
389
390
fn interpolate_rendered_transform(
391
fixed_time: Res<Time<Fixed>>,
392
mut query: Query<(
393
&mut Transform,
394
&PhysicalTranslation,
395
&PreviousPhysicalTranslation,
396
)>,
397
) {
398
for (mut transform, current_physical_translation, previous_physical_translation) in
399
query.iter_mut()
400
{
401
let previous = previous_physical_translation.0;
402
let current = current_physical_translation.0;
403
// The overstep fraction is a value between 0 and 1 that tells us how far we are between two fixed timesteps.
404
let alpha = fixed_time.overstep_fraction();
405
406
let rendered_translation = previous.lerp(current, alpha);
407
transform.translation = rendered_translation;
408
}
409
}
410
411
// Sync the camera's position with the player's interpolated position
412
fn translate_camera(
413
mut camera: Single<&mut Transform, With<Camera>>,
414
player: Single<&Transform, (With<AccumulatedInput>, Without<Camera>)>,
415
) {
416
camera.translation = player.translation;
417
}
418
419