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