src/sim.rs 5.7 K · 193 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 format_clock(tick: SimTick) -> String {
109 let hours = tick.0 / TICKS_PER_HOUR + START_HOUR;
110 let day = hours / HOURS_PER_DAY + 1;
111 let hour = hours % HOURS_PER_DAY;
112 let minute = tick.0 % TICKS_PER_HOUR * 60 / TICKS_PER_HOUR;
113 format!("Day {day}, {hour:02}:{minute:02}")
114 }
115
116 #[cfg(test)]
117 mod tests {
118 use super::*;
119
120 #[test]
121 fn clock_starts_in_the_morning_of_day_one() {
122 assert_eq!(format_clock(SimTick(0)), "Day 1, 06:00");
123 }
124
125 #[test]
126 fn clock_advances_minutes_hours_and_days() {
127 assert_eq!(format_clock(SimTick(TICKS_PER_HOUR / 2)), "Day 1, 06:30");
128 assert_eq!(format_clock(SimTick(TICKS_PER_HOUR * 18)), "Day 2, 00:00");
129 assert_eq!(
130 format_clock(SimTick(TICKS_PER_HOUR * (18 + 24 + 5))),
131 "Day 3, 05:00"
132 );
133 }
134
135 // The simulation only moves things once per tick, but frames render more often (and at 6x
136 // speed several ticks run per frame); rendering interpolates between the last two tick
137 // positions so movement looks smooth regardless of frame rate.
138 #[test]
139 fn transforms_are_interpolated_between_ticks() {
140 let mut app = App::new();
141 app.insert_resource(Time::<Fixed>::from_hz(TICKS_PER_SECOND))
142 .add_systems(Update, interpolate_transforms);
143 let entity = app
144 .world_mut()
145 .spawn((
146 SimPos(Vec2::new(10.0, 0.0)),
147 PrevSimPos(Vec2::ZERO),
148 Transform::from_xyz(0.0, 0.0, 1.0),
149 ))
150 .id();
151 let half_tick = std::time::Duration::from_secs_f64(0.5 / TICKS_PER_SECOND);
152 app.world_mut()
153 .resource_mut::<Time<Fixed>>()
154 .accumulate_overstep(half_tick);
155 app.update();
156 let translation = app.world().get::<Transform>(entity).unwrap().translation;
157 assert!(translation.abs_diff_eq(Vec3::new(5.0, 0.0, 1.0), 1e-3));
158 }
159
160 fn controls_app() -> App {
161 let mut app = App::new();
162 app.add_message::<Action>()
163 .init_resource::<Time<Virtual>>()
164 .init_resource::<SimSpeed>()
165 .add_systems(Update, speed_controls);
166 app
167 }
168
169 fn trigger(app: &mut App, action: Action) {
170 app.world_mut().write_message(action);
171 app.update();
172 }
173
174 #[test]
175 fn speed_actions_scale_virtual_time() {
176 let mut app = controls_app();
177 trigger(&mut app, Action::SetSpeed(SimSpeed::Fastest));
178 assert_eq!(*app.world().resource::<SimSpeed>(), SimSpeed::Fastest);
179 assert_eq!(
180 app.world().resource::<Time<Virtual>>().relative_speed(),
181 6.0
182 );
183 }
184
185 #[test]
186 fn choosing_a_speed_while_paused_resumes() {
187 let mut app = controls_app();
188 trigger(&mut app, Action::TogglePause);
189 assert!(app.world().resource::<Time<Virtual>>().is_paused());
190 trigger(&mut app, Action::SetSpeed(SimSpeed::Fast));
191 assert!(!app.world().resource::<Time<Virtual>>().is_paused());
192 }
193 }