mirror of
https://github.com/bevyengine/bevy
synced 2024-11-21 20:23:28 +00:00
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 <git@tau.garden>
This commit is contained in:
parent
f1f07bec09
commit
c92ee31779
6 changed files with 185 additions and 30 deletions
|
@ -34,7 +34,8 @@ pub mod prelude {
|
|||
app::{App, AppExit},
|
||||
main_schedule::{
|
||||
First, FixedFirst, FixedLast, FixedPostUpdate, FixedPreUpdate, FixedUpdate, Last, Main,
|
||||
PostStartup, PostUpdate, PreStartup, PreUpdate, SpawnScene, Startup, Update,
|
||||
PostStartup, PostUpdate, PreStartup, PreUpdate, RunFixedMainLoop,
|
||||
RunFixedMainLoopSystem, SpawnScene, Startup, Update,
|
||||
},
|
||||
sub_app::SubApp,
|
||||
Plugin, PluginGroup,
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
use crate::{App, Plugin};
|
||||
use bevy_ecs::{
|
||||
schedule::{ExecutorKind, InternedScheduleLabel, Schedule, ScheduleLabel},
|
||||
schedule::{
|
||||
ExecutorKind, InternedScheduleLabel, IntoSystemSetConfigs, Schedule, ScheduleLabel,
|
||||
SystemSet,
|
||||
},
|
||||
system::{Local, Resource},
|
||||
world::{Mut, World},
|
||||
};
|
||||
|
@ -75,6 +78,11 @@ pub struct First;
|
|||
pub struct PreUpdate;
|
||||
|
||||
/// Runs the [`FixedMain`] schedule in a loop according until all relevant elapsed time has been "consumed".
|
||||
/// If you need to order your variable timestep systems
|
||||
/// before or after the fixed update logic, use the [`RunFixedMainLoopSystem`] system set.
|
||||
///
|
||||
/// Note that in contrast to most other Bevy schedules, systems added directly to
|
||||
/// [`RunFixedMainLoop`] will *not* be parallelized between each other.
|
||||
///
|
||||
/// See the [`Main`] schedule for some details about how schedules are run.
|
||||
#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)]
|
||||
|
@ -126,8 +134,8 @@ pub struct FixedLast;
|
|||
|
||||
/// The schedule that contains systems which only run after a fixed period of time has elapsed.
|
||||
///
|
||||
/// The exclusive `run_fixed_main_schedule` system runs this schedule.
|
||||
/// This is run by the [`RunFixedMainLoop`] schedule.
|
||||
/// This is run by the [`RunFixedMainLoop`] schedule. If you need to order your variable timestep systems
|
||||
/// before or after the fixed update logic, use the [`RunFixedMainLoopSystem`] system set.
|
||||
///
|
||||
/// Frequency of execution is configured by inserting `Time<Fixed>` resource, 64 Hz by default.
|
||||
/// See [this example](https://github.com/bevyengine/bevy/blob/latest/examples/time/time.rs).
|
||||
|
@ -288,7 +296,16 @@ impl Plugin for MainSchedulePlugin {
|
|||
.init_resource::<MainScheduleOrder>()
|
||||
.init_resource::<FixedMainScheduleOrder>()
|
||||
.add_systems(Main, Main::run_main)
|
||||
.add_systems(FixedMain, FixedMain::run_fixed_main);
|
||||
.add_systems(FixedMain, FixedMain::run_fixed_main)
|
||||
.configure_sets(
|
||||
RunFixedMainLoop,
|
||||
(
|
||||
RunFixedMainLoopSystem::BeforeFixedMainLoop,
|
||||
RunFixedMainLoopSystem::FixedMainLoop,
|
||||
RunFixedMainLoopSystem::AfterFixedMainLoop,
|
||||
)
|
||||
.chain(),
|
||||
);
|
||||
|
||||
#[cfg(feature = "bevy_debug_stepping")]
|
||||
{
|
||||
|
@ -352,3 +369,96 @@ impl FixedMain {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Set enum for the systems that want to run inside [`RunFixedMainLoop`],
|
||||
/// but before or after the fixed update logic. Systems in this set
|
||||
/// will run exactly once per frame, regardless of the number of fixed updates.
|
||||
/// They will also run under a variable timestep.
|
||||
///
|
||||
/// This is useful for handling things that need to run every frame, but
|
||||
/// also need to be read by the fixed update logic. See the individual variants
|
||||
/// for examples of what kind of systems should be placed in each.
|
||||
///
|
||||
/// Note that in contrast to most other Bevy schedules, systems added directly to
|
||||
/// [`RunFixedMainLoop`] will *not* be parallelized between each other.
|
||||
#[derive(Debug, Hash, PartialEq, Eq, Copy, Clone, SystemSet)]
|
||||
pub enum RunFixedMainLoopSystem {
|
||||
/// Runs before the fixed update logic.
|
||||
///
|
||||
/// A good example of a system that fits here
|
||||
/// is camera movement, which needs to be updated in a variable timestep,
|
||||
/// as you want the camera to move with as much precision and updates as
|
||||
/// the frame rate allows. A physics system that needs to read the camera
|
||||
/// position and orientation, however, should run in the fixed update logic,
|
||||
/// as it needs to be deterministic and run at a fixed rate for better stability.
|
||||
/// Note that we are not placing the camera movement system in `Update`, as that
|
||||
/// would mean that the physics system already ran at that point.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use bevy_app::prelude::*;
|
||||
/// # use bevy_ecs::prelude::*;
|
||||
/// App::new()
|
||||
/// .add_systems(
|
||||
/// RunFixedMainLoop,
|
||||
/// update_camera_rotation.in_set(RunFixedMainLoopSystem::BeforeFixedMainLoop))
|
||||
/// .add_systems(FixedUpdate, update_physics);
|
||||
///
|
||||
/// # fn update_camera_rotation() {}
|
||||
/// # fn update_physics() {}
|
||||
/// ```
|
||||
BeforeFixedMainLoop,
|
||||
/// Contains the fixed update logic.
|
||||
/// Runs [`FixedMain`] zero or more times based on delta of
|
||||
/// [`Time<Virtual>`] and [`Time::overstep`].
|
||||
///
|
||||
/// Don't place systems here, use [`FixedUpdate`] and friends instead.
|
||||
/// Use this system instead to order your systems to run specifically inbetween the fixed update logic and all
|
||||
/// other systems that run in [`RunFixedMainLoopSystem::BeforeFixedMainLoop`] or [`RunFixedMainLoopSystem::AfterFixedMainLoop`].
|
||||
///
|
||||
/// [`Time<Virtual>`]: https://docs.rs/bevy/latest/bevy/prelude/struct.Virtual.html
|
||||
/// [`Time::overstep`]: https://docs.rs/bevy/latest/bevy/time/struct.Time.html#method.overstep
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use bevy_app::prelude::*;
|
||||
/// # use bevy_ecs::prelude::*;
|
||||
/// App::new()
|
||||
/// .add_systems(FixedUpdate, update_physics)
|
||||
/// .add_systems(
|
||||
/// RunFixedMainLoop,
|
||||
/// (
|
||||
/// // This system will be called before all interpolation systems
|
||||
/// // that third-party plugins might add.
|
||||
/// prepare_for_interpolation
|
||||
/// .after(RunFixedMainLoopSystem::FixedMainLoop)
|
||||
/// .before(RunFixedMainLoopSystem::AfterFixedMainLoop),
|
||||
/// )
|
||||
/// );
|
||||
///
|
||||
/// # fn prepare_for_interpolation() {}
|
||||
/// # fn update_physics() {}
|
||||
/// ```
|
||||
FixedMainLoop,
|
||||
/// Runs after the fixed update logic.
|
||||
///
|
||||
/// A good example of a system that fits here
|
||||
/// is a system that interpolates the transform of an entity between the last and current fixed update.
|
||||
/// See the [fixed timestep example] for more details.
|
||||
///
|
||||
/// [fixed timestep example]: https://github.com/bevyengine/bevy/blob/main/examples/movement/physics_in_fixed_timestep.rs
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use bevy_app::prelude::*;
|
||||
/// # use bevy_ecs::prelude::*;
|
||||
/// App::new()
|
||||
/// .add_systems(FixedUpdate, update_physics)
|
||||
/// .add_systems(
|
||||
/// RunFixedMainLoop,
|
||||
/// interpolate_transforms.in_set(RunFixedMainLoopSystem::AfterFixedMainLoop));
|
||||
///
|
||||
/// # fn interpolate_transforms() {}
|
||||
/// # fn update_physics() {}
|
||||
/// ```
|
||||
AfterFixedMainLoop,
|
||||
}
|
||||
|
|
|
@ -246,13 +246,15 @@ impl AppGizmoBuilder for App {
|
|||
.init_resource::<GizmoStorage<Config, Swap<Fixed>>>()
|
||||
.add_systems(
|
||||
RunFixedMainLoop,
|
||||
start_gizmo_context::<Config, Fixed>.before(bevy_time::run_fixed_main_schedule),
|
||||
start_gizmo_context::<Config, Fixed>
|
||||
.in_set(bevy_app::RunFixedMainLoopSystem::BeforeFixedMainLoop),
|
||||
)
|
||||
.add_systems(FixedFirst, clear_gizmo_context::<Config, Fixed>)
|
||||
.add_systems(FixedLast, collect_requested_gizmos::<Config, Fixed>)
|
||||
.add_systems(
|
||||
RunFixedMainLoop,
|
||||
end_gizmo_context::<Config, Fixed>.after(bevy_time::run_fixed_main_schedule),
|
||||
end_gizmo_context::<Config, Fixed>
|
||||
.in_set(bevy_app::RunFixedMainLoopSystem::AfterFixedMainLoop),
|
||||
)
|
||||
.add_systems(
|
||||
Last,
|
||||
|
|
|
@ -233,8 +233,10 @@ impl Default for Fixed {
|
|||
}
|
||||
|
||||
/// Runs [`FixedMain`] zero or more times based on delta of
|
||||
/// [`Time<Virtual>`](Virtual) and [`Time::overstep`]
|
||||
pub fn run_fixed_main_schedule(world: &mut World) {
|
||||
/// [`Time<Virtual>`](Virtual) and [`Time::overstep`].
|
||||
/// You can order your systems relative to this by using
|
||||
/// [`RunFixedMainLoopSystem`](bevy_app::prelude::RunFixedMainLoopSystem).
|
||||
pub(super) fn run_fixed_main_schedule(world: &mut World) {
|
||||
let delta = world.resource::<Time<Virtual>>().delta();
|
||||
world.resource_mut::<Time<Fixed>>().accumulate(delta);
|
||||
|
||||
|
|
|
@ -70,7 +70,10 @@ impl Plugin for TimePlugin {
|
|||
.in_set(TimeSystem)
|
||||
.ambiguous_with(event_update_system),
|
||||
)
|
||||
.add_systems(RunFixedMainLoop, run_fixed_main_schedule);
|
||||
.add_systems(
|
||||
RunFixedMainLoop,
|
||||
run_fixed_main_schedule.in_set(RunFixedMainLoopSystem::FixedMainLoop),
|
||||
);
|
||||
|
||||
// Ensure the events are not dropped until `FixedMain` systems can observe them
|
||||
app.add_systems(FixedPostUpdate, signal_event_update_system);
|
||||
|
|
|
@ -53,16 +53,22 @@
|
|||
//!
|
||||
//! ## Implementation
|
||||
//!
|
||||
//! - The player's inputs since the last physics update are stored in the `AccumulatedInput` component.
|
||||
//! - 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:
|
||||
//! - Accumulate the player's input and set the current speed in the `handle_input` system.
|
||||
//! This is run in the `RunFixedMainLoop` schedule, ordered in `RunFixedMainLoopSystem::BeforeFixedMainLoop`,
|
||||
//! which runs before the fixed timestep loop. This is run every frame.
|
||||
//! - 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_rendered_transform` system.
|
||||
//! Accumulated input is consumed here.
|
||||
//! This is run in the `FixedUpdate` schedule, which runs zero or multiple times per frame.
|
||||
//! - Update the player's visual representation in the `interpolate_rendered_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.
|
||||
//! It is run in the `RunFixedMainLoop` schedule, ordered in `RunFixedMainLoopSystem::AfterFixedMainLoop`,
|
||||
//! which runs after the fixed timestep loop. This is run every frame.
|
||||
//!
|
||||
//!
|
||||
//! ## Controls
|
||||
|
@ -80,13 +86,31 @@ fn main() {
|
|||
App::new()
|
||||
.add_plugins(DefaultPlugins)
|
||||
.add_systems(Startup, (spawn_text, spawn_player))
|
||||
// `FixedUpdate` runs before `Update`, so the physics simulation is advanced before the player's visual representation is updated.
|
||||
// Advance the physics simulation using a fixed timestep.
|
||||
.add_systems(FixedUpdate, advance_physics)
|
||||
.add_systems(Update, (update_rendered_transform, handle_input).chain())
|
||||
.add_systems(
|
||||
// The `RunFixedMainLoop` schedule allows us to schedule systems to run before and after the fixed timestep loop.
|
||||
RunFixedMainLoop,
|
||||
(
|
||||
// The physics simulation needs to know the player's input, so we run this before the fixed timestep loop.
|
||||
// Note that if we ran it in `Update`, it would be too late, as the physics simulation would already have been advanced.
|
||||
// If we ran this in `FixedUpdate`, it would sometimes not register player input, as that schedule may run zero times per frame.
|
||||
handle_input.in_set(RunFixedMainLoopSystem::BeforeFixedMainLoop),
|
||||
// The player's visual representation needs to be updated after the physics simulation has been advanced.
|
||||
// This could be run in `Update`, but if we run it here instead, the systems in `Update`
|
||||
// will be working with the `Transform` that will actually be shown on screen.
|
||||
interpolate_rendered_transform.in_set(RunFixedMainLoopSystem::AfterFixedMainLoop),
|
||||
),
|
||||
)
|
||||
.run();
|
||||
}
|
||||
|
||||
/// How many units per second the player should move.
|
||||
/// A vector representing the player's input, accumulated over all frames that ran
|
||||
/// since the last time the physics simulation was advanced.
|
||||
#[derive(Debug, Component, Clone, Copy, PartialEq, Default, Deref, DerefMut)]
|
||||
struct AccumulatedInput(Vec2);
|
||||
|
||||
/// A vector representing the player's velocity in the physics simulation.
|
||||
#[derive(Debug, Component, Clone, Copy, PartialEq, Default, Deref, DerefMut)]
|
||||
struct Velocity(Vec3);
|
||||
|
||||
|
@ -100,7 +124,7 @@ struct Velocity(Vec3);
|
|||
struct PhysicalTranslation(Vec3);
|
||||
|
||||
/// The value [`PhysicalTranslation`] had in the last fixed timestep.
|
||||
/// Used for interpolation in the `update_rendered_transform` system.
|
||||
/// Used for interpolation in the `interpolate_rendered_transform` system.
|
||||
#[derive(Debug, Component, Clone, Copy, PartialEq, Default, Deref, DerefMut)]
|
||||
struct PreviousPhysicalTranslation(Vec3);
|
||||
|
||||
|
@ -114,6 +138,7 @@ fn spawn_player(mut commands: Commands, asset_server: Res<AssetServer>) {
|
|||
transform: Transform::from_scale(Vec3::splat(0.3)),
|
||||
..default()
|
||||
},
|
||||
AccumulatedInput::default(),
|
||||
Velocity::default(),
|
||||
PhysicalTranslation::default(),
|
||||
PreviousPhysicalTranslation::default(),
|
||||
|
@ -143,31 +168,35 @@ fn spawn_text(mut commands: Commands) {
|
|||
});
|
||||
}
|
||||
|
||||
/// Handle keyboard input to move the player.
|
||||
fn handle_input(keyboard_input: Res<ButtonInput<KeyCode>>, mut query: Query<&mut Velocity>) {
|
||||
/// Handle keyboard input and accumulate it in the `AccumulatedInput` component.
|
||||
/// There are many strategies for how to handle all the input that happened since the last fixed timestep.
|
||||
/// This is a very simple one: we just accumulate the input and average it out by normalizing it.
|
||||
fn handle_input(
|
||||
keyboard_input: Res<ButtonInput<KeyCode>>,
|
||||
mut query: Query<(&mut AccumulatedInput, &mut Velocity)>,
|
||||
) {
|
||||
/// Since Bevy's default 2D camera setup is scaled such that
|
||||
/// one unit is one pixel, you can think of this as
|
||||
/// "How many pixels per second should the player move?"
|
||||
const SPEED: f32 = 210.0;
|
||||
for mut velocity in query.iter_mut() {
|
||||
velocity.0 = Vec3::ZERO;
|
||||
|
||||
for (mut input, mut velocity) in query.iter_mut() {
|
||||
if keyboard_input.pressed(KeyCode::KeyW) {
|
||||
velocity.y += 1.0;
|
||||
input.y += 1.0;
|
||||
}
|
||||
if keyboard_input.pressed(KeyCode::KeyS) {
|
||||
velocity.y -= 1.0;
|
||||
input.y -= 1.0;
|
||||
}
|
||||
if keyboard_input.pressed(KeyCode::KeyA) {
|
||||
velocity.x -= 1.0;
|
||||
input.x -= 1.0;
|
||||
}
|
||||
if keyboard_input.pressed(KeyCode::KeyD) {
|
||||
velocity.x += 1.0;
|
||||
input.x += 1.0;
|
||||
}
|
||||
|
||||
// Need to normalize and scale because otherwise
|
||||
// diagonal movement would be faster than horizontal or vertical movement.
|
||||
velocity.0 = velocity.normalize_or_zero() * SPEED;
|
||||
// This effectively averages the accumulated input.
|
||||
velocity.0 = input.extend(0.0).normalize_or_zero() * SPEED;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -180,18 +209,26 @@ fn advance_physics(
|
|||
mut query: Query<(
|
||||
&mut PhysicalTranslation,
|
||||
&mut PreviousPhysicalTranslation,
|
||||
&mut AccumulatedInput,
|
||||
&Velocity,
|
||||
)>,
|
||||
) {
|
||||
for (mut current_physical_translation, mut previous_physical_translation, velocity) in
|
||||
query.iter_mut()
|
||||
for (
|
||||
mut current_physical_translation,
|
||||
mut previous_physical_translation,
|
||||
mut input,
|
||||
velocity,
|
||||
) in query.iter_mut()
|
||||
{
|
||||
previous_physical_translation.0 = current_physical_translation.0;
|
||||
current_physical_translation.0 += velocity.0 * fixed_time.delta_seconds();
|
||||
|
||||
// Reset the input accumulator, as we are currently consuming all input that happened since the last fixed timestep.
|
||||
input.0 = Vec2::ZERO;
|
||||
}
|
||||
}
|
||||
|
||||
fn update_rendered_transform(
|
||||
fn interpolate_rendered_transform(
|
||||
fixed_time: Res<Time<Fixed>>,
|
||||
mut query: Query<(
|
||||
&mut Transform,
|
||||
|
|
Loading…
Reference in a new issue