use bevy::{app::RunFixedMainLoopSystems, prelude::*}; use crate::actions::Action; pub const TICKS_PER_SECOND: f64 = 60.0; pub const TICKS_PER_HOUR: u64 = 600; pub const HOURS_PER_DAY: u64 = 24; const START_HOUR: u64 = 6; pub struct SimPlugin; impl Plugin for SimPlugin { fn build(&self, app: &mut App) { app.insert_resource(Time::::from_hz(TICKS_PER_SECOND)) .init_resource::() .init_resource::() .configure_sets( FixedUpdate, (SimSystems::Decide, SimSystems::Act, SimSystems::Move).chain(), ) .add_systems(Update, speed_controls) .add_systems(FixedFirst, (advance_tick, remember_positions)) .add_systems( RunFixedMainLoop, interpolate_transforms.in_set(RunFixedMainLoopSystems::AfterFixedMainLoop), ); } } #[derive(SystemSet, Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum SimSystems { Decide, Act, Move, } #[derive(Component, Default, Debug, Clone, Copy, PartialEq)] #[require(PrevSimPos)] pub struct SimPos(pub Vec2); #[derive(Component, Default, Debug, Clone, Copy, PartialEq)] pub struct PrevSimPos(pub Vec2); pub fn sim_pos(pos: Vec2) -> (SimPos, PrevSimPos) { (SimPos(pos), PrevSimPos(pos)) } fn remember_positions(mut query: Query<(&SimPos, &mut PrevSimPos)>) { for (pos, mut prev) in &mut query { prev.0 = pos.0; } } fn interpolate_transforms( time: Res>, mut query: Query<(&SimPos, &PrevSimPos, &mut Transform)>, ) { let alpha = time.overstep_fraction(); for (pos, prev, mut transform) in &mut query { transform.translation = prev.0.lerp(pos.0, alpha).extend(transform.translation.z); } } #[derive(Resource, Default, Debug, Clone, Copy, PartialEq, Eq)] pub struct SimTick(pub u64); #[derive(Resource, Default, Debug, Clone, Copy, PartialEq, Eq)] pub enum SimSpeed { #[default] Normal, Fast, Fastest, } impl SimSpeed { pub fn multiplier(self) -> f32 { match self { SimSpeed::Normal => 1.0, SimSpeed::Fast => 3.0, SimSpeed::Fastest => 6.0, } } } fn advance_tick(mut tick: ResMut) { tick.0 += 1; } fn speed_controls( mut actions: MessageReader, mut speed: ResMut, mut time: ResMut>, ) { for action in actions.read() { match *action { Action::TogglePause if time.is_paused() => time.unpause(), Action::TogglePause => time.pause(), Action::SetSpeed(new_speed) => { *speed = new_speed; time.set_relative_speed(new_speed.multiplier()); time.unpause(); } _ => {} } } } pub fn format_clock(tick: SimTick) -> String { let hours = tick.0 / TICKS_PER_HOUR + START_HOUR; let day = hours / HOURS_PER_DAY + 1; let hour = hours % HOURS_PER_DAY; let minute = tick.0 % TICKS_PER_HOUR * 60 / TICKS_PER_HOUR; format!("Day {day}, {hour:02}:{minute:02}") } #[cfg(test)] mod tests { use super::*; #[test] fn clock_starts_in_the_morning_of_day_one() { assert_eq!(format_clock(SimTick(0)), "Day 1, 06:00"); } #[test] fn clock_advances_minutes_hours_and_days() { assert_eq!(format_clock(SimTick(TICKS_PER_HOUR / 2)), "Day 1, 06:30"); assert_eq!(format_clock(SimTick(TICKS_PER_HOUR * 18)), "Day 2, 00:00"); assert_eq!( format_clock(SimTick(TICKS_PER_HOUR * (18 + 24 + 5))), "Day 3, 05:00" ); } // The simulation only moves things once per tick, but frames render more often (and at 6x // speed several ticks run per frame); rendering interpolates between the last two tick // positions so movement looks smooth regardless of frame rate. #[test] fn transforms_are_interpolated_between_ticks() { let mut app = App::new(); app.insert_resource(Time::::from_hz(TICKS_PER_SECOND)) .add_systems(Update, interpolate_transforms); let entity = app .world_mut() .spawn(( SimPos(Vec2::new(10.0, 0.0)), PrevSimPos(Vec2::ZERO), Transform::from_xyz(0.0, 0.0, 1.0), )) .id(); let half_tick = std::time::Duration::from_secs_f64(0.5 / TICKS_PER_SECOND); app.world_mut() .resource_mut::>() .accumulate_overstep(half_tick); app.update(); let translation = app.world().get::(entity).unwrap().translation; assert!(translation.abs_diff_eq(Vec3::new(5.0, 0.0, 1.0), 1e-3)); } fn controls_app() -> App { let mut app = App::new(); app.add_message::() .init_resource::>() .init_resource::() .add_systems(Update, speed_controls); app } fn trigger(app: &mut App, action: Action) { app.world_mut().write_message(action); app.update(); } #[test] fn speed_actions_scale_virtual_time() { let mut app = controls_app(); trigger(&mut app, Action::SetSpeed(SimSpeed::Fastest)); assert_eq!(*app.world().resource::(), SimSpeed::Fastest); assert_eq!( app.world().resource::>().relative_speed(), 6.0 ); } #[test] fn choosing_a_speed_while_paused_resumes() { let mut app = controls_app(); trigger(&mut app, Action::TogglePause); assert!(app.world().resource::>().is_paused()); trigger(&mut app, Action::SetSpeed(SimSpeed::Fast)); assert!(!app.world().resource::>().is_paused()); } }