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;
#[derive(Debug)]
pub struct FixedTimestepState {
step: f64,
accumulator: f64,
}
impl FixedTimestepState {
pub fn step(&self) -> f64 {
self.step
}
pub fn steps_per_second(&self) -> f64 {
1.0 / self.step
}
pub fn accumulator(&self) -> f64 {
self.accumulator
}
pub fn overstep_percentage(&self) -> f64 {
self.accumulator / self.step
}
}
#[derive(Default, Resource)]
pub struct FixedTimesteps {
fixed_timesteps: HashMap<String, FixedTimestepState>,
}
impl FixedTimesteps {
pub fn get(&self, name: &str) -> Option<&FixedTimestepState> {
self.fixed_timesteps.get(name)
}
}
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 {
pub fn step(step: f64) -> Self {
Self {
state: LocalFixedTimestepState {
step,
..Default::default()
},
..Default::default()
}
}
pub fn steps_per_second(rate: f64) -> Self {
Self {
state: LocalFixedTimestepState {
step: 1.0 / rate,
..Default::default()
},
..Default::default()
}
}
#[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>, 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 {
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),
);
schedule.run(&mut world);
schedule.run(&mut world);
assert_eq!(0, world.resource::<Count>().0);
assert_eq!(0., get_accumulator_deciseconds(&world));
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));
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));
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()
}
}