Skip to content

Commit c92ee31

Browse files
janhohenheimbash
andauthored
Allow ordering variable timesteps around fixed timesteps (#14881)
# Objective - Fixes #14873, see that issue for a whole lot of context ## Solution - Add a blessed system set for this stuff. See [this Discord discussion](https://discord.com/channels/691052431525675048/749335865876021248/1276262931327094908). Note that the gizmo systems, [LWIM](https://github.com/Leafwing-Studios/leafwing-input-manager/pull/522/files#diff-9b59ee4899ad0a5d008889ea89a124a7291316532e42f9f3d6ae842b906fb095R154) and now a new plugin I'm working on are all already ordering against `run_fixed_main_schedule`, so having a dedicated system set should be more robust and hopefully also more discoverable. --- ## ~~Showcase~~ ~~I can add a little video of a smooth camera later if this gets merged :)~~ Apparently a release note is not needed, so I'll leave it out. See the changes in the fixed timestep example for usage showcase and the video in #14873 for a more or less accurate video of the effect (it does not use the same solution though, so it is not quite the same) ## Migration Guide [run_fixed_main_schedule](https://docs.rs/bevy/latest/bevy/time/fn.run_fixed_main_schedule.html) is no longer public. If you used to order against it, use the new dedicated `RunFixedMainLoopSystem` system set instead. You can replace your usage of `run_fixed_main_schedule` one for one by `RunFixedMainLoopSystem::FixedMainLoop`, but it is now more idiomatic to place your systems in either `RunFixedMainLoopSystem::BeforeFixedMainLoop` or `RunFixedMainLoopSystem::AfterFixedMainLoop` Old: ```rust app.add_systems( RunFixedMainLoop, some_system.before(run_fixed_main_schedule) ); ``` New: ```rust app.add_systems( RunFixedMainLoop, some_system.in_set(RunFixedMainLoopSystem::BeforeFixedMainLoop) ); ``` --------- Co-authored-by: Tau Gärtli <[email protected]>
1 parent f1f07be commit c92ee31

File tree

6 files changed

+185
-30
lines changed

6 files changed

+185
-30
lines changed

crates/bevy_app/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ pub mod prelude {
3434
app::{App, AppExit},
3535
main_schedule::{
3636
First, FixedFirst, FixedLast, FixedPostUpdate, FixedPreUpdate, FixedUpdate, Last, Main,
37-
PostStartup, PostUpdate, PreStartup, PreUpdate, SpawnScene, Startup, Update,
37+
PostStartup, PostUpdate, PreStartup, PreUpdate, RunFixedMainLoop,
38+
RunFixedMainLoopSystem, SpawnScene, Startup, Update,
3839
},
3940
sub_app::SubApp,
4041
Plugin, PluginGroup,

crates/bevy_app/src/main_schedule.rs

Lines changed: 114 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
use crate::{App, Plugin};
22
use bevy_ecs::{
3-
schedule::{ExecutorKind, InternedScheduleLabel, Schedule, ScheduleLabel},
3+
schedule::{
4+
ExecutorKind, InternedScheduleLabel, IntoSystemSetConfigs, Schedule, ScheduleLabel,
5+
SystemSet,
6+
},
47
system::{Local, Resource},
58
world::{Mut, World},
69
};
@@ -75,6 +78,11 @@ pub struct First;
7578
pub struct PreUpdate;
7679

7780
/// Runs the [`FixedMain`] schedule in a loop according until all relevant elapsed time has been "consumed".
81+
/// If you need to order your variable timestep systems
82+
/// before or after the fixed update logic, use the [`RunFixedMainLoopSystem`] system set.
83+
///
84+
/// Note that in contrast to most other Bevy schedules, systems added directly to
85+
/// [`RunFixedMainLoop`] will *not* be parallelized between each other.
7886
///
7987
/// See the [`Main`] schedule for some details about how schedules are run.
8088
#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)]
@@ -126,8 +134,8 @@ pub struct FixedLast;
126134

127135
/// The schedule that contains systems which only run after a fixed period of time has elapsed.
128136
///
129-
/// The exclusive `run_fixed_main_schedule` system runs this schedule.
130-
/// This is run by the [`RunFixedMainLoop`] schedule.
137+
/// This is run by the [`RunFixedMainLoop`] schedule. If you need to order your variable timestep systems
138+
/// before or after the fixed update logic, use the [`RunFixedMainLoopSystem`] system set.
131139
///
132140
/// Frequency of execution is configured by inserting `Time<Fixed>` resource, 64 Hz by default.
133141
/// See [this example](https://github.com/bevyengine/bevy/blob/latest/examples/time/time.rs).
@@ -288,7 +296,16 @@ impl Plugin for MainSchedulePlugin {
288296
.init_resource::<MainScheduleOrder>()
289297
.init_resource::<FixedMainScheduleOrder>()
290298
.add_systems(Main, Main::run_main)
291-
.add_systems(FixedMain, FixedMain::run_fixed_main);
299+
.add_systems(FixedMain, FixedMain::run_fixed_main)
300+
.configure_sets(
301+
RunFixedMainLoop,
302+
(
303+
RunFixedMainLoopSystem::BeforeFixedMainLoop,
304+
RunFixedMainLoopSystem::FixedMainLoop,
305+
RunFixedMainLoopSystem::AfterFixedMainLoop,
306+
)
307+
.chain(),
308+
);
292309

293310
#[cfg(feature = "bevy_debug_stepping")]
294311
{
@@ -352,3 +369,96 @@ impl FixedMain {
352369
});
353370
}
354371
}
372+
373+
/// Set enum for the systems that want to run inside [`RunFixedMainLoop`],
374+
/// but before or after the fixed update logic. Systems in this set
375+
/// will run exactly once per frame, regardless of the number of fixed updates.
376+
/// They will also run under a variable timestep.
377+
///
378+
/// This is useful for handling things that need to run every frame, but
379+
/// also need to be read by the fixed update logic. See the individual variants
380+
/// for examples of what kind of systems should be placed in each.
381+
///
382+
/// Note that in contrast to most other Bevy schedules, systems added directly to
383+
/// [`RunFixedMainLoop`] will *not* be parallelized between each other.
384+
#[derive(Debug, Hash, PartialEq, Eq, Copy, Clone, SystemSet)]
385+
pub enum RunFixedMainLoopSystem {
386+
/// Runs before the fixed update logic.
387+
///
388+
/// A good example of a system that fits here
389+
/// is camera movement, which needs to be updated in a variable timestep,
390+
/// as you want the camera to move with as much precision and updates as
391+
/// the frame rate allows. A physics system that needs to read the camera
392+
/// position and orientation, however, should run in the fixed update logic,
393+
/// as it needs to be deterministic and run at a fixed rate for better stability.
394+
/// Note that we are not placing the camera movement system in `Update`, as that
395+
/// would mean that the physics system already ran at that point.
396+
///
397+
/// # Example
398+
/// ```
399+
/// # use bevy_app::prelude::*;
400+
/// # use bevy_ecs::prelude::*;
401+
/// App::new()
402+
/// .add_systems(
403+
/// RunFixedMainLoop,
404+
/// update_camera_rotation.in_set(RunFixedMainLoopSystem::BeforeFixedMainLoop))
405+
/// .add_systems(FixedUpdate, update_physics);
406+
///
407+
/// # fn update_camera_rotation() {}
408+
/// # fn update_physics() {}
409+
/// ```
410+
BeforeFixedMainLoop,
411+
/// Contains the fixed update logic.
412+
/// Runs [`FixedMain`] zero or more times based on delta of
413+
/// [`Time<Virtual>`] and [`Time::overstep`].
414+
///
415+
/// Don't place systems here, use [`FixedUpdate`] and friends instead.
416+
/// Use this system instead to order your systems to run specifically inbetween the fixed update logic and all
417+
/// other systems that run in [`RunFixedMainLoopSystem::BeforeFixedMainLoop`] or [`RunFixedMainLoopSystem::AfterFixedMainLoop`].
418+
///
419+
/// [`Time<Virtual>`]: https://docs.rs/bevy/latest/bevy/prelude/struct.Virtual.html
420+
/// [`Time::overstep`]: https://docs.rs/bevy/latest/bevy/time/struct.Time.html#method.overstep
421+
/// # Example
422+
/// ```
423+
/// # use bevy_app::prelude::*;
424+
/// # use bevy_ecs::prelude::*;
425+
/// App::new()
426+
/// .add_systems(FixedUpdate, update_physics)
427+
/// .add_systems(
428+
/// RunFixedMainLoop,
429+
/// (
430+
/// // This system will be called before all interpolation systems
431+
/// // that third-party plugins might add.
432+
/// prepare_for_interpolation
433+
/// .after(RunFixedMainLoopSystem::FixedMainLoop)
434+
/// .before(RunFixedMainLoopSystem::AfterFixedMainLoop),
435+
/// )
436+
/// );
437+
///
438+
/// # fn prepare_for_interpolation() {}
439+
/// # fn update_physics() {}
440+
/// ```
441+
FixedMainLoop,
442+
/// Runs after the fixed update logic.
443+
///
444+
/// A good example of a system that fits here
445+
/// is a system that interpolates the transform of an entity between the last and current fixed update.
446+
/// See the [fixed timestep example] for more details.
447+
///
448+
/// [fixed timestep example]: https://github.com/bevyengine/bevy/blob/main/examples/movement/physics_in_fixed_timestep.rs
449+
///
450+
/// # Example
451+
/// ```
452+
/// # use bevy_app::prelude::*;
453+
/// # use bevy_ecs::prelude::*;
454+
/// App::new()
455+
/// .add_systems(FixedUpdate, update_physics)
456+
/// .add_systems(
457+
/// RunFixedMainLoop,
458+
/// interpolate_transforms.in_set(RunFixedMainLoopSystem::AfterFixedMainLoop));
459+
///
460+
/// # fn interpolate_transforms() {}
461+
/// # fn update_physics() {}
462+
/// ```
463+
AfterFixedMainLoop,
464+
}

crates/bevy_gizmos/src/lib.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -246,13 +246,15 @@ impl AppGizmoBuilder for App {
246246
.init_resource::<GizmoStorage<Config, Swap<Fixed>>>()
247247
.add_systems(
248248
RunFixedMainLoop,
249-
start_gizmo_context::<Config, Fixed>.before(bevy_time::run_fixed_main_schedule),
249+
start_gizmo_context::<Config, Fixed>
250+
.in_set(bevy_app::RunFixedMainLoopSystem::BeforeFixedMainLoop),
250251
)
251252
.add_systems(FixedFirst, clear_gizmo_context::<Config, Fixed>)
252253
.add_systems(FixedLast, collect_requested_gizmos::<Config, Fixed>)
253254
.add_systems(
254255
RunFixedMainLoop,
255-
end_gizmo_context::<Config, Fixed>.after(bevy_time::run_fixed_main_schedule),
256+
end_gizmo_context::<Config, Fixed>
257+
.in_set(bevy_app::RunFixedMainLoopSystem::AfterFixedMainLoop),
256258
)
257259
.add_systems(
258260
Last,

crates/bevy_time/src/fixed.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -233,8 +233,10 @@ impl Default for Fixed {
233233
}
234234

235235
/// Runs [`FixedMain`] zero or more times based on delta of
236-
/// [`Time<Virtual>`](Virtual) and [`Time::overstep`]
237-
pub fn run_fixed_main_schedule(world: &mut World) {
236+
/// [`Time<Virtual>`](Virtual) and [`Time::overstep`].
237+
/// You can order your systems relative to this by using
238+
/// [`RunFixedMainLoopSystem`](bevy_app::prelude::RunFixedMainLoopSystem).
239+
pub(super) fn run_fixed_main_schedule(world: &mut World) {
238240
let delta = world.resource::<Time<Virtual>>().delta();
239241
world.resource_mut::<Time<Fixed>>().accumulate(delta);
240242

crates/bevy_time/src/lib.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,10 @@ impl Plugin for TimePlugin {
7070
.in_set(TimeSystem)
7171
.ambiguous_with(event_update_system),
7272
)
73-
.add_systems(RunFixedMainLoop, run_fixed_main_schedule);
73+
.add_systems(
74+
RunFixedMainLoop,
75+
run_fixed_main_schedule.in_set(RunFixedMainLoopSystem::FixedMainLoop),
76+
);
7477

7578
// Ensure the events are not dropped until `FixedMain` systems can observe them
7679
app.add_systems(FixedPostUpdate, signal_event_update_system);

examples/movement/physics_in_fixed_timestep.rs

Lines changed: 57 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -53,16 +53,22 @@
5353
//!
5454
//! ## Implementation
5555
//!
56+
//! - The player's inputs since the last physics update are stored in the `AccumulatedInput` component.
5657
//! - The player's velocity is stored in a `Velocity` component. This is the speed in units per second.
5758
//! - The player's current position in the physics simulation is stored in a `PhysicalTranslation` component.
5859
//! - The player's previous position in the physics simulation is stored in a `PreviousPhysicalTranslation` component.
5960
//! - The player's visual representation is stored in Bevy's regular `Transform` component.
6061
//! - Every frame, we go through the following steps:
62+
//! - Accumulate the player's input and set the current speed in the `handle_input` system.
63+
//! This is run in the `RunFixedMainLoop` schedule, ordered in `RunFixedMainLoopSystem::BeforeFixedMainLoop`,
64+
//! which runs before the fixed timestep loop. This is run every frame.
6165
//! - Advance the physics simulation by one fixed timestep in the `advance_physics` system.
62-
//! This is run in the `FixedUpdate` schedule, which runs before the `Update` schedule.
63-
//! - Update the player's visual representation in the `update_rendered_transform` system.
66+
//! Accumulated input is consumed here.
67+
//! This is run in the `FixedUpdate` schedule, which runs zero or multiple times per frame.
68+
//! - Update the player's visual representation in the `interpolate_rendered_transform` system.
6469
//! This interpolates between the player's previous and current position in the physics simulation.
65-
//! - Update the player's velocity based on the player's input in the `handle_input` system.
70+
//! It is run in the `RunFixedMainLoop` schedule, ordered in `RunFixedMainLoopSystem::AfterFixedMainLoop`,
71+
//! which runs after the fixed timestep loop. This is run every frame.
6672
//!
6773
//!
6874
//! ## Controls
@@ -80,13 +86,31 @@ fn main() {
8086
App::new()
8187
.add_plugins(DefaultPlugins)
8288
.add_systems(Startup, (spawn_text, spawn_player))
83-
// `FixedUpdate` runs before `Update`, so the physics simulation is advanced before the player's visual representation is updated.
89+
// Advance the physics simulation using a fixed timestep.
8490
.add_systems(FixedUpdate, advance_physics)
85-
.add_systems(Update, (update_rendered_transform, handle_input).chain())
91+
.add_systems(
92+
// The `RunFixedMainLoop` schedule allows us to schedule systems to run before and after the fixed timestep loop.
93+
RunFixedMainLoop,
94+
(
95+
// The physics simulation needs to know the player's input, so we run this before the fixed timestep loop.
96+
// Note that if we ran it in `Update`, it would be too late, as the physics simulation would already have been advanced.
97+
// If we ran this in `FixedUpdate`, it would sometimes not register player input, as that schedule may run zero times per frame.
98+
handle_input.in_set(RunFixedMainLoopSystem::BeforeFixedMainLoop),
99+
// The player's visual representation needs to be updated after the physics simulation has been advanced.
100+
// This could be run in `Update`, but if we run it here instead, the systems in `Update`
101+
// will be working with the `Transform` that will actually be shown on screen.
102+
interpolate_rendered_transform.in_set(RunFixedMainLoopSystem::AfterFixedMainLoop),
103+
),
104+
)
86105
.run();
87106
}
88107

89-
/// How many units per second the player should move.
108+
/// A vector representing the player's input, accumulated over all frames that ran
109+
/// since the last time the physics simulation was advanced.
110+
#[derive(Debug, Component, Clone, Copy, PartialEq, Default, Deref, DerefMut)]
111+
struct AccumulatedInput(Vec2);
112+
113+
/// A vector representing the player's velocity in the physics simulation.
90114
#[derive(Debug, Component, Clone, Copy, PartialEq, Default, Deref, DerefMut)]
91115
struct Velocity(Vec3);
92116

@@ -100,7 +124,7 @@ struct Velocity(Vec3);
100124
struct PhysicalTranslation(Vec3);
101125

102126
/// The value [`PhysicalTranslation`] had in the last fixed timestep.
103-
/// Used for interpolation in the `update_rendered_transform` system.
127+
/// Used for interpolation in the `interpolate_rendered_transform` system.
104128
#[derive(Debug, Component, Clone, Copy, PartialEq, Default, Deref, DerefMut)]
105129
struct PreviousPhysicalTranslation(Vec3);
106130

@@ -114,6 +138,7 @@ fn spawn_player(mut commands: Commands, asset_server: Res<AssetServer>) {
114138
transform: Transform::from_scale(Vec3::splat(0.3)),
115139
..default()
116140
},
141+
AccumulatedInput::default(),
117142
Velocity::default(),
118143
PhysicalTranslation::default(),
119144
PreviousPhysicalTranslation::default(),
@@ -143,31 +168,35 @@ fn spawn_text(mut commands: Commands) {
143168
});
144169
}
145170

146-
/// Handle keyboard input to move the player.
147-
fn handle_input(keyboard_input: Res<ButtonInput<KeyCode>>, mut query: Query<&mut Velocity>) {
171+
/// Handle keyboard input and accumulate it in the `AccumulatedInput` component.
172+
/// There are many strategies for how to handle all the input that happened since the last fixed timestep.
173+
/// This is a very simple one: we just accumulate the input and average it out by normalizing it.
174+
fn handle_input(
175+
keyboard_input: Res<ButtonInput<KeyCode>>,
176+
mut query: Query<(&mut AccumulatedInput, &mut Velocity)>,
177+
) {
148178
/// Since Bevy's default 2D camera setup is scaled such that
149179
/// one unit is one pixel, you can think of this as
150180
/// "How many pixels per second should the player move?"
151181
const SPEED: f32 = 210.0;
152-
for mut velocity in query.iter_mut() {
153-
velocity.0 = Vec3::ZERO;
154-
182+
for (mut input, mut velocity) in query.iter_mut() {
155183
if keyboard_input.pressed(KeyCode::KeyW) {
156-
velocity.y += 1.0;
184+
input.y += 1.0;
157185
}
158186
if keyboard_input.pressed(KeyCode::KeyS) {
159-
velocity.y -= 1.0;
187+
input.y -= 1.0;
160188
}
161189
if keyboard_input.pressed(KeyCode::KeyA) {
162-
velocity.x -= 1.0;
190+
input.x -= 1.0;
163191
}
164192
if keyboard_input.pressed(KeyCode::KeyD) {
165-
velocity.x += 1.0;
193+
input.x += 1.0;
166194
}
167195

168196
// Need to normalize and scale because otherwise
169197
// diagonal movement would be faster than horizontal or vertical movement.
170-
velocity.0 = velocity.normalize_or_zero() * SPEED;
198+
// This effectively averages the accumulated input.
199+
velocity.0 = input.extend(0.0).normalize_or_zero() * SPEED;
171200
}
172201
}
173202

@@ -180,18 +209,26 @@ fn advance_physics(
180209
mut query: Query<(
181210
&mut PhysicalTranslation,
182211
&mut PreviousPhysicalTranslation,
212+
&mut AccumulatedInput,
183213
&Velocity,
184214
)>,
185215
) {
186-
for (mut current_physical_translation, mut previous_physical_translation, velocity) in
187-
query.iter_mut()
216+
for (
217+
mut current_physical_translation,
218+
mut previous_physical_translation,
219+
mut input,
220+
velocity,
221+
) in query.iter_mut()
188222
{
189223
previous_physical_translation.0 = current_physical_translation.0;
190224
current_physical_translation.0 += velocity.0 * fixed_time.delta_seconds();
225+
226+
// Reset the input accumulator, as we are currently consuming all input that happened since the last fixed timestep.
227+
input.0 = Vec2::ZERO;
191228
}
192229
}
193230

194-
fn update_rendered_transform(
231+
fn interpolate_rendered_transform(
195232
fixed_time: Res<Time<Fixed>>,
196233
mut query: Query<(
197234
&mut Transform,

0 commit comments

Comments
 (0)