From 1a515237afb7ad09353a65f5fbc6e98a7c29ce8e Mon Sep 17 00:00:00 2001 From: Kai Stevenson Date: Sat, 22 Aug 2026 15:00:35 -0700 Subject: sim manager refactor --- src/sim/sim_manager/mod.rs | 152 +++++++++++++++++++++++++++++++++++++++++++ src/sim/sim_manager/utils.rs | 62 ++++++++++++++++++ 2 files changed, 214 insertions(+) create mode 100644 src/sim/sim_manager/mod.rs create mode 100644 src/sim/sim_manager/utils.rs (limited to 'src/sim/sim_manager') diff --git a/src/sim/sim_manager/mod.rs b/src/sim/sim_manager/mod.rs new file mode 100644 index 0000000..a98f3b2 --- /dev/null +++ b/src/sim/sim_manager/mod.rs @@ -0,0 +1,152 @@ +use std::time::Instant; + +use crate::{ + Config, + config::{PHYSICS_DELTA_TIME, PHYSICS_FPS, SIM_FPS}, + sim::{ + cell::{cell::Cell, materials::MaterialId}, + cell_manager::manager::CellManager, + particle_manager::ParticleManager, + rb_manager::RbManager, + sim_manager::utils::write_rb_entity_to_world, + }, +}; + +mod utils; + +pub struct SimManager { + // timing + pub paused: bool, + pub ignore_pause_next_tick: bool, + pub last_cell_update: Instant, + pub cell_updates_due: f32, + pub last_physics_update: Instant, + pub physics_updates_due: f32, + + // systems + pub cell_manager: CellManager, + pub rb_manager: RbManager, + pub particle_manager: ParticleManager, +} + +impl SimManager { + pub fn new() -> Self { + SimManager { + paused: false, + ignore_pause_next_tick: false, + last_cell_update: Instant::now(), + cell_updates_due: 0.0, + last_physics_update: Instant::now(), + physics_updates_due: 0.0, + cell_manager: CellManager::from_default_size(), + rb_manager: RbManager::new(), + particle_manager: ParticleManager::new(), + } + } + + fn cell_update(&mut self, config: &Config) { + // before we tick, write all the rb entities into the sim world + // TODO optimize + let entity_ids: Vec = self.rb_manager.rb_entities.keys().copied().collect(); + let mut cells_written_by_entity: Vec<(u32, Vec<(u8, u8, i32, i32)>)> = Vec::new(); + + for entity_id in entity_ids { + let cells_written = write_rb_entity_to_world(self, entity_id, self.cell_manager.seqno); + cells_written_by_entity.push((entity_id, cells_written)); + } + + self.cell_manager.tick(config.use_threading); + + // after we tick, remove the written rb cells and update the entities + // TODO optimize + for (entity_id, cells_written) in cells_written_by_entity { + let rb_entity = self.rb_manager.rb_entities.get_mut(&entity_id).unwrap(); + for (lx, ly, x, y) in cells_written { + // update the entity + // TODO we should skip cells that weren't changed? + // TODO optimize + let new_local_cell = self.cell_manager.get_cell_from_game_position(x, y).unwrap(); + if new_local_cell.material != MaterialId::Void && !new_local_cell.rb() { + panic!( + "Someone swapped into this rb's cell! ({x}, {y}, {m:#?}, {f})", + m = new_local_cell.material, + f = new_local_cell.flags + ); + } + rb_entity.set_cell_at_local_position(lx, ly, new_local_cell); + // update the world + self.cell_manager + .set_cell_from_game_position(x, y, Cell::void(), false); + } + } + } + + fn physics_update(&mut self, delta_time: f32) { + // before we move the rigidbodies, upsert the current terrain state + // TODO use the chunk sleeping, and make this range dynamic + for cx in -5..5 { + for cy in -5..5 { + if let Some(chunk) = self + .cell_manager + .chunk_position_to_chunk_idx + .get(&(cx, cy)) + .map(|&idx| &self.cell_manager.chunks[idx]) + && !chunk.sleeping + { + self.rb_manager.upsert_chunk_collider(cx, cy, chunk); + } + } + } + + // move the rbs + self.rb_manager.tick(delta_time); + + // move the particles + self.particle_manager + .tick(&mut self.cell_manager, delta_time); + } + + pub fn update(&mut self, config: &Config, _delta_time: f32) -> () { + let now = Instant::now(); + let secs_since_last_cell_update = (now - self.last_cell_update).as_secs_f32(); + let expected_secs_since_last_cell_update = 1.0 / SIM_FPS as f32; + + self.last_cell_update = now; + if self.paused && self.ignore_pause_next_tick { + self.cell_update(config); + } else if !self.paused { + self.cell_updates_due += + secs_since_last_cell_update / expected_secs_since_last_cell_update; + let mut cell_updates_done = 0; + // don't ever update more than 3 times per frame, or else we can get a pseudo deadlock + while self.cell_updates_due >= 1.0 && cell_updates_done < 3 { + self.cell_update(config); + self.cell_updates_due -= 1.0; + cell_updates_done += 1; + } + self.cell_updates_due = self.cell_updates_due.min(3.0); + } + + // 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_physics_update = now; + if self.paused && self.ignore_pause_next_tick { + self.physics_update(PHYSICS_DELTA_TIME); + } else if !self.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); + self.physics_updates_due -= 1.0; + updates_done += 1; + } + self.physics_updates_due = self.physics_updates_due.min(3.0); + } + + self.ignore_pause_next_tick = false; + } +} diff --git a/src/sim/sim_manager/utils.rs b/src/sim/sim_manager/utils.rs new file mode 100644 index 0000000..b636186 --- /dev/null +++ b/src/sim/sim_manager/utils.rs @@ -0,0 +1,62 @@ +use crate::sim::{cell::materials::MaterialId, sim_manager::SimManager}; + +pub fn write_rb_entity_to_world( + sim: &mut SimManager, + rb_entity_id: u32, + // make sure these cells will be simulated + seqno: u64, + // (entity_x, entity_y, cell_x, cell_y) +) -> Vec<(u8, u8, i32, i32)> { + let mut cells_written: Vec<(u8, u8, i32, i32)> = Vec::new(); + if let Some(rb_entity) = sim.rb_manager.rb_entities.get(&rb_entity_id) + && let Some((rb_x, rb_y, cos, sin)) = sim.rb_manager.get_rb_entity_transform(rb_entity_id) + { + let (half_size_x, half_size_y) = + (rb_entity.width as f32 / 2.0, rb_entity.height as f32 / 2.0); + // half-extent of the rotated grid's axis-aligned bounding box, plus a cell of margin + let (radius_x, radius_y) = ( + half_size_x * (cos.abs() + sin.abs()) + 1.0, + half_size_y * (cos.abs() + sin.abs()) + 1.0, + ); + + let world_xl = (rb_x - radius_x).floor() as i32; + let world_xu = (rb_x + radius_x).ceil() as i32; + let world_yl = (rb_y - radius_y).floor() as i32; + let world_yu = (rb_y + radius_y).ceil() as i32; + + for world_x in world_xl..=world_xu { + for world_y in world_yl..=world_yu { + if let Some(cur_world_cell) = sim + .cell_manager + .get_cell_from_game_position(world_x, world_y) + && cur_world_cell.material == MaterialId::Void + { + // same as shader + let d = (world_x as f32 + 0.5 - rb_x, world_y as f32 + 0.5 - rb_y); + let q = (d.0.floor() + 0.5, d.1.floor() + 0.5); + let (lx, ly) = ( + (q.0 * cos + q.1 * sin + half_size_x).floor() as i32, + (-q.0 * sin + q.1 * cos + half_size_y).floor() as i32, + ); + + if lx < 0 || ly < 0 || lx >= rb_entity.width || ly >= rb_entity.height { + continue; + } + + let mut cell = rb_entity.get_cell_at_local_position(lx as u8, ly as u8); + if cell.material == MaterialId::Void { + continue; + } + + cell.match_parity(seqno); + + // TODO: OPTIMIZE!! + sim.cell_manager + .set_cell_from_game_position(world_x, world_y, cell, false); + cells_written.push((lx as u8, ly as u8, world_x, world_y)); + } + } + } + } + cells_written +} -- cgit v1.3.1