| 1 |
use bevy::{app::RunFixedMainLoopSystems, prelude::*}; |
| 2 |
|
| 3 |
use crate::actions::Action; |
| 4 |
|
| 5 |
pub const TICKS_PER_SECOND: f64 = 60.0; |
| 6 |
pub const TICKS_PER_HOUR: u64 = 600; |
| 7 |
pub const HOURS_PER_DAY: u64 = 24; |
| 8 |
const START_HOUR: u64 = 6; |
| 9 |
|
| 10 |
pub struct SimPlugin; |
| 11 |
|
| 12 |
impl Plugin for SimPlugin { |
| 13 |
fn build(&self, app: &mut App) { |
| 14 |
app.insert_resource(Time::<Fixed>::from_hz(TICKS_PER_SECOND)) |
| 15 |
.init_resource::<SimTick>() |
| 16 |
.init_resource::<SimSpeed>() |
| 17 |
.configure_sets( |
| 18 |
FixedUpdate, |
| 19 |
(SimSystems::Decide, SimSystems::Act, SimSystems::Move).chain(), |
| 20 |
) |
| 21 |
.add_systems(Update, speed_controls) |
| 22 |
.add_systems(FixedFirst, (advance_tick, remember_positions)) |
| 23 |
.add_systems( |
| 24 |
RunFixedMainLoop, |
| 25 |
interpolate_transforms.in_set(RunFixedMainLoopSystems::AfterFixedMainLoop), |
| 26 |
); |
| 27 |
} |
| 28 |
} |
| 29 |
|
| 30 |
#[derive(SystemSet, Debug, Clone, Copy, PartialEq, Eq, Hash)] |
| 31 |
pub enum SimSystems { |
| 32 |
Decide, |
| 33 |
Act, |
| 34 |
Move, |
| 35 |
} |
| 36 |
|
| 37 |
#[derive(Component, Default, Debug, Clone, Copy, PartialEq)] |
| 38 |
#[require(PrevSimPos)] |
| 39 |
pub struct SimPos(pub Vec2); |
| 40 |
|
| 41 |
#[derive(Component, Default, Debug, Clone, Copy, PartialEq)] |
| 42 |
pub struct PrevSimPos(pub Vec2); |
| 43 |
|
| 44 |
pub fn sim_pos(pos: Vec2) -> (SimPos, PrevSimPos) { |
| 45 |
(SimPos(pos), PrevSimPos(pos)) |
| 46 |
} |
| 47 |
|
| 48 |
fn remember_positions(mut query: Query<(&SimPos, &mut PrevSimPos)>) { |
| 49 |
for (pos, mut prev) in &mut query { |
| 50 |
prev.0 = pos.0; |
| 51 |
} |
| 52 |
} |
| 53 |
|
| 54 |
fn interpolate_transforms( |
| 55 |
time: Res<Time<Fixed>>, |
| 56 |
mut query: Query<(&SimPos, &PrevSimPos, &mut Transform)>, |
| 57 |
) { |
| 58 |
let alpha = time.overstep_fraction(); |
| 59 |
for (pos, prev, mut transform) in &mut query { |
| 60 |
transform.translation = prev.0.lerp(pos.0, alpha).extend(transform.translation.z); |
| 61 |
} |
| 62 |
} |
| 63 |
|
| 64 |
#[derive(Resource, Default, Debug, Clone, Copy, PartialEq, Eq)] |
| 65 |
pub struct SimTick(pub u64); |
| 66 |
|
| 67 |
#[derive(Resource, Default, Debug, Clone, Copy, PartialEq, Eq)] |
| 68 |
pub enum SimSpeed { |
| 69 |
#[default] |
| 70 |
Normal, |
| 71 |
Fast, |
| 72 |
Fastest, |
| 73 |
} |
| 74 |
|
| 75 |
impl SimSpeed { |
| 76 |
pub fn multiplier(self) -> f32 { |
| 77 |
match self { |
| 78 |
SimSpeed::Normal => 1.0, |
| 79 |
SimSpeed::Fast => 3.0, |
| 80 |
SimSpeed::Fastest => 6.0, |
| 81 |
} |
| 82 |
} |
| 83 |
} |
| 84 |
|
| 85 |
fn advance_tick(mut tick: ResMut<SimTick>) { |
| 86 |
tick.0 += 1; |
| 87 |
} |
| 88 |
|
| 89 |
fn speed_controls( |
| 90 |
mut actions: MessageReader<Action>, |
| 91 |
mut speed: ResMut<SimSpeed>, |
| 92 |
mut time: ResMut<Time<Virtual>>, |
| 93 |
) { |
| 94 |
for action in actions.read() { |
| 95 |
match *action { |
| 96 |
Action::TogglePause if time.is_paused() => time.unpause(), |
| 97 |
Action::TogglePause => time.pause(), |
| 98 |
Action::SetSpeed(new_speed) => { |
| 99 |
*speed = new_speed; |
| 100 |
time.set_relative_speed(new_speed.multiplier()); |
| 101 |
time.unpause(); |
| 102 |
} |
| 103 |
_ => {} |
| 104 |
} |
| 105 |
} |
| 106 |
} |
| 107 |
|
| 108 |
pub fn hour_of_day(tick: SimTick) -> f32 { |
| 109 |
let hours = tick.0 as f64 / TICKS_PER_HOUR as f64 + START_HOUR as f64; |
| 110 |
(hours % HOURS_PER_DAY as f64) as f32 |
| 111 |
} |
| 112 |
|
| 113 |
pub fn format_clock(tick: SimTick) -> String { |
| 114 |
let hours = tick.0 / TICKS_PER_HOUR + START_HOUR; |
| 115 |
let day = hours / HOURS_PER_DAY + 1; |
| 116 |
let hour = hours % HOURS_PER_DAY; |
| 117 |
let minute = tick.0 % TICKS_PER_HOUR * 60 / TICKS_PER_HOUR; |
| 118 |
format!("Day {day}, {hour:02}:{minute:02}") |
| 119 |
} |
| 120 |
|
| 121 |
#[cfg(test)] |
| 122 |
mod tests { |
| 123 |
use super::*; |
| 124 |
|
| 125 |
#[test] |
| 126 |
fn clock_starts_in_the_morning_of_day_one() { |
| 127 |
assert_eq!(format_clock(SimTick(0)), "Day 1, 06:00"); |
| 128 |
} |
| 129 |
|
| 130 |
#[test] |
| 131 |
fn hour_of_day_wraps_around_midnight() { |
| 132 |
assert_eq!(hour_of_day(SimTick(0)), 6.0); |
| 133 |
assert_eq!(hour_of_day(SimTick(TICKS_PER_HOUR * 20)), 2.0); |
| 134 |
} |
| 135 |
|
| 136 |
#[test] |
| 137 |
fn clock_advances_minutes_hours_and_days() { |
| 138 |
assert_eq!(format_clock(SimTick(TICKS_PER_HOUR / 2)), "Day 1, 06:30"); |
| 139 |
assert_eq!(format_clock(SimTick(TICKS_PER_HOUR * 18)), "Day 2, 00:00"); |
| 140 |
assert_eq!( |
| 141 |
format_clock(SimTick(TICKS_PER_HOUR * (18 + 24 + 5))), |
| 142 |
"Day 3, 05:00" |
| 143 |
); |
| 144 |
} |
| 145 |
|
| 146 |
|
| 147 |
|
| 148 |
|
| 149 |
#[test] |
| 150 |
fn transforms_are_interpolated_between_ticks() { |
| 151 |
let mut app = App::new(); |
| 152 |
app.insert_resource(Time::<Fixed>::from_hz(TICKS_PER_SECOND)) |
| 153 |
.add_systems(Update, interpolate_transforms); |
| 154 |
let entity = app |
| 155 |
.world_mut() |
| 156 |
.spawn(( |
| 157 |
SimPos(Vec2::new(10.0, 0.0)), |
| 158 |
PrevSimPos(Vec2::ZERO), |
| 159 |
Transform::from_xyz(0.0, 0.0, 1.0), |
| 160 |
)) |
| 161 |
.id(); |
| 162 |
let half_tick = std::time::Duration::from_secs_f64(0.5 / TICKS_PER_SECOND); |
| 163 |
app.world_mut() |
| 164 |
.resource_mut::<Time<Fixed>>() |
| 165 |
.accumulate_overstep(half_tick); |
| 166 |
app.update(); |
| 167 |
let translation = app.world().get::<Transform>(entity).unwrap().translation; |
| 168 |
assert!(translation.abs_diff_eq(Vec3::new(5.0, 0.0, 1.0), 1e-3)); |
| 169 |
} |
| 170 |
|
| 171 |
fn controls_app() -> App { |
| 172 |
let mut app = App::new(); |
| 173 |
app.add_message::<Action>() |
| 174 |
.init_resource::<Time<Virtual>>() |
| 175 |
.init_resource::<SimSpeed>() |
| 176 |
.add_systems(Update, speed_controls); |
| 177 |
app |
| 178 |
} |
| 179 |
|
| 180 |
fn trigger(app: &mut App, action: Action) { |
| 181 |
app.world_mut().write_message(action); |
| 182 |
app.update(); |
| 183 |
} |
| 184 |
|
| 185 |
#[test] |
| 186 |
fn speed_actions_scale_virtual_time() { |
| 187 |
let mut app = controls_app(); |
| 188 |
trigger(&mut app, Action::SetSpeed(SimSpeed::Fastest)); |
| 189 |
assert_eq!(*app.world().resource::<SimSpeed>(), SimSpeed::Fastest); |
| 190 |
assert_eq!( |
| 191 |
app.world().resource::<Time<Virtual>>().relative_speed(), |
| 192 |
6.0 |
| 193 |
); |
| 194 |
} |
| 195 |
|
| 196 |
#[test] |
| 197 |
fn choosing_a_speed_while_paused_resumes() { |
| 198 |
let mut app = controls_app(); |
| 199 |
trigger(&mut app, Action::TogglePause); |
| 200 |
assert!(app.world().resource::<Time<Virtual>>().is_paused()); |
| 201 |
trigger(&mut app, Action::SetSpeed(SimSpeed::Fast)); |
| 202 |
assert!(!app.world().resource::<Time<Virtual>>().is_paused()); |
| 203 |
} |
| 204 |
} |