mirror of
https://github.com/bevyengine/bevy
synced 2025-01-09 19:58:58 +00:00
dc3f801239
# Objective The [Stageless RFC](https://github.com/bevyengine/rfcs/pull/45) involves allowing exclusive systems to be referenced and ordered relative to parallel systems. We've agreed that unifying systems under `System` is the right move. This is an alternative to #4166 (see rationale in the comments I left there). Note that this builds on the learnings established there (and borrows some patterns). ## Solution This unifies parallel and exclusive systems under the shared `System` trait, removing the old `ExclusiveSystem` trait / impls. This is accomplished by adding a new `ExclusiveFunctionSystem` impl similar to `FunctionSystem`. It is backed by `ExclusiveSystemParam`, which is similar to `SystemParam`. There is a new flattened out SystemContainer api (which cuts out a lot of trait and type complexity). This means you can remove all cases of `exclusive_system()`: ```rust // before commands.add_system(some_system.exclusive_system()); // after commands.add_system(some_system); ``` I've also implemented `ExclusiveSystemParam` for `&mut QueryState` and `&mut SystemState`, which makes this possible in exclusive systems: ```rust fn some_exclusive_system( world: &mut World, transforms: &mut QueryState<&Transform>, state: &mut SystemState<(Res<Time>, Query<&Player>)>, ) { for transform in transforms.iter(world) { println!("{transform:?}"); } let (time, players) = state.get(world); for player in players.iter() { println!("{player:?}"); } } ``` Note that "exclusive function systems" assume `&mut World` is present (and the first param). I think this is a fair assumption, given that the presence of `&mut World` is what defines the need for an exclusive system. I added some targeted SystemParam `static` constraints, which removed the need for this: ``` rust fn some_exclusive_system(state: &mut SystemState<(Res<'static, Time>, Query<&'static Player>)>) {} ``` ## Related - #2923 - #3001 - #3946 ## Changelog - `ExclusiveSystem` trait (and implementations) has been removed in favor of sharing the `System` trait. - `ExclusiveFunctionSystem` and `ExclusiveSystemParam` were added, enabling flexible exclusive function systems - `&mut SystemState` and `&mut QueryState` now implement `ExclusiveSystemParam` - Exclusive and parallel System configuration is now done via a unified `SystemDescriptor`, `IntoSystemDescriptor`, and `SystemContainer` api. ## Migration Guide Calling `.exclusive_system()` is no longer required (or supported) for converting exclusive system functions to exclusive systems: ```rust // Old (0.8) app.add_system(some_exclusive_system.exclusive_system()); // New (0.9) app.add_system(some_exclusive_system); ``` Converting "normal" parallel systems to exclusive systems is done by calling the exclusive ordering apis: ```rust // Old (0.8) app.add_system(some_system.exclusive_system().at_end()); // New (0.9) app.add_system(some_system.at_end()); ``` Query state in exclusive systems can now be cached via ExclusiveSystemParams, which should be preferred for clarity and performance reasons: ```rust // Old (0.8) fn some_system(world: &mut World) { let mut transforms = world.query::<&Transform>(); for transform in transforms.iter(world) { } } // New (0.9) fn some_system(world: &mut World, transforms: &mut QueryState<&Transform>) { for transform in transforms.iter(world) { } } ```
317 lines
9.5 KiB
Rust
317 lines
9.5 KiB
Rust
use crate::Time;
|
|
use bevy_ecs::{
|
|
archetype::ArchetypeComponentId,
|
|
component::ComponentId,
|
|
query::Access,
|
|
schedule::ShouldRun,
|
|
system::{IntoSystem, Res, ResMut, Resource, System},
|
|
world::World,
|
|
};
|
|
use bevy_utils::HashMap;
|
|
use std::borrow::Cow;
|
|
|
|
/// The internal state of each [`FixedTimestep`].
|
|
#[derive(Debug)]
|
|
pub struct FixedTimestepState {
|
|
step: f64,
|
|
accumulator: f64,
|
|
}
|
|
|
|
impl FixedTimestepState {
|
|
/// The amount of time each step takes.
|
|
pub fn step(&self) -> f64 {
|
|
self.step
|
|
}
|
|
|
|
/// The number of steps made in a second.
|
|
pub fn steps_per_second(&self) -> f64 {
|
|
1.0 / self.step
|
|
}
|
|
|
|
/// The amount of time (in seconds) left over from the last step.
|
|
pub fn accumulator(&self) -> f64 {
|
|
self.accumulator
|
|
}
|
|
|
|
/// The percentage of "step" stored inside the accumulator. Calculated as accumulator / step.
|
|
pub fn overstep_percentage(&self) -> f64 {
|
|
self.accumulator / self.step
|
|
}
|
|
}
|
|
|
|
/// A global resource that tracks the individual [`FixedTimestepState`]s
|
|
/// for every labeled [`FixedTimestep`].
|
|
#[derive(Default, Resource)]
|
|
pub struct FixedTimesteps {
|
|
fixed_timesteps: HashMap<String, FixedTimestepState>,
|
|
}
|
|
|
|
impl FixedTimesteps {
|
|
/// Gets the [`FixedTimestepState`] for a given label.
|
|
pub fn get(&self, name: &str) -> Option<&FixedTimestepState> {
|
|
self.fixed_timesteps.get(name)
|
|
}
|
|
}
|
|
|
|
/// A system run criteria that enables systems or stages to run at a fixed timestep between executions.
|
|
///
|
|
/// This does not guarantee that the time elapsed between executions is exactly the provided
|
|
/// fixed timestep, but will guarantee that the execution will run multiple times per game tick
|
|
/// until the number of repetitions is as expected.
|
|
///
|
|
/// For example, a system with a fixed timestep run criteria of 120 times per second will run
|
|
/// two times during a ~16.667ms frame, once during a ~8.333ms frame, and once every two frames
|
|
/// with ~4.167ms frames. However, the same criteria may not result in exactly 8.333ms passing
|
|
/// between each execution.
|
|
///
|
|
/// When using this run criteria, it is advised not to rely on [`Time::delta`] or any of it's
|
|
/// variants for game simulation, but rather use the constant time delta used to initialize the
|
|
/// [`FixedTimestep`] instead.
|
|
///
|
|
/// For more fine tuned information about the execution status of a given fixed timestep,
|
|
/// use the [`FixedTimesteps`] resource.
|
|
pub struct FixedTimestep {
|
|
state: LocalFixedTimestepState,
|
|
internal_system: Box<dyn System<In = (), Out = ShouldRun>>,
|
|
}
|
|
|
|
impl Default for FixedTimestep {
|
|
fn default() -> Self {
|
|
Self {
|
|
state: LocalFixedTimestepState::default(),
|
|
internal_system: Box::new(IntoSystem::into_system(Self::prepare_system(
|
|
Default::default(),
|
|
))),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl FixedTimestep {
|
|
/// Creates a [`FixedTimestep`] that ticks once every `step` seconds.
|
|
pub fn step(step: f64) -> Self {
|
|
Self {
|
|
state: LocalFixedTimestepState {
|
|
step,
|
|
..Default::default()
|
|
},
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
/// Creates a [`FixedTimestep`] that ticks once every `rate` times per second.
|
|
pub fn steps_per_second(rate: f64) -> Self {
|
|
Self {
|
|
state: LocalFixedTimestepState {
|
|
step: 1.0 / rate,
|
|
..Default::default()
|
|
},
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
/// Sets the label for the timestep. Setting a label allows a timestep
|
|
/// to be observed by the global [`FixedTimesteps`] resource.
|
|
#[must_use]
|
|
pub fn with_label(mut self, label: &str) -> Self {
|
|
self.state.label = Some(label.to_string());
|
|
self
|
|
}
|
|
|
|
fn prepare_system(
|
|
mut state: LocalFixedTimestepState,
|
|
) -> impl FnMut(Res<Time>, ResMut<FixedTimesteps>) -> ShouldRun {
|
|
move |time, mut fixed_timesteps| {
|
|
let should_run = state.update(&time);
|
|
if let Some(ref label) = state.label {
|
|
let res_state = fixed_timesteps.fixed_timesteps.get_mut(label).unwrap();
|
|
res_state.step = state.step;
|
|
res_state.accumulator = state.accumulator;
|
|
}
|
|
|
|
should_run
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
struct LocalFixedTimestepState {
|
|
label: Option<String>, // TODO: consider making this a TypedLabel
|
|
step: f64,
|
|
accumulator: f64,
|
|
looping: bool,
|
|
}
|
|
|
|
impl Default for LocalFixedTimestepState {
|
|
fn default() -> Self {
|
|
Self {
|
|
step: 1.0 / 60.0,
|
|
accumulator: 0.0,
|
|
label: None,
|
|
looping: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl LocalFixedTimestepState {
|
|
fn update(&mut self, time: &Time) -> ShouldRun {
|
|
if !self.looping {
|
|
self.accumulator += time.delta_seconds_f64();
|
|
}
|
|
|
|
if self.accumulator >= self.step {
|
|
self.accumulator -= self.step;
|
|
self.looping = true;
|
|
ShouldRun::YesAndCheckAgain
|
|
} else {
|
|
self.looping = false;
|
|
ShouldRun::No
|
|
}
|
|
}
|
|
}
|
|
|
|
impl System for FixedTimestep {
|
|
type In = ();
|
|
type Out = ShouldRun;
|
|
|
|
fn name(&self) -> Cow<'static, str> {
|
|
Cow::Borrowed(std::any::type_name::<FixedTimestep>())
|
|
}
|
|
|
|
fn archetype_component_access(&self) -> &Access<ArchetypeComponentId> {
|
|
self.internal_system.archetype_component_access()
|
|
}
|
|
|
|
fn component_access(&self) -> &Access<ComponentId> {
|
|
self.internal_system.component_access()
|
|
}
|
|
|
|
fn is_send(&self) -> bool {
|
|
self.internal_system.is_send()
|
|
}
|
|
|
|
fn is_exclusive(&self) -> bool {
|
|
false
|
|
}
|
|
|
|
unsafe fn run_unsafe(&mut self, _input: (), world: &World) -> ShouldRun {
|
|
// SAFETY: this system inherits the internal system's component access and archetype component
|
|
// access, which means the caller has ensured running the internal system is safe
|
|
self.internal_system.run_unsafe((), world)
|
|
}
|
|
|
|
fn apply_buffers(&mut self, world: &mut World) {
|
|
self.internal_system.apply_buffers(world);
|
|
}
|
|
|
|
fn initialize(&mut self, world: &mut World) {
|
|
self.internal_system = Box::new(IntoSystem::into_system(Self::prepare_system(
|
|
self.state.clone(),
|
|
)));
|
|
self.internal_system.initialize(world);
|
|
if let Some(ref label) = self.state.label {
|
|
let mut fixed_timesteps = world.resource_mut::<FixedTimesteps>();
|
|
fixed_timesteps.fixed_timesteps.insert(
|
|
label.clone(),
|
|
FixedTimestepState {
|
|
accumulator: 0.0,
|
|
step: self.state.step,
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
fn update_archetype_component_access(&mut self, world: &World) {
|
|
self.internal_system
|
|
.update_archetype_component_access(world);
|
|
}
|
|
|
|
fn check_change_tick(&mut self, change_tick: u32) {
|
|
self.internal_system.check_change_tick(change_tick);
|
|
}
|
|
|
|
fn get_last_change_tick(&self) -> u32 {
|
|
self.internal_system.get_last_change_tick()
|
|
}
|
|
|
|
fn set_last_change_tick(&mut self, last_change_tick: u32) {
|
|
self.internal_system.set_last_change_tick(last_change_tick);
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use super::*;
|
|
use bevy_ecs::prelude::*;
|
|
use bevy_utils::Instant;
|
|
use std::ops::{Add, Mul};
|
|
use std::time::Duration;
|
|
|
|
#[derive(Resource)]
|
|
struct Count(usize);
|
|
const LABEL: &str = "test_step";
|
|
|
|
#[test]
|
|
fn test() {
|
|
let mut world = World::default();
|
|
let mut time = Time::default();
|
|
let instance = Instant::now();
|
|
time.update_with_instant(instance);
|
|
world.insert_resource(time);
|
|
world.insert_resource(FixedTimesteps::default());
|
|
world.insert_resource(Count(0));
|
|
let mut schedule = Schedule::default();
|
|
|
|
#[derive(StageLabel)]
|
|
struct Update;
|
|
schedule.add_stage(
|
|
Update,
|
|
SystemStage::parallel()
|
|
.with_run_criteria(FixedTimestep::step(0.5).with_label(LABEL))
|
|
.with_system(fixed_update),
|
|
);
|
|
|
|
// if time does not progress, the step does not run
|
|
schedule.run(&mut world);
|
|
schedule.run(&mut world);
|
|
assert_eq!(0, world.resource::<Count>().0);
|
|
assert_eq!(0., get_accumulator_deciseconds(&world));
|
|
|
|
// let's progress less than one step
|
|
advance_time(&mut world, instance, 0.4);
|
|
schedule.run(&mut world);
|
|
assert_eq!(0, world.resource::<Count>().0);
|
|
assert_eq!(4., get_accumulator_deciseconds(&world));
|
|
|
|
// finish the first step with 0.1s above the step length
|
|
advance_time(&mut world, instance, 0.6);
|
|
schedule.run(&mut world);
|
|
assert_eq!(1, world.resource::<Count>().0);
|
|
assert_eq!(1., get_accumulator_deciseconds(&world));
|
|
|
|
// runs multiple times if the delta is multiple step lengths
|
|
advance_time(&mut world, instance, 1.7);
|
|
schedule.run(&mut world);
|
|
assert_eq!(3, world.resource::<Count>().0);
|
|
assert_eq!(2., get_accumulator_deciseconds(&world));
|
|
}
|
|
|
|
fn fixed_update(mut count: ResMut<Count>) {
|
|
count.0 += 1;
|
|
}
|
|
|
|
fn advance_time(world: &mut World, instance: Instant, seconds: f32) {
|
|
world
|
|
.resource_mut::<Time>()
|
|
.update_with_instant(instance.add(Duration::from_secs_f32(seconds)));
|
|
}
|
|
|
|
fn get_accumulator_deciseconds(world: &World) -> f64 {
|
|
world
|
|
.resource::<FixedTimesteps>()
|
|
.get(LABEL)
|
|
.unwrap()
|
|
.accumulator
|
|
.mul(10.)
|
|
.round()
|
|
}
|
|
}
|