| 1 |
use std::time::Duration; |
| 2 |
|
| 3 |
use bevy::{app::RunFixedMainLoopSystems, platform::time::Instant, prelude::*}; |
| 4 |
|
| 5 |
use crate::actions::Action; |
| 6 |
|
| 7 |
pub const TICKS_PER_SECOND: f64 = 60.0; |
| 8 |
pub const TICKS_PER_HOUR: u64 = 600; |
| 9 |
pub const HOURS_PER_DAY: u64 = 24; |
| 10 |
pub const START_HOUR: u64 = 6; |
| 11 |
|
| 12 |
pub struct SimPlugin; |
| 13 |
|
| 14 |
impl Plugin for SimPlugin { |
| 15 |
fn build(&self, app: &mut App) { |
| 16 |
app.insert_resource(Time::<Fixed>::from_hz(TICKS_PER_SECOND)) |
| 17 |
.init_resource::<SimTick>() |
| 18 |
.init_resource::<SimSpeed>() |
| 19 |
.configure_sets( |
| 20 |
FixedUpdate, |
| 21 |
(SimSystems::Decide, SimSystems::Act, SimSystems::Move).chain(), |
| 22 |
) |
| 23 |
.init_resource::<SimClock>() |
| 24 |
.add_systems(Update, (speed_controls, adapt_max_speed).chain()) |
| 25 |
.add_systems(FixedFirst, (advance_tick, remember_positions)) |
| 26 |
.add_systems( |
| 27 |
RunFixedMainLoop, |
| 28 |
( |
| 29 |
start_sim_clock.in_set(RunFixedMainLoopSystems::BeforeFixedMainLoop), |
| 30 |
(stop_sim_clock, interpolate_transforms) |
| 31 |
.in_set(RunFixedMainLoopSystems::AfterFixedMainLoop), |
| 32 |
), |
| 33 |
); |
| 34 |
} |
| 35 |
} |
| 36 |
|
| 37 |
#[derive(SystemSet, Debug, Clone, Copy, PartialEq, Eq, Hash)] |
| 38 |
pub enum SimSystems { |
| 39 |
Decide, |
| 40 |
Act, |
| 41 |
Move, |
| 42 |
} |
| 43 |
|
| 44 |
#[derive(Component, Default, Debug, Clone, Copy, PartialEq)] |
| 45 |
#[require(PrevSimPos)] |
| 46 |
pub struct SimPos(pub Vec2); |
| 47 |
|
| 48 |
#[derive(Component, Default, Debug, Clone, Copy, PartialEq)] |
| 49 |
pub struct PrevSimPos(pub Vec2); |
| 50 |
|
| 51 |
pub fn sim_pos(pos: Vec2) -> (SimPos, PrevSimPos) { |
| 52 |
(SimPos(pos), PrevSimPos(pos)) |
| 53 |
} |
| 54 |
|
| 55 |
fn remember_positions(mut query: Query<(&SimPos, &mut PrevSimPos)>) { |
| 56 |
for (pos, mut prev) in &mut query { |
| 57 |
prev.0 = pos.0; |
| 58 |
} |
| 59 |
} |
| 60 |
|
| 61 |
fn interpolate_transforms( |
| 62 |
time: Res<Time<Fixed>>, |
| 63 |
mut query: Query<(&SimPos, &PrevSimPos, &mut Transform)>, |
| 64 |
) { |
| 65 |
let alpha = time.overstep_fraction(); |
| 66 |
for (pos, prev, mut transform) in &mut query { |
| 67 |
transform.translation = prev.0.lerp(pos.0, alpha).extend(transform.translation.z); |
| 68 |
} |
| 69 |
} |
| 70 |
|
| 71 |
#[derive(Resource, Default, Debug, Clone, Copy, PartialEq, Eq)] |
| 72 |
pub struct SimTick(pub u64); |
| 73 |
|
| 74 |
#[derive(Resource, Default, Debug, Clone, Copy, PartialEq, Eq)] |
| 75 |
pub enum SimSpeed { |
| 76 |
#[default] |
| 77 |
Normal, |
| 78 |
Fast, |
| 79 |
Fastest, |
| 80 |
Max, |
| 81 |
} |
| 82 |
|
| 83 |
impl SimSpeed { |
| 84 |
pub fn multiplier(self) -> f32 { |
| 85 |
match self { |
| 86 |
SimSpeed::Normal => 1.0, |
| 87 |
SimSpeed::Fast => 3.0, |
| 88 |
SimSpeed::Fastest => 6.0, |
| 89 |
SimSpeed::Max => MAX_SPEED_START, |
| 90 |
} |
| 91 |
} |
| 92 |
} |
| 93 |
|
| 94 |
pub const MAX_SPEED_START: f32 = 10.0; |
| 95 |
pub const MAX_SPEED_LIMITS: (f32, f32) = (6.0, 2000.0); |
| 96 |
pub const MAX_SPEED_FRAME_SECS: f32 = 1.0 / 30.0; |
| 97 |
const DEFAULT_MAX_DELTA: Duration = Duration::from_millis(250); |
| 98 |
|
| 99 |
pub fn max_mode_max_delta(speed: f32) -> Duration { |
| 100 |
Duration::from_secs_f32(MAX_SPEED_FRAME_SECS * speed) |
| 101 |
} |
| 102 |
|
| 103 |
const MAX_SPEED_BUDGET_SHARE: f32 = 0.6; |
| 104 |
const MAX_SPEED_SMOOTHING: f32 = 0.3; |
| 105 |
const MAX_SPEED_RISE: f32 = 1.5; |
| 106 |
|
| 107 |
pub fn next_max_speed(current: f32, clock: &SimClock, ticks_in_frame: u64) -> f32 { |
| 108 |
let sim_target = if ticks_in_frame == 0 { |
| 109 |
current * 1.1 |
| 110 |
} else { |
| 111 |
let secs_per_tick = clock.last_sim_secs / ticks_in_frame as f32; |
| 112 |
let affordable_ticks = MAX_SPEED_FRAME_SECS * MAX_SPEED_BUDGET_SHARE / secs_per_tick; |
| 113 |
affordable_ticks / (TICKS_PER_SECOND as f32 * MAX_SPEED_FRAME_SECS) |
| 114 |
}; |
| 115 |
let frame_target = current * MAX_SPEED_FRAME_SECS / clock.last_frame_secs.max(f32::EPSILON); |
| 116 |
let target = if clock.last_frame_secs > MAX_SPEED_FRAME_SECS { |
| 117 |
sim_target.min(frame_target) |
| 118 |
} else { |
| 119 |
sim_target |
| 120 |
}; |
| 121 |
(current + (target.min(current * MAX_SPEED_RISE) - current) * MAX_SPEED_SMOOTHING) |
| 122 |
.clamp(MAX_SPEED_LIMITS.0, MAX_SPEED_LIMITS.1) |
| 123 |
} |
| 124 |
|
| 125 |
#[derive(Resource, Default)] |
| 126 |
pub struct SimClock { |
| 127 |
started: Option<Instant>, |
| 128 |
frame_started: Option<Instant>, |
| 129 |
pub last_sim_secs: f32, |
| 130 |
pub last_frame_secs: f32, |
| 131 |
} |
| 132 |
|
| 133 |
fn start_sim_clock(mut clock: ResMut<SimClock>) { |
| 134 |
let now = Instant::now(); |
| 135 |
if let Some(previous) = clock.frame_started { |
| 136 |
clock.last_frame_secs = now.duration_since(previous).as_secs_f32(); |
| 137 |
} |
| 138 |
clock.frame_started = Some(now); |
| 139 |
clock.started = Some(now); |
| 140 |
} |
| 141 |
|
| 142 |
fn stop_sim_clock(mut clock: ResMut<SimClock>) { |
| 143 |
if let Some(started) = clock.started.take() { |
| 144 |
clock.last_sim_secs = started.elapsed().as_secs_f32(); |
| 145 |
} |
| 146 |
} |
| 147 |
|
| 148 |
fn adapt_max_speed( |
| 149 |
speed: Res<SimSpeed>, |
| 150 |
clock: Res<SimClock>, |
| 151 |
tick: Res<SimTick>, |
| 152 |
mut time: ResMut<Time<Virtual>>, |
| 153 |
mut last_tick: Local<u64>, |
| 154 |
) { |
| 155 |
let ticks = tick.0.saturating_sub(*last_tick); |
| 156 |
*last_tick = tick.0; |
| 157 |
if *speed != SimSpeed::Max || time.is_paused() { |
| 158 |
return; |
| 159 |
} |
| 160 |
let next = next_max_speed(time.relative_speed(), &clock, ticks); |
| 161 |
time.set_relative_speed(next); |
| 162 |
time.set_max_delta(max_mode_max_delta(next)); |
| 163 |
} |
| 164 |
|
| 165 |
fn advance_tick(mut tick: ResMut<SimTick>) { |
| 166 |
tick.0 += 1; |
| 167 |
} |
| 168 |
|
| 169 |
fn speed_controls( |
| 170 |
mut actions: MessageReader<Action>, |
| 171 |
mut speed: ResMut<SimSpeed>, |
| 172 |
mut time: ResMut<Time<Virtual>>, |
| 173 |
) { |
| 174 |
for action in actions.read() { |
| 175 |
match *action { |
| 176 |
Action::TogglePause if time.is_paused() => time.unpause(), |
| 177 |
Action::TogglePause => time.pause(), |
| 178 |
Action::SetSpeed(new_speed) => { |
| 179 |
*speed = new_speed; |
| 180 |
time.set_relative_speed(new_speed.multiplier()); |
| 181 |
time.set_max_delta(if new_speed == SimSpeed::Max { |
| 182 |
max_mode_max_delta(MAX_SPEED_START) |
| 183 |
} else { |
| 184 |
DEFAULT_MAX_DELTA |
| 185 |
}); |
| 186 |
time.unpause(); |
| 187 |
} |
| 188 |
_ => {} |
| 189 |
} |
| 190 |
} |
| 191 |
} |
| 192 |
|
| 193 |
pub fn hour_of_day(tick: SimTick) -> f32 { |
| 194 |
let hours = tick.0 as f64 / TICKS_PER_HOUR as f64 + START_HOUR as f64; |
| 195 |
(hours % HOURS_PER_DAY as f64) as f32 |
| 196 |
} |
| 197 |
|
| 198 |
pub fn format_clock(tick: SimTick) -> String { |
| 199 |
let hours = tick.0 / TICKS_PER_HOUR + START_HOUR; |
| 200 |
let day = hours / HOURS_PER_DAY + 1; |
| 201 |
let hour = hours % HOURS_PER_DAY; |
| 202 |
let minute = tick.0 % TICKS_PER_HOUR * 60 / TICKS_PER_HOUR; |
| 203 |
format!("Day {day}, {hour:02}:{minute:02}") |
| 204 |
} |
| 205 |
|
| 206 |
#[cfg(test)] |
| 207 |
mod tests { |
| 208 |
use super::*; |
| 209 |
|
| 210 |
#[test] |
| 211 |
fn clock_starts_in_the_morning_of_day_one() { |
| 212 |
assert_eq!(format_clock(SimTick(0)), "Day 1, 06:00"); |
| 213 |
} |
| 214 |
|
| 215 |
#[test] |
| 216 |
fn hour_of_day_wraps_around_midnight() { |
| 217 |
assert_eq!(hour_of_day(SimTick(0)), 6.0); |
| 218 |
assert_eq!(hour_of_day(SimTick(TICKS_PER_HOUR * 20)), 2.0); |
| 219 |
} |
| 220 |
|
| 221 |
#[test] |
| 222 |
fn clock_advances_minutes_hours_and_days() { |
| 223 |
assert_eq!(format_clock(SimTick(TICKS_PER_HOUR / 2)), "Day 1, 06:30"); |
| 224 |
assert_eq!(format_clock(SimTick(TICKS_PER_HOUR * 18)), "Day 2, 00:00"); |
| 225 |
assert_eq!( |
| 226 |
format_clock(SimTick(TICKS_PER_HOUR * (18 + 24 + 5))), |
| 227 |
"Day 3, 05:00" |
| 228 |
); |
| 229 |
} |
| 230 |
|
| 231 |
|
| 232 |
|
| 233 |
|
| 234 |
#[test] |
| 235 |
fn transforms_are_interpolated_between_ticks() { |
| 236 |
let mut app = App::new(); |
| 237 |
app.insert_resource(Time::<Fixed>::from_hz(TICKS_PER_SECOND)) |
| 238 |
.add_systems(Update, interpolate_transforms); |
| 239 |
let entity = app |
| 240 |
.world_mut() |
| 241 |
.spawn(( |
| 242 |
SimPos(Vec2::new(10.0, 0.0)), |
| 243 |
PrevSimPos(Vec2::ZERO), |
| 244 |
Transform::from_xyz(0.0, 0.0, 1.0), |
| 245 |
)) |
| 246 |
.id(); |
| 247 |
let half_tick = std::time::Duration::from_secs_f64(0.5 / TICKS_PER_SECOND); |
| 248 |
app.world_mut() |
| 249 |
.resource_mut::<Time<Fixed>>() |
| 250 |
.accumulate_overstep(half_tick); |
| 251 |
app.update(); |
| 252 |
let translation = app.world().get::<Transform>(entity).unwrap().translation; |
| 253 |
assert!(translation.abs_diff_eq(Vec3::new(5.0, 0.0, 1.0), 1e-3)); |
| 254 |
} |
| 255 |
|
| 256 |
fn controls_app() -> App { |
| 257 |
let mut app = App::new(); |
| 258 |
app.add_message::<Action>() |
| 259 |
.init_resource::<Time<Virtual>>() |
| 260 |
.init_resource::<SimTick>() |
| 261 |
.init_resource::<SimClock>() |
| 262 |
.init_resource::<SimSpeed>() |
| 263 |
.add_systems(Update, (speed_controls, adapt_max_speed).chain()); |
| 264 |
app |
| 265 |
} |
| 266 |
|
| 267 |
fn trigger(app: &mut App, action: Action) { |
| 268 |
app.world_mut().write_message(action); |
| 269 |
app.update(); |
| 270 |
} |
| 271 |
|
| 272 |
#[test] |
| 273 |
fn speed_actions_scale_virtual_time() { |
| 274 |
let mut app = controls_app(); |
| 275 |
trigger(&mut app, Action::SetSpeed(SimSpeed::Fastest)); |
| 276 |
assert_eq!(*app.world().resource::<SimSpeed>(), SimSpeed::Fastest); |
| 277 |
assert_eq!( |
| 278 |
app.world().resource::<Time<Virtual>>().relative_speed(), |
| 279 |
6.0 |
| 280 |
); |
| 281 |
} |
| 282 |
|
| 283 |
fn clock(sim_secs: f32, frame_secs: f32) -> SimClock { |
| 284 |
SimClock { |
| 285 |
last_sim_secs: sim_secs, |
| 286 |
last_frame_secs: frame_secs, |
| 287 |
..default() |
| 288 |
} |
| 289 |
} |
| 290 |
|
| 291 |
|
| 292 |
|
| 293 |
|
| 294 |
|
| 295 |
|
| 296 |
#[test] |
| 297 |
fn max_speed_settles_on_what_the_machine_can_afford() { |
| 298 |
let secs_per_tick = 0.0005; |
| 299 |
let mut speed = MAX_SPEED_START; |
| 300 |
for _ in 0..60 { |
| 301 |
let ticks = (speed * TICKS_PER_SECOND as f32 * MAX_SPEED_FRAME_SECS) as u64; |
| 302 |
let sim = ticks as f32 * secs_per_tick; |
| 303 |
speed = next_max_speed(speed, &clock(sim, sim.max(1.0 / 120.0)), ticks); |
| 304 |
} |
| 305 |
let frame = speed * TICKS_PER_SECOND as f32 * MAX_SPEED_FRAME_SECS * secs_per_tick; |
| 306 |
assert!( |
| 307 |
(frame - MAX_SPEED_FRAME_SECS * MAX_SPEED_BUDGET_SHARE).abs() < 0.002, |
| 308 |
"{frame}" |
| 309 |
); |
| 310 |
} |
| 311 |
|
| 312 |
|
| 313 |
|
| 314 |
#[test] |
| 315 |
fn max_speed_backs_off_when_frames_are_slow() { |
| 316 |
let slow = clock(0.001, MAX_SPEED_FRAME_SECS * 4.0); |
| 317 |
assert!(next_max_speed(400.0, &slow, 800) < 400.0); |
| 318 |
} |
| 319 |
|
| 320 |
|
| 321 |
|
| 322 |
#[test] |
| 323 |
fn max_speed_rises_gradually_on_suspiciously_cheap_ticks() { |
| 324 |
assert!(next_max_speed(10.0, &clock(0.0, 0.01), 7) <= 10.0 * MAX_SPEED_RISE); |
| 325 |
} |
| 326 |
|
| 327 |
#[test] |
| 328 |
fn max_speed_stays_within_limits() { |
| 329 |
assert_eq!( |
| 330 |
next_max_speed(MAX_SPEED_LIMITS.1, &clock(0.0001, 0.001), 1000), |
| 331 |
MAX_SPEED_LIMITS.1 |
| 332 |
); |
| 333 |
assert_eq!( |
| 334 |
next_max_speed(MAX_SPEED_LIMITS.0, &clock(10.0, 10.0), 10), |
| 335 |
MAX_SPEED_LIMITS.0 |
| 336 |
); |
| 337 |
assert!(next_max_speed(MAX_SPEED_START, &clock(0.0, 0.01), 0) > MAX_SPEED_START); |
| 338 |
} |
| 339 |
|
| 340 |
|
| 341 |
|
| 342 |
#[test] |
| 343 |
fn max_speed_lifts_the_per_frame_time_cap() { |
| 344 |
let mut app = controls_app(); |
| 345 |
trigger(&mut app, Action::SetSpeed(SimSpeed::Max)); |
| 346 |
assert!(app.world().resource::<Time<Virtual>>().max_delta() > DEFAULT_MAX_DELTA); |
| 347 |
trigger(&mut app, Action::SetSpeed(SimSpeed::Normal)); |
| 348 |
assert_eq!( |
| 349 |
app.world().resource::<Time<Virtual>>().max_delta(), |
| 350 |
DEFAULT_MAX_DELTA |
| 351 |
); |
| 352 |
} |
| 353 |
|
| 354 |
#[test] |
| 355 |
fn choosing_a_speed_while_paused_resumes() { |
| 356 |
let mut app = controls_app(); |
| 357 |
trigger(&mut app, Action::TogglePause); |
| 358 |
assert!(app.world().resource::<Time<Virtual>>().is_paused()); |
| 359 |
trigger(&mut app, Action::SetSpeed(SimSpeed::Fast)); |
| 360 |
assert!(!app.world().resource::<Time<Virtual>>().is_paused()); |
| 361 |
} |
| 362 |
} |