src/sim.rs 5.7 K · 194 lines · raw · history

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 // The simulation only moves things once per tick, but frames render more often (and at 6x
129 // speed several ticks run per frame); rendering interpolates between the last two tick
130 // positions so movement looks smooth regardless of frame rate.
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 }