summaryrefslogtreecommitdiff
path: root/src/main.rs
diff options
context:
space:
mode:
authorKai Stevenson <kai@kaistevenson.com>2026-08-16 10:53:45 -0700
committerKai Stevenson <kai@kaistevenson.com>2026-08-16 10:53:45 -0700
commit940e692d5b24f40147dc92c2dc23ce74c3d5ab7f (patch)
tree8abdd835e65f46eba1235227ed1099f71849252c /src/main.rs
parent24886c2752548f1c32ed54968a7bfdf2b1fe38ea (diff)
small refactor to update loops and timing
Diffstat (limited to 'src/main.rs')
-rw-r--r--src/main.rs307
1 files changed, 185 insertions, 122 deletions
diff --git a/src/main.rs b/src/main.rs
index 7cbf9d0..7cac1f3 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -19,7 +19,7 @@ use winit::{
use crate::{
camera::Camera,
- config::{SIM_FPS, WINDOW_TITLE},
+ config::{PHYSICS_DELTA_TIME, PHYSICS_FPS, SIM_DELTA_TIME, SIM_FPS, WINDOW_TITLE},
renderer::RendererState,
sim::{cell::Cell, materials::MaterialId, sim::sim_tick, world::World},
};
@@ -28,14 +28,14 @@ pub type Error = Box<dyn std::error::Error>;
pub type Result<T> = std::result::Result<T, Error>;
struct Config {
- brush_radius: u8,
+ brush_radius: f32,
brush_material: MaterialId,
use_threading: bool,
}
struct Input {
- last_mouse_pos_on_screen: Option<(f64, f64)>,
- last_mouse_pos_on_board: Option<(i32, i32)>,
+ last_mouse_pos_on_screen: Option<(f32, f32)>,
+ last_mouse_world_pos: Option<(f32, f32)>,
is_lmb_pressed: bool,
// keybindings
@@ -65,16 +65,82 @@ struct App {
sim_seqno: u64,
sim_paused: bool,
ignore_pause_next_tick: bool,
+ last_sim_update: Instant,
+ sim_updates_due: f32,
+
+ // physics state
+ last_physics_update: Instant,
+ physics_updates_due: f32,
- // used to compute delta_time
- last_sim_tick: Instant,
- sim_ticks_due: f32,
last_render: Instant,
config: Config,
diagnostics: Diagnostics,
}
+impl App {
+ // called as often as possible
+ // delta time is the real seconds elapsed since the last time this was called
+ fn update(&mut self, delta_time: f32) -> () {
+ // apply inputs
+ match &mut self.camera {
+ Some(camera) => camera.handle_camera_input(&self.input, delta_time),
+ _ => {}
+ }
+
+ // // --TEST DRAWING--
+ if self.input.is_lmb_pressed
+ && let Some(lm) = self.input.last_mouse_world_pos
+ {
+ // start with the bounding box of the drawing brush circle + some margin
+ // clamp the bounding box to the board sie
+ let bb_xl = (lm.0 - self.config.brush_radius).round() as i32;
+ let bb_xu = (lm.0 + self.config.brush_radius).round() as i32;
+ let bb_yl = (lm.1 - self.config.brush_radius).round() as i32;
+ let bb_yu = (lm.1 + self.config.brush_radius).round() as i32;
+
+ // for each point, check if the distance is less than the brush size and write the pixel
+ for x in bb_xl..bb_xu {
+ for y in bb_yl..bb_yu {
+ let r = random_range(0.0..1.0);
+ if ((x - lm.0.round() as i32).pow(2) + (y - lm.1.round() as i32).pow(2))
+ < (self.config.brush_radius as i32).pow(2)
+ && r > 0.9
+ {
+ let mut cell = Cell::from_material(self.config.brush_material);
+ // ensure we simulate on the first tick
+ cell.flags = (self.sim_seqno as u8) & 0b1;
+ match &mut self.world {
+ Some(world) => world.set_cell_from_game_position(
+ x, y, cell, false, // wake the chunk
+ ),
+ _ => {}
+ }
+ }
+ }
+ }
+ }
+ }
+ // called SIM_FPS times per second
+ // will be called before the render and before the physics update(s)
+ // may be called multiple times if the sim time is behind
+ // sim_delta_time is statically 1/SIM_FPS
+ fn sim_update(&mut self, sim_delta_time: f32) -> () {
+ match &mut self.world {
+ Some(world) => {
+ sim_tick(world, self.sim_seqno, self.config.use_threading);
+ self.sim_seqno += 1;
+ }
+ _ => {}
+ }
+ }
+ // called PHYSICS_FPS times per second
+ // will be called before the render
+ // may be called multiple times if the physics time is behind
+ // physics_delta_time is statically 1/PHYSICS_FPS
+ fn physics_update(&mut self, physics_delta_time: f32) -> () {}
+}
+
impl Default for App {
fn default() -> Self {
Self {
@@ -83,7 +149,7 @@ impl Default for App {
input: Input {
last_mouse_pos_on_screen: None,
- last_mouse_pos_on_board: None,
+ last_mouse_world_pos: None,
is_lmb_pressed: false,
is_up_pressed: false,
@@ -99,14 +165,17 @@ impl Default for App {
sim_seqno: 0,
sim_paused: false,
ignore_pause_next_tick: false,
+ last_sim_update: Instant::now(),
+ sim_updates_due: 0.0,
+
+ last_physics_update: Instant::now(),
+ physics_updates_due: 0.0,
- last_sim_tick: Instant::now(),
- sim_ticks_due: 0.,
last_render: Instant::now(),
config: Config {
use_threading: true,
- brush_radius: 10,
+ brush_radius: 10.0,
brush_material: MaterialId::Sand,
},
diagnostics: Diagnostics {
@@ -144,145 +213,139 @@ impl ApplicationHandler for App {
_: winit::window::WindowId,
event: WindowEvent,
) {
- if let Some(window) = &self.window
- && let Some(renderer_state) = &mut self.renderer_state
- && let Some(world) = &mut self.world
- && let Some(camera) = &mut self.camera
+ if let Some(renderer_state) = &mut self.renderer_state
+ && let Some(window) = &mut self.window
{
- let egui_response = renderer_state.egui_state.on_window_event(window, &event);
+ let egui_response = renderer_state.egui_state.on_window_event(&window, &event);
// if egui consumed the event, it means we shouldn't treat any e.g., mouse clicks
if egui_response.consumed {
return;
}
+ }
- match event {
- WindowEvent::KeyboardInput {
- event:
- KeyEvent {
- physical_key: PhysicalKey::Code(code),
- state,
- ..
- },
- ..
- } => {
- let pressed = state.is_pressed();
- match code {
- KeyCode::KeyW => self.input.is_up_pressed = pressed,
- KeyCode::KeyA => self.input.is_left_pressed = pressed,
- KeyCode::KeyS => self.input.is_down_pressed = pressed,
- KeyCode::KeyD => self.input.is_right_pressed = pressed,
- KeyCode::KeyC => self.world = Some(World::from_default_size()),
- KeyCode::Space => {
- if pressed {
- self.sim_paused = !self.sim_paused
- }
+ match event {
+ WindowEvent::KeyboardInput {
+ event:
+ KeyEvent {
+ physical_key: PhysicalKey::Code(code),
+ state,
+ ..
+ },
+ ..
+ } => {
+ let pressed = state.is_pressed();
+ match code {
+ KeyCode::KeyW => self.input.is_up_pressed = pressed,
+ KeyCode::KeyA => self.input.is_left_pressed = pressed,
+ KeyCode::KeyS => self.input.is_down_pressed = pressed,
+ KeyCode::KeyD => self.input.is_right_pressed = pressed,
+ KeyCode::KeyC => self.world = Some(World::from_default_size()),
+ KeyCode::Space => {
+ if pressed {
+ self.sim_paused = !self.sim_paused
}
- KeyCode::KeyX => {
- if pressed {
- self.ignore_pause_next_tick = true
- }
+ }
+ KeyCode::KeyX => {
+ if pressed {
+ self.ignore_pause_next_tick = true
}
- _ => {}
}
+ _ => {}
}
- WindowEvent::CursorMoved { position, .. } => {
- self.input.last_mouse_pos_on_screen = Some((position.x, position.y));
- let world_pos =
- camera.screen_position_to_world(position.x as f32, position.y as f32);
- self.input.last_mouse_pos_on_board =
- Some((world_pos.0 as i32, world_pos.1 as i32))
+ }
+ WindowEvent::CursorMoved { position, .. } => {
+ self.input.last_mouse_pos_on_screen = Some((position.x as f32, position.y as f32));
+ self.input.last_mouse_world_pos = if let Some(camera) = &mut self.camera {
+ Some(camera.screen_position_to_world(position.x as f32, position.y as f32))
+ } else {
+ None
}
- WindowEvent::MouseInput { state, button, .. } => {
- if button == MouseButton::Left {
- self.input.is_lmb_pressed = state == ElementState::Pressed
- }
+ }
+ WindowEvent::MouseInput { state, button, .. } => {
+ if button == MouseButton::Left {
+ self.input.is_lmb_pressed = state == ElementState::Pressed
}
- WindowEvent::Resized(size) => {
+ }
+ WindowEvent::Resized(size) => {
+ if let Some(renderer_state) = &mut self.renderer_state
+ && let Some(camera) = &mut self.camera
+ {
renderer_state.resize(size.width, size.height);
camera.resize((size.width as i32, size.height as i32));
}
- WindowEvent::CloseRequested => {
- event_loop.exit();
- }
- WindowEvent::RedrawRequested => {
- #[cfg(feature = "profiler")]
- puffin::GlobalProfiler::lock().new_frame();
+ }
+ WindowEvent::CloseRequested => {
+ event_loop.exit();
+ }
+ WindowEvent::RedrawRequested => {
+ #[cfg(feature = "profiler")]
+ puffin::GlobalProfiler::lock().new_frame();
- puffin::profile_scope!("redraw_requested");
- // compute FPS diagnostics
- let now = Instant::now();
- let secs_since_last_frame = (now - self.last_render).as_secs_f32();
- self.last_render = now;
+ puffin::profile_scope!("redraw_requested");
+ // compute FPS diagnostics
+ let now = Instant::now();
+ let secs_since_last_frame = (now - self.last_render).as_secs_f32();
+ self.last_render = now;
- let delta_time = secs_since_last_frame / (1.0 / 60.0);
+ let delta_time = secs_since_last_frame / (1.0 / 60.0);
- self.diagnostics
- .frame_times
- .push_back(secs_since_last_frame);
+ self.diagnostics
+ .frame_times
+ .push_back(secs_since_last_frame);
- if self.diagnostics.frame_times.len() > 30 {
- self.diagnostics.frame_times.pop_front();
- }
+ if self.diagnostics.frame_times.len() > 30 {
+ self.diagnostics.frame_times.pop_front();
+ }
- let average_frame_time = self.diagnostics.frame_times.iter().sum::<f32>()
- / self.diagnostics.frame_times.len() as f32;
+ let average_frame_time = self.diagnostics.frame_times.iter().sum::<f32>()
+ / self.diagnostics.frame_times.len() as f32;
- self.diagnostics.fps = 1.0 / average_frame_time;
+ self.diagnostics.fps = 1.0 / average_frame_time;
- // apply inputs
- camera.handle_camera_input(&self.input, delta_time);
+ // UPDATE
+ self.update(delta_time);
- // // --TEST DRAWING--
- if self.input.is_lmb_pressed
- && let Some(lm) = self.input.last_mouse_pos_on_board
- {
- // start with the bounding box of the drawing brush circle + some margin
- // clamp the bounding box to the board sie
- let bb_xl = lm.0 - self.config.brush_radius as i32;
- let bb_xu = lm.0 + self.config.brush_radius as i32;
- let bb_yl = lm.1 - self.config.brush_radius as i32;
- let bb_yu = lm.1 + self.config.brush_radius as i32;
+ // SIM UPDATE
+ let secs_since_last_sim_update = (now - self.last_sim_update).as_secs_f32();
+ let expected_secs_since_last_sim_update = 1.0 / SIM_FPS as f32;
- // for each point, check if the distance is less than the brush size and write the pixel
- for x in bb_xl..bb_xu {
- for y in bb_yl..bb_yu {
- let r = random_range(0.0..1.0);
- if ((x - lm.0).pow(2) + (y - lm.1).pow(2))
- < (self.config.brush_radius as i32).pow(2)
- && r > 0.9
- {
- let mut cell = Cell::from_material(self.config.brush_material);
- // ensure we simulate on the first tick
- cell.flags = (self.sim_seqno as u8) & 0b1;
- world.set_cell_from_game_position(
- x, y, cell, false, // wake the chunk
- );
- }
- }
- }
+ self.last_sim_update = now;
+ if self.sim_paused && self.ignore_pause_next_tick {
+ self.sim_update(SIM_DELTA_TIME);
+ self.ignore_pause_next_tick = false;
+ } else if !self.sim_paused {
+ self.sim_updates_due +=
+ secs_since_last_sim_update / expected_secs_since_last_sim_update;
+ let mut updates_done = 0;
+ // don't ever update more than 3 times per frame, or else we can get a pseudo deadlock
+ while self.sim_updates_due >= 1.0 && updates_done < 3 {
+ self.sim_update(SIM_DELTA_TIME);
+ updates_done += 1;
}
+ self.sim_updates_due -= updates_done as f32;
+ }
- // SIM logic
- let secs_since_last_tick = (now - self.last_sim_tick).as_secs_f32();
- let expected_secs_since_last_tick = 1.0 / SIM_FPS as f32;
+ // PHYSICS UPDATE
+ let secs_since_last_physics_update = (now - self.last_physics_update).as_secs_f32();
+ let expected_secs_since_last_physics_update = 1.0 / PHYSICS_FPS as f32;
- self.last_sim_tick = now;
- if self.sim_paused && self.ignore_pause_next_tick {
- sim_tick(world, self.sim_seqno, self.config.use_threading);
- self.sim_seqno += 1;
- self.ignore_pause_next_tick = false;
- } else if !self.sim_paused {
- self.sim_ticks_due += secs_since_last_tick / expected_secs_since_last_tick;
- let mut ticks_done = 0;
- // don't ever tick more than 3 times per frame, or else we can get a pseudo deadlock
- while self.sim_ticks_due >= 1.0 && ticks_done < 3 {
- sim_tick(world, self.sim_seqno, self.config.use_threading);
- self.sim_seqno += 1;
- ticks_done += 1;
- }
- self.sim_ticks_due -= ticks_done as f32;
+ self.last_physics_update = now;
+ if !self.sim_paused {
+ self.physics_updates_due +=
+ secs_since_last_physics_update / expected_secs_since_last_physics_update;
+ let mut updates_done = 0;
+ // don't ever update more than 3 times per frame, or else we can get a pseudo deadlock
+ while self.physics_updates_due >= 1.0 && updates_done < 3 {
+ self.physics_update(PHYSICS_DELTA_TIME);
+ updates_done += 1;
}
+ self.physics_updates_due -= updates_done as f32;
+ }
+ if let Some(renderer_state) = &mut self.renderer_state
+ && let Some(world) = &mut self.world
+ && let Some(camera) = &mut self.camera
+ {
renderer_state.render(
world,
camera,
@@ -291,8 +354,8 @@ impl ApplicationHandler for App {
&mut self.input,
);
}
- _ => {}
}
+ _ => {}
}
}