summaryrefslogtreecommitdiff
path: root/src/sim/rb_manager
diff options
context:
space:
mode:
authorKai Stevenson <kai@kaistevenson.com>2026-08-22 15:00:35 -0700
committerKai Stevenson <kai@kaistevenson.com>2026-08-22 15:00:35 -0700
commit1a515237afb7ad09353a65f5fbc6e98a7c29ce8e (patch)
tree48cd4b4bcf8f8e357811f905f22607838d7c1f96 /src/sim/rb_manager
parent6350e4ffca8ce1e46465284ef3d7559e0f40229b (diff)
sim manager refactor
Diffstat (limited to 'src/sim/rb_manager')
-rw-r--r--src/sim/rb_manager/debug_ops.rs48
-rw-r--r--src/sim/rb_manager/debug_render.rs71
-rw-r--r--src/sim/rb_manager/mod.rs289
-rw-r--r--src/sim/rb_manager/rb_entity.rs39
4 files changed, 447 insertions, 0 deletions
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<DebugVertex>,
+}
+
+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<u32, RbEntity>,
+
+ 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<Cell>, 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<u32>,
+ ) -> (Vec<Vec2>, 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<u32>,
+ ) -> 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<u32>,
+ ) -> 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<Cell>,
+ pub rb: prelude::RigidBodyHandle,
+ pub collider: Option<prelude::ColliderHandle>,
+}
+
+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)
+ }
+}