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