src/sim.rs 6.1 K · 204 lines · raw · history

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