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/cell/materials/fire.rs | 2 +- src/sim/cell/materials/gas.rs | 2 +- src/sim/cell/materials/liquid.rs | 2 +- src/sim/cell/materials/mod.rs | 2 +- src/sim/cell/materials/powder.rs | 2 +- src/sim/cell_manager/chunk.rs | 47 +++++ src/sim/cell_manager/manager.rs | 76 ++++++++ src/sim/cell_manager/mod.rs | 3 + src/sim/cell_manager/sim.rs | 337 +++++++++++++++++++++++++++++++++++ src/sim/cell_sim/chunk.rs | 47 ----- src/sim/cell_sim/mod.rs | 3 - src/sim/cell_sim/sim.rs | 337 ----------------------------------- src/sim/cell_sim/world.rs | 69 ------- src/sim/entity/mod.rs | 28 +++ src/sim/lib/force.rs | 24 ++- src/sim/mod.rs | 69 +------ src/sim/particle_manager/mod.rs | 85 +++++++++ src/sim/particle_manager/particle.rs | 21 +++ src/sim/particle_sim/mod.rs | 85 --------- src/sim/particle_sim/particle.rs | 21 --- src/sim/rb_manager/debug_ops.rs | 48 +++++ src/sim/rb_manager/debug_render.rs | 71 ++++++++ src/sim/rb_manager/mod.rs | 289 ++++++++++++++++++++++++++++++ src/sim/rb_manager/rb_entity.rs | 39 ++++ src/sim/rb_sim/debug_ops.rs | 48 ----- src/sim/rb_sim/debug_render.rs | 71 -------- src/sim/rb_sim/mod.rs | 289 ------------------------------ src/sim/rb_sim/rb_entity.rs | 39 ---- src/sim/sim_manager/mod.rs | 152 ++++++++++++++++ src/sim/sim_manager/utils.rs | 62 +++++++ 30 files changed, 1279 insertions(+), 1091 deletions(-) create mode 100644 src/sim/cell_manager/chunk.rs create mode 100644 src/sim/cell_manager/manager.rs create mode 100644 src/sim/cell_manager/mod.rs create mode 100644 src/sim/cell_manager/sim.rs delete mode 100644 src/sim/cell_sim/chunk.rs delete mode 100644 src/sim/cell_sim/mod.rs delete mode 100644 src/sim/cell_sim/sim.rs delete mode 100644 src/sim/cell_sim/world.rs create mode 100644 src/sim/entity/mod.rs create mode 100644 src/sim/particle_manager/mod.rs create mode 100644 src/sim/particle_manager/particle.rs delete mode 100644 src/sim/particle_sim/mod.rs delete mode 100644 src/sim/particle_sim/particle.rs create mode 100644 src/sim/rb_manager/debug_ops.rs create mode 100644 src/sim/rb_manager/debug_render.rs create mode 100644 src/sim/rb_manager/mod.rs create mode 100644 src/sim/rb_manager/rb_entity.rs delete mode 100644 src/sim/rb_sim/debug_ops.rs delete mode 100644 src/sim/rb_sim/debug_render.rs delete mode 100644 src/sim/rb_sim/mod.rs delete mode 100644 src/sim/rb_sim/rb_entity.rs create mode 100644 src/sim/sim_manager/mod.rs create mode 100644 src/sim/sim_manager/utils.rs (limited to 'src/sim') diff --git a/src/sim/cell/materials/fire.rs b/src/sim/cell/materials/fire.rs index 8667cea..8cabe30 100644 --- a/src/sim/cell/materials/fire.rs +++ b/src/sim/cell/materials/fire.rs @@ -2,7 +2,7 @@ use rand::RngExt; use crate::sim::{ cell::{cell::Cell, materials::MaterialId}, - cell_sim::sim::{PostUpdateAction, UpdateCtx}, + cell_manager::sim::{PostUpdateAction, UpdateCtx}, }; pub trait FireCellView { diff --git a/src/sim/cell/materials/gas.rs b/src/sim/cell/materials/gas.rs index af8f4fd..229d0e9 100644 --- a/src/sim/cell/materials/gas.rs +++ b/src/sim/cell/materials/gas.rs @@ -1,6 +1,6 @@ use rand::RngExt; -use crate::sim::cell_sim::sim::{PostUpdateAction, UpdateCtx}; +use crate::sim::cell_manager::sim::{PostUpdateAction, UpdateCtx}; #[inline] pub fn sim_update(ctx: &mut UpdateCtx) -> PostUpdateAction { diff --git a/src/sim/cell/materials/liquid.rs b/src/sim/cell/materials/liquid.rs index bff0ac0..6c3bdcd 100644 --- a/src/sim/cell/materials/liquid.rs +++ b/src/sim/cell/materials/liquid.rs @@ -1,4 +1,4 @@ -use crate::sim::cell_sim::sim::{PostUpdateAction, UpdateCtx}; +use crate::sim::cell_manager::sim::{PostUpdateAction, UpdateCtx}; #[inline] pub fn sim_update(ctx: &mut UpdateCtx) -> PostUpdateAction { diff --git a/src/sim/cell/materials/mod.rs b/src/sim/cell/materials/mod.rs index ee2b4d0..5439cca 100644 --- a/src/sim/cell/materials/mod.rs +++ b/src/sim/cell/materials/mod.rs @@ -1,4 +1,4 @@ -use crate::sim::cell_sim::sim::{PostUpdateAction, UpdateCtx}; +use crate::sim::cell_manager::sim::{PostUpdateAction, UpdateCtx}; pub mod fire; pub mod gas; diff --git a/src/sim/cell/materials/powder.rs b/src/sim/cell/materials/powder.rs index e08ea1f..8c916e4 100644 --- a/src/sim/cell/materials/powder.rs +++ b/src/sim/cell/materials/powder.rs @@ -1,4 +1,4 @@ -use crate::sim::cell_sim::sim::{PostUpdateAction, UpdateCtx}; +use crate::sim::cell_manager::sim::{PostUpdateAction, UpdateCtx}; #[inline] pub fn sim_update(ctx: &mut UpdateCtx) -> PostUpdateAction { diff --git a/src/sim/cell_manager/chunk.rs b/src/sim/cell_manager/chunk.rs new file mode 100644 index 0000000..3fe5056 --- /dev/null +++ b/src/sim/cell_manager/chunk.rs @@ -0,0 +1,47 @@ +use crate::{ + config::{CELLS_IN_CHUNK, CHUNK_SIZE}, + sim::{ + cell::{cell::Cell, materials::MaterialForm}, + lib::marching_squares::Marchable, + }, +}; + +pub struct Chunk { + pub cells: Box<[Cell; CELLS_IN_CHUNK]>, + pub sleeping: bool, + pub needs_texture_update: bool, +} + +impl Chunk { + #[inline] + pub fn get_cell_at_local_position(&self, x: u8, y: u8) -> Cell { + self.cells[x as usize + y as usize * CHUNK_SIZE as usize] + } + #[inline] + pub fn set_cell_at_local_position(&mut self, x: u8, y: u8, cell: Cell) { + self.cells[x as usize + y as usize * CHUNK_SIZE as usize] = cell; + } + + pub fn void() -> Self { + Chunk { + cells: Box::new([Cell::void(); CELLS_IN_CHUNK]), + sleeping: true, + needs_texture_update: true, + } + } +} + +impl Marchable for Chunk { + fn occupied(&self, x: i32, y: i32) -> bool { + if x < 0 || x >= CHUNK_SIZE || y < 0 || y >= CHUNK_SIZE { + false + } else { + // we only build a path for solid cells or settled powder cells + let cell = self.get_cell_at_local_position(x as u8, y as u8); + cell.material.def().form == MaterialForm::Solid || cell.settled() > 4 + } + } + fn size(&self) -> (i32, i32) { + (CHUNK_SIZE, CHUNK_SIZE) + } +} diff --git a/src/sim/cell_manager/manager.rs b/src/sim/cell_manager/manager.rs new file mode 100644 index 0000000..094f345 --- /dev/null +++ b/src/sim/cell_manager/manager.rs @@ -0,0 +1,76 @@ +use fxhash::FxHashMap; + +use crate::{ + config::CHUNK_SIZE, + sim::{ + cell::cell::Cell, + cell_manager::{chunk::Chunk, sim::sim_tick}, + }, +}; + +pub struct CellManager { + pub seqno: u64, + pub chunks: Vec, + // TODO FxFxHashMap? + pub chunk_position_to_chunk_idx: FxHashMap<(i32, i32), usize>, +} + +impl CellManager { + #[inline] + pub fn split_game_position(x: i32, y: i32) -> ((i32, i32), (u8, u8)) { + ( + (x.div_euclid(CHUNK_SIZE), y.div_euclid(CHUNK_SIZE)), + ( + x.rem_euclid(CHUNK_SIZE) as u8, + y.rem_euclid(CHUNK_SIZE) as u8, + ), + ) + } + + // VERY EXPENSIVE + pub fn get_cell_from_game_position(&self, x: i32, y: i32) -> Option { + let ((cx, cy), (dx, dy)) = CellManager::split_game_position(x, y); + self.chunk_position_to_chunk_idx + .get(&(cx, cy)) + .map(|&idx| self.chunks[idx].get_cell_at_local_position(dx, dy)) + } + + // VERY EXPENSIVE + pub fn set_cell_from_game_position(&mut self, x: i32, y: i32, cell: Cell, sleeping: bool) { + let ((cx, cy), (dx, dy)) = CellManager::split_game_position(x, y); + if let Some(&idx) = self.chunk_position_to_chunk_idx.get(&(cx, cy)) { + self.chunks[idx].set_cell_at_local_position(dx, dy, cell); + // this is a temporary hack + self.chunks[idx].sleeping = sleeping; + self.chunks[idx].needs_texture_update = true; + } + } + + pub fn insert(&mut self, x: i32, y: i32, chunk: Chunk) { + self.chunk_position_to_chunk_idx + .insert((x, y), self.chunks.len()); + self.chunks.push(chunk); + } + + pub fn tick(&mut self, use_threading: bool) { + let seqno = self.seqno; + sim_tick(self, seqno, use_threading); + self.seqno += 1; + } + + pub fn from_default_size() -> Self { + let mut world = CellManager { + seqno: 0, + chunks: Vec::new(), + chunk_position_to_chunk_idx: FxHashMap::default(), + }; + + for y in -10..1 { + for x in -100..100 { + world.insert(x, y, Chunk::void()); + } + } + + world + } +} diff --git a/src/sim/cell_manager/mod.rs b/src/sim/cell_manager/mod.rs new file mode 100644 index 0000000..67d7614 --- /dev/null +++ b/src/sim/cell_manager/mod.rs @@ -0,0 +1,3 @@ +pub mod chunk; +pub mod manager; +pub mod sim; diff --git a/src/sim/cell_manager/sim.rs b/src/sim/cell_manager/sim.rs new file mode 100644 index 0000000..b79608e --- /dev/null +++ b/src/sim/cell_manager/sim.rs @@ -0,0 +1,337 @@ +use std::marker::PhantomData; + +use fxhash::FxHashMap; +use rand::{Rng, SeedableRng, rngs::SmallRng}; +use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; + +use crate::{ + config::{CHUNK_SIZE, SETTLED_THRESOHLD}, + sim::{ + cell::{cell::Cell, materials::MaterialDef}, + cell_manager::{chunk::Chunk, manager::CellManager}, + }, +}; + +struct ChunkAccess<'a> { + ptr: *mut Chunk, + len: usize, + _marker: PhantomData<&'a mut [Chunk]>, +} + +impl<'a> ChunkAccess<'a> { + pub fn new(chunks: &'a mut [Chunk]) -> Self { + Self { + ptr: chunks.as_mut_ptr(), + len: chunks.len(), + _marker: PhantomData, + } + } + unsafe fn get(&self, i: usize) -> &'a mut Chunk { + debug_assert!(i < self.len); + unsafe { &mut *self.ptr.add(i) } + } +} + +unsafe impl Sync for ChunkAccess<'_> {} + +const NEIGHBORHOOD_OFFSETS: [(i32, i32); 9] = [ + (-1, -1), + (0, -1), + (1, -1), + (-1, 0), + (0, 0), + (1, 0), + (-1, 1), + (0, 1), + (1, 1), +]; + +#[inline] +fn neighbourhood_index(x: i8, y: i8) -> usize { + (x + 1 + (y + 1) * 3) as usize +} + +fn get_cell(chunks: &[Option<&mut Chunk>; 9], x: i32, y: i32) -> Option { + let dcx = x.div_euclid(CHUNK_SIZE); + let dcy = y.div_euclid(CHUNK_SIZE); + if dcx != 0 || dcy != 0 { + // in a different chunk + let nc_x = x.rem_euclid(CHUNK_SIZE) as u8; + let nc_y = y.rem_euclid(CHUNK_SIZE) as u8; + + chunks[neighbourhood_index(dcx as i8, dcy as i8)] + .as_ref() + .map(|chunk| chunk.get_cell_at_local_position(nc_x, nc_y)) + } else { + chunks[4] + .as_ref() + .map(|target| target.get_cell_at_local_position(x as u8, y as u8)) + } +} + +fn adjacent_chunks(x: u8, y: u8) -> Vec { + if x == 0 { + if y == 0 { + vec![ + // L + neighbourhood_index(-1, 0), + // U + neighbourhood_index(0, -1), + // LU + neighbourhood_index(-1, -1), + ] + } else if y == (CHUNK_SIZE - 1) as u8 { + vec![ + // L + neighbourhood_index(-1, 0), + // D + neighbourhood_index(0, 1), + // LD + neighbourhood_index(-1, 1), + ] + } else { + // L + vec![neighbourhood_index(-1, 0)] + } + } else if x == (CHUNK_SIZE - 1) as u8 { + if y == 0 { + vec![ + // R + neighbourhood_index(1, 0), + // U + neighbourhood_index(0, -1), + // RU + neighbourhood_index(1, -1), + ] + } else if y == (CHUNK_SIZE - 1) as u8 { + vec![ + // R + neighbourhood_index(1, 0), + // D + neighbourhood_index(0, 1), + // RD + neighbourhood_index(1, 1), + ] + } else { + // R + vec![neighbourhood_index(1, 0)] + } + } else if y == 0 { + // U + vec![neighbourhood_index(0, -1)] + } else if y == (CHUNK_SIZE - 1) as u8 { + // D + vec![neighbourhood_index(0, 1)] + } else { + vec![] + } +} + +fn internal_set_cell(chunks: &mut [Option<&mut Chunk>; 9], x: i32, y: i32, cell: Cell) { + let cx = x.div_euclid(CHUNK_SIZE); + let cy = y.div_euclid(CHUNK_SIZE); + let lx = x.rem_euclid(CHUNK_SIZE) as u8; + let ly = y.rem_euclid(CHUNK_SIZE) as u8; + if cx != 0 || cy != 0 { + // in a different chunk + if let Some(chunk) = &mut chunks[neighbourhood_index(cx as i8, cy as i8)] { + chunk.set_cell_at_local_position(lx, ly, cell); + chunk.needs_texture_update = true; + chunk.sleeping = false; + // if we're at the boundaries of the chunk, wake the adjacent chunk(s) + for idx in adjacent_chunks(lx, ly) { + if let Some(chunk) = chunks[idx].as_mut() { + chunk.sleeping = false; + } + } + } + } else { + if let Some(target) = &mut chunks[4] { + target.set_cell_at_local_position(x as u8, y as u8, cell); + target.needs_texture_update = true; + target.sleeping = false; + // if we're at the boundaries of the chunk, wake the adjacent chunk(s) + for idx in adjacent_chunks(lx, ly) { + if let Some(chunk) = chunks[idx].as_mut() { + chunk.sleeping = false; + } + } + } + } +} + +#[derive(PartialEq, Eq, Clone, Copy)] +// the action that the cell's update fn took +pub enum PostUpdateAction { + // the cell managed its own lifecycle, the updater will take no action + None, + // the updater should rewrite this cell, settling it, and let the chunk sleep if it's fully settled + // this cell's parity should be flipped if it is not yet settled + Settle, + // the cell changed itself, the updater should unconditionally rewrite it and not settle the cell or sleep + Apply, +} + +pub struct UpdateCtx<'a, 'b, 'c> { + pub chunks: &'a mut [Option<&'b mut Chunk>; 9], + pub seqno: u64, + pub seqno_parity: u8, + + pub x: i32, + pub y: i32, + pub cell: &'c mut Cell, + pub material: &'c MaterialDef, + + pub rng: &'c mut dyn Rng, +} + +impl UpdateCtx<'_, '_, '_> { + pub fn get_cell(&self, dx: i32, dy: i32) -> Option { + let x = self.x + dx; + let y = self.y + dy; + get_cell(self.chunks, x, y) + } + + pub fn set_cell(&mut self, dx: i32, dy: i32, cell: Cell) { + // cannot move out of the neighbourhood, but also cannot move to the edge of the neighbourhood + // as this would wake a chunk outside of the neighbourhood + debug_assert!(dx > -CHUNK_SIZE + 1 && dx < CHUNK_SIZE - 1); + debug_assert!(dy > -CHUNK_SIZE + 1 && dy < CHUNK_SIZE - 1); + let x = self.x + dx; + let y = self.y + dy; + internal_set_cell(self.chunks, x, y, cell); + } + + pub fn swap_or_settle(&mut self, candidates: &[(i32, i32)]) -> PostUpdateAction { + for &(dx, dy) in candidates { + if let Some(mut candidate_cell) = self.get_cell(dx, dy) + && candidate_cell.material.def().density < self.material.density + { + candidate_cell.reset_settled(); + self.cell.reset_settled(); + self.cell.match_parity(self.seqno + 1); + + self.set_cell(0, 0, candidate_cell); + self.set_cell(dx, dy, *self.cell); + return PostUpdateAction::None; + } + } + PostUpdateAction::Settle + } +} + +fn sim_tick_chunk(chunks: &mut [Option<&mut Chunk>; 9], seqno: u64) { + puffin::profile_function!(); + let seqno_parity = (seqno as u8) & 0b1; + let mut rng = SmallRng::seed_from_u64(seqno); + + if chunks[4].is_some() { + for y in (0..CHUNK_SIZE).rev() { + for i in 0..CHUNK_SIZE { + let x = if seqno_parity == 0 { + i + } else { + (CHUNK_SIZE) - i - 1 + }; + + let mut cell = get_cell(chunks, x, y).unwrap(); + let material = cell.material.def(); + + if let Some(update) = material.sim_update + // NOTE we only update parity when cells are updated, which means that static cells + // are only evaluated every other tick + // it also means that the settled counter increases every other tick + && cell.parity() == seqno_parity + { + let mut update_ctx = UpdateCtx { + chunks, + seqno, + seqno_parity, + + x, + y, + cell: &mut cell, + material, + + // TODO this is platform-dependent, will break for multiplayer + rng: &mut rng, + }; + + let action = update(&mut update_ctx); + + match action { + PostUpdateAction::None => {} + PostUpdateAction::Settle => { + // the cell didn't move, so it's more settled, and we also need to update its state for parity + if cell.settled() < SETTLED_THRESOHLD { + cell.match_parity(seqno + 1); + cell.increment_settled(); + internal_set_cell(chunks, x, y, cell); + } + } + PostUpdateAction::Apply => { + cell.match_parity(seqno + 1); + internal_set_cell(chunks, x, y, cell); + } + } + } + } + } + } +} + +pub fn sim_tick(world: &mut CellManager, seqno: u64, use_threading: bool) { + puffin::profile_function!(); + + let mut columns: FxHashMap> = FxHashMap::default(); + for &(cx, cy) in world.chunk_position_to_chunk_idx.keys() { + columns.entry(cx).or_default().push(cy); + } + + // color columns s.t. columns of same color are separated by two columns + // and sort the column bottom-to-top + // -------------------- + // | 0, 1, 2, 0, 1, 2 | + // | 0, 1, 2, 0, 1, 2 | + // | 0, 1, 2, 0, 1, 2 | + // -------------------- + let mut columns_by_color: [Vec<(i32, Vec)>; 3] = Default::default(); + for (cx, mut cys) in columns { + cys.sort_unstable_by(|a, b| b.cmp(a)); + columns_by_color[cx.rem_euclid(3) as usize].push((cx, cys)); + } + + let access = ChunkAccess::new(&mut world.chunks); + + for color in &columns_by_color { + puffin::profile_scope!("chunk_color"); + + let chunk_closure = |(cx, cys): &(i32, Vec)| { + let cx = *cx; + for &cy in cys { + let mut chunks: [Option<&mut Chunk>; 9] = NEIGHBORHOOD_OFFSETS.map(|(dx, dy)| { + world + .chunk_position_to_chunk_idx + .get(&(cx + dx, cy + dy)) + .map(|&idx| unsafe { access.get(idx) }) + }); + + if let Some(target) = &mut chunks[4] { + if target.sleeping { + continue; + } + target.sleeping = true; + } + + sim_tick_chunk(&mut chunks, seqno); + } + }; + + if use_threading { + // TODO use forte + color.par_iter().for_each(chunk_closure); + } else { + color.iter().for_each(chunk_closure); + }; + } +} diff --git a/src/sim/cell_sim/chunk.rs b/src/sim/cell_sim/chunk.rs deleted file mode 100644 index 3fe5056..0000000 --- a/src/sim/cell_sim/chunk.rs +++ /dev/null @@ -1,47 +0,0 @@ -use crate::{ - config::{CELLS_IN_CHUNK, CHUNK_SIZE}, - sim::{ - cell::{cell::Cell, materials::MaterialForm}, - lib::marching_squares::Marchable, - }, -}; - -pub struct Chunk { - pub cells: Box<[Cell; CELLS_IN_CHUNK]>, - pub sleeping: bool, - pub needs_texture_update: bool, -} - -impl Chunk { - #[inline] - pub fn get_cell_at_local_position(&self, x: u8, y: u8) -> Cell { - self.cells[x as usize + y as usize * CHUNK_SIZE as usize] - } - #[inline] - pub fn set_cell_at_local_position(&mut self, x: u8, y: u8, cell: Cell) { - self.cells[x as usize + y as usize * CHUNK_SIZE as usize] = cell; - } - - pub fn void() -> Self { - Chunk { - cells: Box::new([Cell::void(); CELLS_IN_CHUNK]), - sleeping: true, - needs_texture_update: true, - } - } -} - -impl Marchable for Chunk { - fn occupied(&self, x: i32, y: i32) -> bool { - if x < 0 || x >= CHUNK_SIZE || y < 0 || y >= CHUNK_SIZE { - false - } else { - // we only build a path for solid cells or settled powder cells - let cell = self.get_cell_at_local_position(x as u8, y as u8); - cell.material.def().form == MaterialForm::Solid || cell.settled() > 4 - } - } - fn size(&self) -> (i32, i32) { - (CHUNK_SIZE, CHUNK_SIZE) - } -} diff --git a/src/sim/cell_sim/mod.rs b/src/sim/cell_sim/mod.rs deleted file mode 100644 index b7d0597..0000000 --- a/src/sim/cell_sim/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod chunk; -pub mod sim; -pub mod world; diff --git a/src/sim/cell_sim/sim.rs b/src/sim/cell_sim/sim.rs deleted file mode 100644 index fbaf2f0..0000000 --- a/src/sim/cell_sim/sim.rs +++ /dev/null @@ -1,337 +0,0 @@ -use std::marker::PhantomData; - -use fxhash::FxHashMap; -use rand::{Rng, SeedableRng, rngs::SmallRng}; -use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; - -use crate::{ - config::{CHUNK_SIZE, SETTLED_THRESOHLD}, - sim::{ - cell::{cell::Cell, materials::MaterialDef}, - cell_sim::{chunk::Chunk, world::World}, - }, -}; - -struct ChunkAccess<'a> { - ptr: *mut Chunk, - len: usize, - _marker: PhantomData<&'a mut [Chunk]>, -} - -impl<'a> ChunkAccess<'a> { - pub fn new(chunks: &'a mut [Chunk]) -> Self { - Self { - ptr: chunks.as_mut_ptr(), - len: chunks.len(), - _marker: PhantomData, - } - } - unsafe fn get(&self, i: usize) -> &'a mut Chunk { - debug_assert!(i < self.len); - unsafe { &mut *self.ptr.add(i) } - } -} - -unsafe impl Sync for ChunkAccess<'_> {} - -const NEIGHBORHOOD_OFFSETS: [(i32, i32); 9] = [ - (-1, -1), - (0, -1), - (1, -1), - (-1, 0), - (0, 0), - (1, 0), - (-1, 1), - (0, 1), - (1, 1), -]; - -#[inline] -fn neighbourhood_index(x: i8, y: i8) -> usize { - (x + 1 + (y + 1) * 3) as usize -} - -fn get_cell(chunks: &[Option<&mut Chunk>; 9], x: i32, y: i32) -> Option { - let dcx = x.div_euclid(CHUNK_SIZE); - let dcy = y.div_euclid(CHUNK_SIZE); - if dcx != 0 || dcy != 0 { - // in a different chunk - let nc_x = x.rem_euclid(CHUNK_SIZE) as u8; - let nc_y = y.rem_euclid(CHUNK_SIZE) as u8; - - chunks[neighbourhood_index(dcx as i8, dcy as i8)] - .as_ref() - .map(|chunk| chunk.get_cell_at_local_position(nc_x, nc_y)) - } else { - chunks[4] - .as_ref() - .map(|target| target.get_cell_at_local_position(x as u8, y as u8)) - } -} - -fn adjacent_chunks(x: u8, y: u8) -> Vec { - if x == 0 { - if y == 0 { - vec![ - // L - neighbourhood_index(-1, 0), - // U - neighbourhood_index(0, -1), - // LU - neighbourhood_index(-1, -1), - ] - } else if y == (CHUNK_SIZE - 1) as u8 { - vec![ - // L - neighbourhood_index(-1, 0), - // D - neighbourhood_index(0, 1), - // LD - neighbourhood_index(-1, 1), - ] - } else { - // L - vec![neighbourhood_index(-1, 0)] - } - } else if x == (CHUNK_SIZE - 1) as u8 { - if y == 0 { - vec![ - // R - neighbourhood_index(1, 0), - // U - neighbourhood_index(0, -1), - // RU - neighbourhood_index(1, -1), - ] - } else if y == (CHUNK_SIZE - 1) as u8 { - vec![ - // R - neighbourhood_index(1, 0), - // D - neighbourhood_index(0, 1), - // RD - neighbourhood_index(1, 1), - ] - } else { - // R - vec![neighbourhood_index(1, 0)] - } - } else if y == 0 { - // U - vec![neighbourhood_index(0, -1)] - } else if y == (CHUNK_SIZE - 1) as u8 { - // D - vec![neighbourhood_index(0, 1)] - } else { - vec![] - } -} - -fn internal_set_cell(chunks: &mut [Option<&mut Chunk>; 9], x: i32, y: i32, cell: Cell) { - let cx = x.div_euclid(CHUNK_SIZE); - let cy = y.div_euclid(CHUNK_SIZE); - let lx = x.rem_euclid(CHUNK_SIZE) as u8; - let ly = y.rem_euclid(CHUNK_SIZE) as u8; - if cx != 0 || cy != 0 { - // in a different chunk - if let Some(chunk) = &mut chunks[neighbourhood_index(cx as i8, cy as i8)] { - chunk.set_cell_at_local_position(lx, ly, cell); - chunk.needs_texture_update = true; - chunk.sleeping = false; - // if we're at the boundaries of the chunk, wake the adjacent chunk(s) - for idx in adjacent_chunks(lx, ly) { - if let Some(chunk) = chunks[idx].as_mut() { - chunk.sleeping = false; - } - } - } - } else { - if let Some(target) = &mut chunks[4] { - target.set_cell_at_local_position(x as u8, y as u8, cell); - target.needs_texture_update = true; - target.sleeping = false; - // if we're at the boundaries of the chunk, wake the adjacent chunk(s) - for idx in adjacent_chunks(lx, ly) { - if let Some(chunk) = chunks[idx].as_mut() { - chunk.sleeping = false; - } - } - } - } -} - -#[derive(PartialEq, Eq, Clone, Copy)] -// the action that the cell's update fn took -pub enum PostUpdateAction { - // the cell managed its own lifecycle, the updater will take no action - None, - // the updater should rewrite this cell, settling it, and let the chunk sleep if it's fully settled - // this cell's parity should be flipped if it is not yet settled - Settle, - // the cell changed itself, the updater should unconditionally rewrite it and not settle the cell or sleep - Apply, -} - -pub struct UpdateCtx<'a, 'b, 'c> { - pub chunks: &'a mut [Option<&'b mut Chunk>; 9], - pub seqno: u64, - pub seqno_parity: u8, - - pub x: i32, - pub y: i32, - pub cell: &'c mut Cell, - pub material: &'c MaterialDef, - - pub rng: &'c mut dyn Rng, -} - -impl UpdateCtx<'_, '_, '_> { - pub fn get_cell(&self, dx: i32, dy: i32) -> Option { - let x = self.x + dx; - let y = self.y + dy; - get_cell(self.chunks, x, y) - } - - pub fn set_cell(&mut self, dx: i32, dy: i32, cell: Cell) { - // cannot move out of the neighbourhood, but also cannot move to the edge of the neighbourhood - // as this would wake a chunk outside of the neighbourhood - debug_assert!(dx > -CHUNK_SIZE + 1 && dx < CHUNK_SIZE - 1); - debug_assert!(dy > -CHUNK_SIZE + 1 && dy < CHUNK_SIZE - 1); - let x = self.x + dx; - let y = self.y + dy; - internal_set_cell(self.chunks, x, y, cell); - } - - pub fn swap_or_settle(&mut self, candidates: &[(i32, i32)]) -> PostUpdateAction { - for &(dx, dy) in candidates { - if let Some(mut candidate_cell) = self.get_cell(dx, dy) - && candidate_cell.material.def().density < self.material.density - { - candidate_cell.reset_settled(); - self.cell.reset_settled(); - self.cell.match_parity(self.seqno + 1); - - self.set_cell(0, 0, candidate_cell); - self.set_cell(dx, dy, *self.cell); - return PostUpdateAction::None; - } - } - PostUpdateAction::Settle - } -} - -pub fn sim_tick_chunk(chunks: &mut [Option<&mut Chunk>; 9], seqno: u64) { - puffin::profile_function!(); - let seqno_parity = (seqno as u8) & 0b1; - let mut rng = SmallRng::seed_from_u64(seqno); - - if chunks[4].is_some() { - for y in (0..CHUNK_SIZE).rev() { - for i in 0..CHUNK_SIZE { - let x = if seqno_parity == 0 { - i - } else { - (CHUNK_SIZE) - i - 1 - }; - - let mut cell = get_cell(chunks, x, y).unwrap(); - let material = cell.material.def(); - - if let Some(update) = material.sim_update - // NOTE we only update parity when cells are updated, which means that static cells - // are only evaluated every other tick - // it also means that the settled counter increases every other tick - && cell.parity() == seqno_parity - { - let mut update_ctx = UpdateCtx { - chunks, - seqno, - seqno_parity, - - x, - y, - cell: &mut cell, - material, - - // TODO this is platform-dependent, will break for multiplayer - rng: &mut rng, - }; - - let action = update(&mut update_ctx); - - match action { - PostUpdateAction::None => {} - PostUpdateAction::Settle => { - // the cell didn't move, so it's more settled, and we also need to update its state for parity - if cell.settled() < SETTLED_THRESOHLD { - cell.match_parity(seqno + 1); - cell.increment_settled(); - internal_set_cell(chunks, x, y, cell); - } - } - PostUpdateAction::Apply => { - cell.match_parity(seqno + 1); - internal_set_cell(chunks, x, y, cell); - } - } - } - } - } - } -} - -pub fn sim_tick(world: &mut World, seqno: u64, use_threading: bool) { - puffin::profile_function!(); - - let mut columns: FxHashMap> = FxHashMap::default(); - for &(cx, cy) in world.chunk_position_to_chunk_idx.keys() { - columns.entry(cx).or_default().push(cy); - } - - // color columns s.t. columns of same color are separated by two columns - // and sort the column bottom-to-top - // -------------------- - // | 0, 1, 2, 0, 1, 2 | - // | 0, 1, 2, 0, 1, 2 | - // | 0, 1, 2, 0, 1, 2 | - // -------------------- - let mut columns_by_color: [Vec<(i32, Vec)>; 3] = Default::default(); - for (cx, mut cys) in columns { - cys.sort_unstable_by(|a, b| b.cmp(a)); - columns_by_color[cx.rem_euclid(3) as usize].push((cx, cys)); - } - - let access = ChunkAccess::new(&mut world.chunks); - - for color in &columns_by_color { - puffin::profile_scope!("chunk_color"); - - let chunk_closure = |(cx, cys): &(i32, Vec)| { - let cx = *cx; - for &cy in cys { - let mut chunks: [Option<&mut Chunk>; 9] = NEIGHBORHOOD_OFFSETS.map(|(dx, dy)| { - world - .chunk_position_to_chunk_idx - .get(&(cx + dx, cy + dy)) - .map(|&idx| unsafe { access.get(idx) }) - }); - - if let Some(target) = &mut chunks[4] { - if target.sleeping { - continue; - } - target.sleeping = true; - } - - sim_tick_chunk(&mut chunks, seqno); - } - }; - - if use_threading { - // TODO use forte - color.par_iter().for_each(chunk_closure); - } else { - color.iter().for_each(chunk_closure); - }; - } -} diff --git a/src/sim/cell_sim/world.rs b/src/sim/cell_sim/world.rs deleted file mode 100644 index 924639a..0000000 --- a/src/sim/cell_sim/world.rs +++ /dev/null @@ -1,69 +0,0 @@ -use fxhash::FxHashMap; - -use crate::{ - config::CHUNK_SIZE, - sim::{cell::cell::Cell, cell_sim::chunk::Chunk}, -}; - -pub struct World { - pub chunks: Vec, - // TODO FxFxHashMap? - pub chunk_position_to_chunk_idx: FxHashMap<(i32, i32), usize>, -} - -impl World { - #[inline] - pub fn split_game_position(x: i32, y: i32) -> ((i32, i32), (u8, u8)) { - ( - ( - // TODO is this cast expensive? - x.div_euclid(CHUNK_SIZE), - y.div_euclid(CHUNK_SIZE), - ), - ( - x.rem_euclid(CHUNK_SIZE) as u8, - y.rem_euclid(CHUNK_SIZE) as u8, - ), - ) - } - - // VERY EXPENSIVE - pub fn get_cell_from_game_position(&self, x: i32, y: i32) -> Option { - let ((cx, cy), (dx, dy)) = World::split_game_position(x, y); - self.chunk_position_to_chunk_idx - .get(&(cx, cy)) - .map(|&idx| self.chunks[idx].get_cell_at_local_position(dx, dy)) - } - - // VERY EXPENSIVE - pub fn set_cell_from_game_position(&mut self, x: i32, y: i32, cell: Cell, sleeping: bool) { - let ((cx, cy), (dx, dy)) = World::split_game_position(x, y); - if let Some(&idx) = self.chunk_position_to_chunk_idx.get(&(cx, cy)) { - self.chunks[idx].set_cell_at_local_position(dx, dy, cell); - // this is a temporary hack - self.chunks[idx].sleeping = sleeping; - self.chunks[idx].needs_texture_update = true; - } - } - - pub fn insert(&mut self, x: i32, y: i32, chunk: Chunk) { - self.chunk_position_to_chunk_idx - .insert((x, y), self.chunks.len()); - self.chunks.push(chunk); - } - - pub fn from_default_size() -> Self { - let mut world = World { - chunks: Vec::new(), - chunk_position_to_chunk_idx: FxHashMap::default(), - }; - - for y in -10..1 { - for x in -100..100 { - world.insert(x, y, Chunk::void()); - } - } - - world - } -} diff --git a/src/sim/entity/mod.rs b/src/sim/entity/mod.rs new file mode 100644 index 0000000..ab10ea8 --- /dev/null +++ b/src/sim/entity/mod.rs @@ -0,0 +1,28 @@ +// use glam::{IVec2, Vec2}; +// use rapier2d::{dynamics::RigidBodyHandle, geometry::ColliderHandle}; + +// use crate::sim::cell::cell::Cell; + +// pub struct EntityCells { +// pub size: IVec2, +// pub cells: Vec, +// } +// pub trait EntityBehaviour { +// fn update (&mut self) -> (); +// } + +// pub struct Entity { +// pub rb_h: Option, +// pub collider_h: Option, +// pub cells: EntityCells, +// pub behaviour: +// } + +// impl Entity { +// pub fn position(&self, rbsm:) -> Vec2 { + +// } +// pub fn destroy(&mut self) -> { + +// } +// } diff --git a/src/sim/lib/force.rs b/src/sim/lib/force.rs index 50b5c70..e8f7e90 100644 --- a/src/sim/lib/force.rs +++ b/src/sim/lib/force.rs @@ -6,20 +6,16 @@ use crate::sim::{ cell::Cell, materials::{MaterialId, fire::FireCellView}, }, - cell_sim::world::World, - particle_sim::{ParticleManager, particle::Particle}, - rb_sim::RbSimManager, + particle_manager::particle::Particle, + sim_manager::SimManager, }; pub fn apply_explosion( + sim: &mut SimManager, centre: Vec2, radius: i32, force_offset: Vec2, force_multiplier: f32, - next_seqno: u64, - particle_manager: &mut ParticleManager, - world: &mut World, - rbsm: &mut RbSimManager, ) { let get_vel = |pos: Vec2| { let d = pos.distance(centre); @@ -38,17 +34,18 @@ pub fn apply_explosion( // if there's a cell, convert it to a particle let pos = Vec2::new(centre.x + x as f32, centre.y + y as f32); let ipos = pos.round().as_ivec2(); - if let Some(cell) = world.get_cell_from_game_position(ipos.x, ipos.y) { + if let Some(cell) = sim.cell_manager.get_cell_from_game_position(ipos.x, ipos.y) { let mut fire = Cell::from_material(MaterialId::Fire); fire.set_ticks_lived(300); - fire.match_parity(next_seqno); + fire.match_parity(sim.cell_manager.seqno); if cell.material == MaterialId::Void && random_range(0.0..1.0) > 0.5 { if random_range(0.0..1.0) > 0.1 { - world.set_cell_from_game_position(ipos.x, ipos.y, fire, false); + sim.cell_manager + .set_cell_from_game_position(ipos.x, ipos.y, fire, false); } else { let vel = get_vel(pos); - particle_manager.particles.push(Particle::new( + sim.particle_manager.particles.push(Particle::new( pos, vel, MaterialId::Fire, @@ -56,10 +53,11 @@ pub fn apply_explosion( )); } } else { - world.set_cell_from_game_position(ipos.x, ipos.y, fire, false); + sim.cell_manager + .set_cell_from_game_position(ipos.x, ipos.y, fire, false); if random_range(0.0..1.0) > 0.6 { let vel = get_vel(pos); - particle_manager.particles.push(Particle::new( + sim.particle_manager.particles.push(Particle::new( pos, vel, cell.material, diff --git a/src/sim/mod.rs b/src/sim/mod.rs index d9e358b..95fb2db 100644 --- a/src/sim/mod.rs +++ b/src/sim/mod.rs @@ -1,66 +1,7 @@ -use crate::sim::{cell::materials::MaterialId, cell_sim::world::World, rb_sim::RbSimManager}; - pub mod cell; -pub mod cell_sim; +pub mod cell_manager; +pub mod entity; pub mod lib; -pub mod particle_sim; -pub mod rb_sim; - -pub fn write_rb_entity_to_world( - world: &mut World, - rb_sim_manager: &RbSimManager, - 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) = rb_sim_manager.rb_entities.get(&rb_entity_id) - && let Some((rb_x, rb_y, cos, sin)) = rb_sim_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) = world.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!! - world.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 -} +pub mod particle_manager; +pub mod rb_manager; +pub mod sim_manager; diff --git a/src/sim/particle_manager/mod.rs b/src/sim/particle_manager/mod.rs new file mode 100644 index 0000000..e5e47eb --- /dev/null +++ b/src/sim/particle_manager/mod.rs @@ -0,0 +1,85 @@ +use crate::{ + config::PIXELS_TO_METRES, + sim::{ + cell::{cell::Cell, materials::MaterialForm}, + cell_manager::manager::CellManager, + lib::ray::AwDda, + particle_manager::particle::Particle, + }, +}; + +pub mod particle; + +pub struct ParticleManager { + pub particles: Vec, +} + +const PARTICLE_GRAVITY: f32 = 9.81 * PIXELS_TO_METRES; + +impl ParticleManager { + pub fn tick(&mut self, world: &mut CellManager, delta_time: f32) { + puffin::profile_function!(); + let mut i = 0; + 'outer: while i < self.particles.len() { + let p = &mut self.particles[i]; + + p.life -= delta_time; + if p.life <= 0.0 { + self.particles.swap_remove(i); + continue 'outer; + } + + p.velocity.y += PARTICLE_GRAVITY * delta_time; + let dt_velocity = p.velocity * delta_time; + + let mut dda = AwDda::new(p.position, p.position + dt_velocity); + + if let Some(mut prev) = dda.next() { + if let Some(cell) = world.get_cell_from_game_position(prev.x, prev.y) + && [ + MaterialForm::Solid, + MaterialForm::Powder, + MaterialForm::Liquid, + ] + .contains(&cell.material.def().form) + { + // the particle is already inside a collider, we should just kill it + self.particles.swap_remove(i); + continue 'outer; + } + + for next in dda { + if let Some(cell) = world.get_cell_from_game_position(next.x, next.y) + && [ + MaterialForm::Solid, + MaterialForm::Powder, + MaterialForm::Liquid, + ] + .contains(&cell.material.def().form) + { + // write ourselves to the board + world.set_cell_from_game_position( + prev.x, + prev.y, + Cell::from_material(p.material), + false, + ); + self.particles.swap_remove(i); + continue 'outer; + } + prev = next; + } + } + + // no collision, just move + p.position += dt_velocity; + i += 1; + } + } + + pub fn new() -> Self { + ParticleManager { + particles: Vec::new(), + } + } +} diff --git a/src/sim/particle_manager/particle.rs b/src/sim/particle_manager/particle.rs new file mode 100644 index 0000000..cc7dd28 --- /dev/null +++ b/src/sim/particle_manager/particle.rs @@ -0,0 +1,21 @@ +use glam::Vec2; + +use crate::sim::cell::materials::MaterialId; + +pub struct Particle { + pub position: Vec2, + pub velocity: Vec2, + pub material: MaterialId, + pub life: f32, +} + +impl Particle { + pub fn new(position: Vec2, velocity: Vec2, material: MaterialId, life: f32) -> Self { + Particle { + position, + velocity, + material, + life, + } + } +} diff --git a/src/sim/particle_sim/mod.rs b/src/sim/particle_sim/mod.rs deleted file mode 100644 index 84a728a..0000000 --- a/src/sim/particle_sim/mod.rs +++ /dev/null @@ -1,85 +0,0 @@ -use crate::{ - config::PIXELS_TO_METRES, - sim::{ - cell::{cell::Cell, materials::MaterialForm}, - cell_sim::world::World, - lib::ray::AwDda, - particle_sim::particle::Particle, - }, -}; - -pub mod particle; - -pub struct ParticleManager { - pub particles: Vec, -} - -const PARTICLE_GRAVITY: f32 = 9.81 * PIXELS_TO_METRES; - -impl ParticleManager { - pub fn particle_tick(&mut self, world: &mut World, delta_time: f32) { - puffin::profile_function!(); - let mut i = 0; - 'outer: while i < self.particles.len() { - let p = &mut self.particles[i]; - - p.life -= delta_time; - if p.life <= 0.0 { - self.particles.swap_remove(i); - continue 'outer; - } - - p.velocity.y += PARTICLE_GRAVITY * delta_time; - let dt_velocity = p.velocity * delta_time; - - let mut dda = AwDda::new(p.position, p.position + dt_velocity); - - if let Some(mut prev) = dda.next() { - if let Some(cell) = world.get_cell_from_game_position(prev.x, prev.y) - && [ - MaterialForm::Solid, - MaterialForm::Powder, - MaterialForm::Liquid, - ] - .contains(&cell.material.def().form) - { - // the particle is already inside a collider, we should just kill it - self.particles.swap_remove(i); - continue 'outer; - } - - for next in dda { - if let Some(cell) = world.get_cell_from_game_position(next.x, next.y) - && [ - MaterialForm::Solid, - MaterialForm::Powder, - MaterialForm::Liquid, - ] - .contains(&cell.material.def().form) - { - // write ourselves to the board - world.set_cell_from_game_position( - prev.x, - prev.y, - Cell::from_material(p.material), - false, - ); - self.particles.swap_remove(i); - continue 'outer; - } - prev = next; - } - } - - // no collision, just move - p.position += dt_velocity; - i += 1; - } - } - - pub fn new() -> Self { - ParticleManager { - particles: Vec::new(), - } - } -} diff --git a/src/sim/particle_sim/particle.rs b/src/sim/particle_sim/particle.rs deleted file mode 100644 index cc7dd28..0000000 --- a/src/sim/particle_sim/particle.rs +++ /dev/null @@ -1,21 +0,0 @@ -use glam::Vec2; - -use crate::sim::cell::materials::MaterialId; - -pub struct Particle { - pub position: Vec2, - pub velocity: Vec2, - pub material: MaterialId, - pub life: f32, -} - -impl Particle { - pub fn new(position: Vec2, velocity: Vec2, material: MaterialId, life: f32) -> Self { - Particle { - position, - velocity, - material, - life, - } - } -} diff --git a/src/sim/rb_manager/debug_ops.rs b/src/sim/rb_manager/debug_ops.rs new file mode 100644 index 0000000..cb56256 --- /dev/null +++ b/src/sim/rb_manager/debug_ops.rs @@ -0,0 +1,48 @@ +use glam::{ivec2, vec2}; + +use crate::sim::{ + cell::{cell::Cell, materials::MaterialId}, + rb_manager::RbManager, +}; + +pub trait DebugOperator { + fn test_spawn_box(&mut self, x: f32, y: f32, material: MaterialId) -> (); + fn test_spawn_ball(&mut self, x: f32, y: f32, material: MaterialId) -> (); +} + +impl DebugOperator for RbManager { + fn test_spawn_box(&mut self, x: f32, y: f32, material: MaterialId) { + let w = 10; + let h = 10; + let mut test_cells = vec![Cell::void(); (w * h) as usize]; + + for x in 0..w { + for y in 0..h { + let cell_idx = x + y * w; + test_cells[cell_idx as usize] = Cell::from_material(material); + test_cells[cell_idx as usize].set_rb(true); + } + } + + self.create_rb_entity(vec2(x, y), test_cells, w, h); + } + + fn test_spawn_ball(&mut self, x: f32, y: f32, material: MaterialId) { + let r = 5; + let w = r * 2; + let h = r * 2; + let mut test_cells = vec![Cell::void(); (w * h) as usize]; + + for x in 0..w { + for y in 0..h { + let cell_idx = x + y * w; + if ivec2(x, y).distance_squared(ivec2(w / 2, h / 2)) < r.pow(2) { + test_cells[cell_idx as usize] = Cell::from_material(material); + test_cells[cell_idx as usize].set_rb(true); + } + } + } + + self.create_rb_entity(vec2(x, y), test_cells, w, h); + } +} diff --git a/src/sim/rb_manager/debug_render.rs b/src/sim/rb_manager/debug_render.rs new file mode 100644 index 0000000..342fc5b --- /dev/null +++ b/src/sim/rb_manager/debug_render.rs @@ -0,0 +1,71 @@ +use rapier2d::pipeline::{DebugColor, DebugRenderBackend, DebugRenderObject}; + +use crate::config::PIXELS_TO_METRES; + +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +pub struct DebugVertex { + pub position: [f32; 2], + pub color: [f32; 4], +} + +#[derive(Default)] +pub struct DebugLineBuffer { + pub vertices: Vec, +} + +impl DebugRenderBackend for DebugLineBuffer { + fn draw_line( + &mut self, + _object: DebugRenderObject, + a: rapier2d::math::Vector, + b: rapier2d::math::Vector, + color: DebugColor, + ) { + let color = hsla_to_linear_rgba(color); + self.vertices.push(DebugVertex { + position: [a.x * PIXELS_TO_METRES, a.y * PIXELS_TO_METRES], + color, + }); + self.vertices.push(DebugVertex { + position: [b.x * PIXELS_TO_METRES, b.y * PIXELS_TO_METRES], + color, + }); + } +} + +fn hsla_to_linear_rgba(hsla: DebugColor) -> [f32; 4] { + let [hue, saturation, lightness, alpha] = hsla; + + let hue = hue.rem_euclid(360.0) / 60.0; + let saturation = saturation.clamp(0.0, 1.0); + let lightness = lightness.clamp(0.0, 1.0); + + let chroma = (1.0 - (2.0 * lightness - 1.0).abs()) * saturation; + let second = chroma * (1.0 - (hue % 2.0 - 1.0).abs()); + let (r, g, b) = match hue as u32 { + 0 => (chroma, second, 0.0), + 1 => (second, chroma, 0.0), + 2 => (0.0, chroma, second), + 3 => (0.0, second, chroma), + 4 => (second, 0.0, chroma), + _ => (chroma, 0.0, second), + }; + let m = lightness - chroma / 2.0; + + [ + srgb_to_linear(r + m), + srgb_to_linear(g + m), + srgb_to_linear(b + m), + alpha.clamp(0.0, 1.0), + ] +} + +fn srgb_to_linear(channel: f32) -> f32 { + let channel = channel.clamp(0.0, 1.0); + if channel <= 0.04045 { + channel / 12.92 + } else { + ((channel + 0.055) / 1.055).powf(2.4) + } +} diff --git a/src/sim/rb_manager/mod.rs b/src/sim/rb_manager/mod.rs new file mode 100644 index 0000000..2c64f12 --- /dev/null +++ b/src/sim/rb_manager/mod.rs @@ -0,0 +1,289 @@ +pub mod debug_ops; +pub mod debug_render; +pub mod rb_entity; + +use fxhash::FxHashMap; +use glam::Vec2; +use rapier2d::{dynamics, geometry, glamx::vec2, prelude}; + +use crate::{ + config::{CHUNK_SIZE, PHYSICS_DELTA_TIME, PIXELS_TO_METRES}, + sim::{ + cell::cell::Cell, + cell_manager::chunk::Chunk, + lib::marching_squares::{Marchable, marching_squares_vertex_trace}, + rb_manager::{ + debug_render::{DebugLineBuffer, DebugVertex}, + rb_entity::RbEntity, + }, + }, +}; + +pub use rapier2d::pipeline::DebugRenderMode; + +pub struct PhysicsManager { + rigid_body_set: prelude::RigidBodySet, + collider_set: prelude::ColliderSet, + physics_pipeline: prelude::PhysicsPipeline, + integration_parameters: prelude::IntegrationParameters, + island_manager: prelude::IslandManager, + broad_phase: prelude::DefaultBroadPhase, + narrow_phase: prelude::NarrowPhase, + impulse_joint_set: prelude::ImpulseJointSet, + multibody_joint_set: prelude::MultibodyJointSet, + ccd_solver: prelude::CCDSolver, + debug_render_pipeline: prelude::DebugRenderPipeline, +} + +impl PhysicsManager { + pub fn new() -> Self { + PhysicsManager { + rigid_body_set: prelude::RigidBodySet::new(), + collider_set: prelude::ColliderSet::new(), + physics_pipeline: prelude::PhysicsPipeline::new(), + integration_parameters: prelude::IntegrationParameters { + // 20 pixels <-> 1 meter + length_unit: PIXELS_TO_METRES, + dt: PHYSICS_DELTA_TIME, + ..prelude::IntegrationParameters::default() + }, + island_manager: prelude::IslandManager::new(), + broad_phase: prelude::DefaultBroadPhase::new(), + narrow_phase: prelude::NarrowPhase::new(), + impulse_joint_set: prelude::ImpulseJointSet::new(), + multibody_joint_set: prelude::MultibodyJointSet::new(), + ccd_solver: prelude::CCDSolver::new(), + debug_render_pipeline: prelude::DebugRenderPipeline::new( + prelude::DebugRenderStyle { + sleep_color_multiplier: [1.0; 4], + sleep_eligible_color_multiplier: [1.0; 4], + ..prelude::DebugRenderStyle::default() + }, + prelude::DebugRenderMode::default(), + ), + } + } +} + +pub struct RbManager { + chunk_colliders: FxHashMap<(i32, i32), geometry::ColliderHandle>, + pub rb_entities: FxHashMap, + + physics_manager: PhysicsManager, + next_id: u32, + debug_line_buffer: DebugLineBuffer, +} + +impl RbManager { + pub fn tick(&mut self, _delta_time: f32) { + let gravity = vec2(0.0, 9.81); + + self.physics_manager.physics_pipeline.step( + gravity, + &self.physics_manager.integration_parameters, + &mut self.physics_manager.island_manager, + &mut self.physics_manager.broad_phase, + &mut self.physics_manager.narrow_phase, + &mut self.physics_manager.rigid_body_set, + &mut self.physics_manager.collider_set, + &mut self.physics_manager.impulse_joint_set, + &mut self.physics_manager.multibody_joint_set, + &mut self.physics_manager.ccd_solver, + &(), + &(), + ); + } + + pub fn debug_render(&mut self, mode: DebugRenderMode) -> &[DebugVertex] { + puffin::profile_function!(); + + let physics = &mut self.physics_manager; + self.debug_line_buffer.vertices.clear(); + physics.debug_render_pipeline.mode = mode; + physics.debug_render_pipeline.render( + &mut self.debug_line_buffer, + &physics.rigid_body_set, + &physics.collider_set, + &physics.impulse_joint_set, + &physics.multibody_joint_set, + &physics.narrow_phase, + ); + + &self.debug_line_buffer.vertices + } + + pub fn create_rb_entity(&mut self, position: Vec2, cells: Vec, w: i32, h: i32) -> u32 { + let id = self.next_id; + self.next_id += 1; + + let rb = dynamics::RigidBodyBuilder::dynamic() + .translation(position / PIXELS_TO_METRES) + .build(); + let rb_handle = self.physics_manager.rigid_body_set.insert(rb); + + let mut entity: RbEntity = RbEntity { + id, + cells, + width: w, + height: h, + rb: rb_handle, + collider: None, + }; + + let collider = self + .convex_hull_collider_from_marchable(&entity, None) + .build(); + let collider_handle = self.physics_manager.collider_set.insert_with_parent( + collider, + rb_handle, + &mut self.physics_manager.rigid_body_set, + ); + + entity.collider = Some(collider_handle); + + self.rb_entities.insert(id, entity); + + id + } + + pub fn update_rb_entity(&mut self, entity_id: u32) { + let entity = self.rb_entities.get(&entity_id).unwrap(); + let new_collider = self + .convex_hull_collider_from_marchable(entity, None) + .build(); + + self.physics_manager.collider_set.remove( + entity.collider.unwrap(), + &mut self.physics_manager.island_manager, + &mut self.physics_manager.rigid_body_set, + false, + ); + + let new_collider_handle = self.physics_manager.collider_set.insert_with_parent( + new_collider, + entity.rb, + &mut self.physics_manager.rigid_body_set, + ); + + let entity = self.rb_entities.get_mut(&entity_id).unwrap(); + entity.collider = Some(new_collider_handle); + } + + pub fn destroy_rb_entity(&mut self, entity_id: u32) { + if let Some(entity) = self.rb_entities.get(&entity_id) { + self.physics_manager.rigid_body_set.remove( + entity.rb, + &mut self.physics_manager.island_manager, + &mut self.physics_manager.collider_set, + &mut self.physics_manager.impulse_joint_set, + &mut self.physics_manager.multibody_joint_set, + true, + ); + self.rb_entities.remove(&entity_id); + } + } + + pub fn get_rb_entity_transform(&self, entity_id: u32) -> Option<(f32, f32, f32, f32)> { + let entity = self.rb_entities.get(&entity_id); + match entity { + Some(entity) => { + let rb = self.physics_manager.rigid_body_set.get(entity.rb); + rb.map(|rb| { + let position = rb.position(); + let angle = position.rotation.angle(); + ( + position.translation.x * PIXELS_TO_METRES, + position.translation.y * PIXELS_TO_METRES, + angle.cos(), + angle.sin(), + ) + }) + } + None => return None, + } + } + + fn polyline_from_marchable( + &self, + marchable: &impl Marchable, + minimum_verts: Option, + ) -> (Vec, Vec<[u32; 2]>) { + let (w, h) = marchable.size(); + let polys = marching_squares_vertex_trace(marchable, w, h); + let mut vertices = Vec::new(); + let mut indices = Vec::new(); + + let minimum_verts = minimum_verts.unwrap_or(3); + + for poly in polys { + let v = vertices.len() as u32; + let p = poly.len() as u32; + if p < minimum_verts { + continue; + } + for i in 0..p { + vertices.push( + (poly[i as usize] - vec2((w as f32 - 1.0) / 2.0, (h as f32 - 1.0) / 2.0)) + / PIXELS_TO_METRES, + ); + indices.push([v + i, v + (i + 1) % p]); + } + } + + (vertices, indices) + } + + fn polyline_collider_from_marchable( + &self, + marchable: &impl Marchable, + minimum_verts: Option, + ) -> geometry::ColliderBuilder { + let (vertices, indices) = self.polyline_from_marchable(marchable, minimum_verts); + + geometry::ColliderBuilder::polyline(vertices, Some(indices)) + } + + fn convex_hull_collider_from_marchable( + &self, + marchable: &impl Marchable, + minimum_verts: Option, + ) -> geometry::ColliderBuilder { + let (vertices, indices) = self.polyline_from_marchable(marchable, minimum_verts); + geometry::ColliderBuilder::convex_decomposition(&vertices, &indices) + } + + pub fn upsert_chunk_collider(&mut self, cx: i32, cy: i32, chunk: &Chunk) { + puffin::profile_function!(); + let collider = self + .polyline_collider_from_marchable(chunk, Some(20)) + .translation( + vec2( + (cx as f32 + 0.5) * CHUNK_SIZE as f32, + (cy as f32 + 0.5) * CHUNK_SIZE as f32, + ) / PIXELS_TO_METRES, + ); + + if let Some(handle) = self.chunk_colliders.remove(&(cx, cy)) { + self.physics_manager.collider_set.remove( + handle, + &mut self.physics_manager.island_manager, + &mut self.physics_manager.rigid_body_set, + false, + ); + } + + let handle = self.physics_manager.collider_set.insert(collider); + self.chunk_colliders.insert((cx, cy), handle); + } + + pub fn new() -> Self { + RbManager { + rb_entities: FxHashMap::default(), + chunk_colliders: FxHashMap::default(), + + physics_manager: PhysicsManager::new(), + next_id: 0, + debug_line_buffer: DebugLineBuffer::default(), + } + } +} diff --git a/src/sim/rb_manager/rb_entity.rs b/src/sim/rb_manager/rb_entity.rs new file mode 100644 index 0000000..18a6693 --- /dev/null +++ b/src/sim/rb_manager/rb_entity.rs @@ -0,0 +1,39 @@ +use rapier2d::prelude; + +use crate::sim::{ + cell::{cell::Cell, materials::MaterialId}, + lib::marching_squares::Marchable, +}; + +pub struct RbEntity { + pub id: u32, + pub width: i32, + pub height: i32, + pub cells: Vec, + pub rb: prelude::RigidBodyHandle, + pub collider: Option, +} + +impl RbEntity { + #[inline] + pub fn get_cell_at_local_position(&self, x: u8, y: u8) -> Cell { + self.cells[x as usize + y as usize * self.width as usize] + } + #[inline] + pub fn set_cell_at_local_position(&mut self, x: u8, y: u8, cell: Cell) { + self.cells[x as usize + y as usize * self.width as usize] = cell; + } +} + +impl Marchable for RbEntity { + fn occupied(&self, x: i32, y: i32) -> bool { + if x < 0 || x >= self.width || y < 0 || y >= self.height { + false + } else { + self.get_cell_at_local_position(x as u8, y as u8).material != MaterialId::Void + } + } + fn size(&self) -> (i32, i32) { + (self.width, self.height) + } +} diff --git a/src/sim/rb_sim/debug_ops.rs b/src/sim/rb_sim/debug_ops.rs deleted file mode 100644 index d6852da..0000000 --- a/src/sim/rb_sim/debug_ops.rs +++ /dev/null @@ -1,48 +0,0 @@ -use glam::{ivec2, vec2}; - -use crate::sim::{ - cell::{cell::Cell, materials::MaterialId}, - rb_sim::RbSimManager, -}; - -pub trait DebugOperator { - fn test_spawn_box(&mut self, x: f32, y: f32, material: MaterialId) -> (); - fn test_spawn_ball(&mut self, x: f32, y: f32, material: MaterialId) -> (); -} - -impl DebugOperator for RbSimManager { - fn test_spawn_box(&mut self, x: f32, y: f32, material: MaterialId) { - let w = 10; - let h = 10; - let mut test_cells = vec![Cell::void(); (w * h) as usize]; - - for x in 0..w { - for y in 0..h { - let cell_idx = x + y * w; - test_cells[cell_idx as usize] = Cell::from_material(material); - test_cells[cell_idx as usize].set_rb(true); - } - } - - self.create_rb_entity(vec2(x, y), test_cells, w, h); - } - - fn test_spawn_ball(&mut self, x: f32, y: f32, material: MaterialId) { - let r = 5; - let w = r * 2; - let h = r * 2; - let mut test_cells = vec![Cell::void(); (w * h) as usize]; - - for x in 0..w { - for y in 0..h { - let cell_idx = x + y * w; - if ivec2(x, y).distance_squared(ivec2(w / 2, h / 2)) < r.pow(2) { - test_cells[cell_idx as usize] = Cell::from_material(material); - test_cells[cell_idx as usize].set_rb(true); - } - } - } - - self.create_rb_entity(vec2(x, y), test_cells, w, h); - } -} diff --git a/src/sim/rb_sim/debug_render.rs b/src/sim/rb_sim/debug_render.rs deleted file mode 100644 index 342fc5b..0000000 --- a/src/sim/rb_sim/debug_render.rs +++ /dev/null @@ -1,71 +0,0 @@ -use rapier2d::pipeline::{DebugColor, DebugRenderBackend, DebugRenderObject}; - -use crate::config::PIXELS_TO_METRES; - -#[repr(C)] -#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] -pub struct DebugVertex { - pub position: [f32; 2], - pub color: [f32; 4], -} - -#[derive(Default)] -pub struct DebugLineBuffer { - pub vertices: Vec, -} - -impl DebugRenderBackend for DebugLineBuffer { - fn draw_line( - &mut self, - _object: DebugRenderObject, - a: rapier2d::math::Vector, - b: rapier2d::math::Vector, - color: DebugColor, - ) { - let color = hsla_to_linear_rgba(color); - self.vertices.push(DebugVertex { - position: [a.x * PIXELS_TO_METRES, a.y * PIXELS_TO_METRES], - color, - }); - self.vertices.push(DebugVertex { - position: [b.x * PIXELS_TO_METRES, b.y * PIXELS_TO_METRES], - color, - }); - } -} - -fn hsla_to_linear_rgba(hsla: DebugColor) -> [f32; 4] { - let [hue, saturation, lightness, alpha] = hsla; - - let hue = hue.rem_euclid(360.0) / 60.0; - let saturation = saturation.clamp(0.0, 1.0); - let lightness = lightness.clamp(0.0, 1.0); - - let chroma = (1.0 - (2.0 * lightness - 1.0).abs()) * saturation; - let second = chroma * (1.0 - (hue % 2.0 - 1.0).abs()); - let (r, g, b) = match hue as u32 { - 0 => (chroma, second, 0.0), - 1 => (second, chroma, 0.0), - 2 => (0.0, chroma, second), - 3 => (0.0, second, chroma), - 4 => (second, 0.0, chroma), - _ => (chroma, 0.0, second), - }; - let m = lightness - chroma / 2.0; - - [ - srgb_to_linear(r + m), - srgb_to_linear(g + m), - srgb_to_linear(b + m), - alpha.clamp(0.0, 1.0), - ] -} - -fn srgb_to_linear(channel: f32) -> f32 { - let channel = channel.clamp(0.0, 1.0); - if channel <= 0.04045 { - channel / 12.92 - } else { - ((channel + 0.055) / 1.055).powf(2.4) - } -} diff --git a/src/sim/rb_sim/mod.rs b/src/sim/rb_sim/mod.rs deleted file mode 100644 index f81d5f1..0000000 --- a/src/sim/rb_sim/mod.rs +++ /dev/null @@ -1,289 +0,0 @@ -pub mod debug_ops; -pub mod debug_render; -pub mod rb_entity; - -use fxhash::FxHashMap; -use glam::Vec2; -use rapier2d::{dynamics, geometry, glamx::vec2, prelude}; - -use crate::{ - config::{CHUNK_SIZE, PHYSICS_DELTA_TIME, PIXELS_TO_METRES}, - sim::{ - cell::cell::Cell, - cell_sim::chunk::Chunk, - lib::marching_squares::{Marchable, marching_squares_vertex_trace}, - rb_sim::{ - debug_render::{DebugLineBuffer, DebugVertex}, - rb_entity::RbEntity, - }, - }, -}; - -pub use rapier2d::pipeline::DebugRenderMode; - -pub struct PhysicsManager { - rigid_body_set: prelude::RigidBodySet, - collider_set: prelude::ColliderSet, - physics_pipeline: prelude::PhysicsPipeline, - integration_parameters: prelude::IntegrationParameters, - island_manager: prelude::IslandManager, - broad_phase: prelude::DefaultBroadPhase, - narrow_phase: prelude::NarrowPhase, - impulse_joint_set: prelude::ImpulseJointSet, - multibody_joint_set: prelude::MultibodyJointSet, - ccd_solver: prelude::CCDSolver, - debug_render_pipeline: prelude::DebugRenderPipeline, -} - -impl PhysicsManager { - pub fn new() -> Self { - PhysicsManager { - rigid_body_set: prelude::RigidBodySet::new(), - collider_set: prelude::ColliderSet::new(), - physics_pipeline: prelude::PhysicsPipeline::new(), - integration_parameters: prelude::IntegrationParameters { - // 20 pixels <-> 1 meter - length_unit: PIXELS_TO_METRES, - dt: PHYSICS_DELTA_TIME, - ..prelude::IntegrationParameters::default() - }, - island_manager: prelude::IslandManager::new(), - broad_phase: prelude::DefaultBroadPhase::new(), - narrow_phase: prelude::NarrowPhase::new(), - impulse_joint_set: prelude::ImpulseJointSet::new(), - multibody_joint_set: prelude::MultibodyJointSet::new(), - ccd_solver: prelude::CCDSolver::new(), - debug_render_pipeline: prelude::DebugRenderPipeline::new( - prelude::DebugRenderStyle { - sleep_color_multiplier: [1.0; 4], - sleep_eligible_color_multiplier: [1.0; 4], - ..prelude::DebugRenderStyle::default() - }, - prelude::DebugRenderMode::default(), - ), - } - } -} - -pub struct RbSimManager { - chunk_colliders: FxHashMap<(i32, i32), geometry::ColliderHandle>, - pub rb_entities: FxHashMap, - - physics_manager: PhysicsManager, - next_id: u32, - debug_line_buffer: DebugLineBuffer, -} - -impl RbSimManager { - pub fn rb_tick(&mut self, delta_time: f32) { - let gravity = vec2(0.0, 9.81); - - self.physics_manager.physics_pipeline.step( - gravity, - &self.physics_manager.integration_parameters, - &mut self.physics_manager.island_manager, - &mut self.physics_manager.broad_phase, - &mut self.physics_manager.narrow_phase, - &mut self.physics_manager.rigid_body_set, - &mut self.physics_manager.collider_set, - &mut self.physics_manager.impulse_joint_set, - &mut self.physics_manager.multibody_joint_set, - &mut self.physics_manager.ccd_solver, - &(), - &(), - ); - } - - pub fn debug_render(&mut self, mode: DebugRenderMode) -> &[DebugVertex] { - puffin::profile_function!(); - - let physics = &mut self.physics_manager; - self.debug_line_buffer.vertices.clear(); - physics.debug_render_pipeline.mode = mode; - physics.debug_render_pipeline.render( - &mut self.debug_line_buffer, - &physics.rigid_body_set, - &physics.collider_set, - &physics.impulse_joint_set, - &physics.multibody_joint_set, - &physics.narrow_phase, - ); - - &self.debug_line_buffer.vertices - } - - pub fn create_rb_entity(&mut self, position: Vec2, cells: Vec, w: i32, h: i32) -> u32 { - let id = self.next_id; - self.next_id += 1; - - let rb = dynamics::RigidBodyBuilder::dynamic() - .translation(position / PIXELS_TO_METRES) - .build(); - let rb_handle = self.physics_manager.rigid_body_set.insert(rb); - - let mut entity: RbEntity = RbEntity { - id, - cells, - width: w, - height: h, - rb: rb_handle, - collider: None, - }; - - let collider = self - .convex_hull_collider_from_marchable(&entity, None) - .build(); - let collider_handle = self.physics_manager.collider_set.insert_with_parent( - collider, - rb_handle, - &mut self.physics_manager.rigid_body_set, - ); - - entity.collider = Some(collider_handle); - - self.rb_entities.insert(id, entity); - - id - } - - pub fn update_rb_entity(&mut self, entity_id: u32) { - let entity = self.rb_entities.get(&entity_id).unwrap(); - let new_collider = self - .convex_hull_collider_from_marchable(entity, None) - .build(); - - self.physics_manager.collider_set.remove( - entity.collider.unwrap(), - &mut self.physics_manager.island_manager, - &mut self.physics_manager.rigid_body_set, - false, - ); - - let new_collider_handle = self.physics_manager.collider_set.insert_with_parent( - new_collider, - entity.rb, - &mut self.physics_manager.rigid_body_set, - ); - - let entity = self.rb_entities.get_mut(&entity_id).unwrap(); - entity.collider = Some(new_collider_handle); - } - - pub fn destroy_rb_entity(&mut self, entity_id: u32) { - if let Some(entity) = self.rb_entities.get(&entity_id) { - self.physics_manager.rigid_body_set.remove( - entity.rb, - &mut self.physics_manager.island_manager, - &mut self.physics_manager.collider_set, - &mut self.physics_manager.impulse_joint_set, - &mut self.physics_manager.multibody_joint_set, - true, - ); - self.rb_entities.remove(&entity_id); - } - } - - pub fn get_rb_entity_transform(&self, entity_id: u32) -> Option<(f32, f32, f32, f32)> { - let entity = self.rb_entities.get(&entity_id); - match entity { - Some(entity) => { - let rb = self.physics_manager.rigid_body_set.get(entity.rb); - rb.map(|rb| { - let position = rb.position(); - let angle = position.rotation.angle(); - ( - position.translation.x * PIXELS_TO_METRES, - position.translation.y * PIXELS_TO_METRES, - angle.cos(), - angle.sin(), - ) - }) - } - None => return None, - } - } - - fn polyline_from_marchable( - &self, - marchable: &impl Marchable, - minimum_verts: Option, - ) -> (Vec, Vec<[u32; 2]>) { - let (w, h) = marchable.size(); - let polys = marching_squares_vertex_trace(marchable, w, h); - let mut vertices = Vec::new(); - let mut indices = Vec::new(); - - let minimum_verts = minimum_verts.unwrap_or(3); - - for poly in polys { - let v = vertices.len() as u32; - let p = poly.len() as u32; - if p < minimum_verts { - continue; - } - for i in 0..p { - vertices.push( - (poly[i as usize] - vec2((w as f32 - 1.0) / 2.0, (h as f32 - 1.0) / 2.0)) - / PIXELS_TO_METRES, - ); - indices.push([v + i, v + (i + 1) % p]); - } - } - - (vertices, indices) - } - - fn polyline_collider_from_marchable( - &self, - marchable: &impl Marchable, - minimum_verts: Option, - ) -> geometry::ColliderBuilder { - let (vertices, indices) = self.polyline_from_marchable(marchable, minimum_verts); - - geometry::ColliderBuilder::polyline(vertices, Some(indices)) - } - - fn convex_hull_collider_from_marchable( - &self, - marchable: &impl Marchable, - minimum_verts: Option, - ) -> geometry::ColliderBuilder { - let (vertices, indices) = self.polyline_from_marchable(marchable, minimum_verts); - geometry::ColliderBuilder::convex_decomposition(&vertices, &indices) - } - - pub fn upsert_chunk_collider(&mut self, cx: i32, cy: i32, chunk: &Chunk) { - puffin::profile_function!(); - let collider = self - .polyline_collider_from_marchable(chunk, Some(20)) - .translation( - vec2( - (cx as f32 + 0.5) * CHUNK_SIZE as f32, - (cy as f32 + 0.5) * CHUNK_SIZE as f32, - ) / PIXELS_TO_METRES, - ); - - if let Some(handle) = self.chunk_colliders.remove(&(cx, cy)) { - self.physics_manager.collider_set.remove( - handle, - &mut self.physics_manager.island_manager, - &mut self.physics_manager.rigid_body_set, - false, - ); - } - - let handle = self.physics_manager.collider_set.insert(collider); - self.chunk_colliders.insert((cx, cy), handle); - } - - pub fn new() -> Self { - RbSimManager { - rb_entities: FxHashMap::default(), - chunk_colliders: FxHashMap::default(), - - physics_manager: PhysicsManager::new(), - next_id: 0, - debug_line_buffer: DebugLineBuffer::default(), - } - } -} diff --git a/src/sim/rb_sim/rb_entity.rs b/src/sim/rb_sim/rb_entity.rs deleted file mode 100644 index 18a6693..0000000 --- a/src/sim/rb_sim/rb_entity.rs +++ /dev/null @@ -1,39 +0,0 @@ -use rapier2d::prelude; - -use crate::sim::{ - cell::{cell::Cell, materials::MaterialId}, - lib::marching_squares::Marchable, -}; - -pub struct RbEntity { - pub id: u32, - pub width: i32, - pub height: i32, - pub cells: Vec, - pub rb: prelude::RigidBodyHandle, - pub collider: Option, -} - -impl RbEntity { - #[inline] - pub fn get_cell_at_local_position(&self, x: u8, y: u8) -> Cell { - self.cells[x as usize + y as usize * self.width as usize] - } - #[inline] - pub fn set_cell_at_local_position(&mut self, x: u8, y: u8, cell: Cell) { - self.cells[x as usize + y as usize * self.width as usize] = cell; - } -} - -impl Marchable for RbEntity { - fn occupied(&self, x: i32, y: i32) -> bool { - if x < 0 || x >= self.width || y < 0 || y >= self.height { - false - } else { - self.get_cell_at_local_position(x as u8, y as u8).material != MaterialId::Void - } - } - fn size(&self) -> (i32, i32) { - (self.width, self.height) - } -} 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