diff --git a/CHANGELOG.md b/CHANGELOG.md index 798f42c1a8..c7b2198476 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Sprite spawn rates - The Interact button can be used on campfires to sit - Made map icons fade out when near the edge of the map display +- Roughly doubled the speed of entity vs terrain physics checks ### Removed diff --git a/common/src/comp/fluid_dynamics.rs b/common/src/comp/fluid_dynamics.rs index 209ccfb79d..1607cb5f2c 100644 --- a/common/src/comp/fluid_dynamics.rs +++ b/common/src/comp/fluid_dynamics.rs @@ -19,6 +19,7 @@ impl LiquidKind { /// If an entity is in multiple overlapping liquid blocks, which one takes /// precedence? (should be a rare edge case, since checkerboard patterns of /// water and lava shouldn't show up in worldgen) + #[inline] pub fn merge(self, other: LiquidKind) -> LiquidKind { use LiquidKind::{Lava, Water}; match (self, other) { diff --git a/common/src/terrain/chonk.rs b/common/src/terrain/chonk.rs index 29240560de..359479e7de 100644 --- a/common/src/terrain/chonk.rs +++ b/common/src/terrain/chonk.rs @@ -57,8 +57,10 @@ impl Chonk { pub fn meta(&self) -> &M { &self.meta } + #[inline] pub fn get_min_z(&self) -> i32 { self.z_offset } + #[inline] pub fn get_max_z(&self) -> i32 { self.z_offset + (self.sub_chunks.len() as u32 * SubChunkSize::::SIZE.z) as i32 } @@ -71,6 +73,7 @@ impl Chonk { // Returns the index (in self.sub_chunks) of the SubChunk that contains // layer z; note that this index changes when more SubChunks are prepended + #[inline] fn sub_chunk_idx(&self, z: i32) -> i32 { let diff = z - self.z_offset; diff >> (SubChunkSize::::SIZE.z - 1).count_ones() diff --git a/common/src/terrain/sprite.rs b/common/src/terrain/sprite.rs index cf67732a01..05f046f092 100644 --- a/common/src/terrain/sprite.rs +++ b/common/src/terrain/sprite.rs @@ -181,6 +181,7 @@ make_case_elim!( ); impl SpriteKind { + #[inline] pub fn solid_height(&self) -> Option { // Beware: the height *must* be <= `MAX_HEIGHT` or the collision system will not // properly detect it! @@ -268,6 +269,7 @@ impl SpriteKind { } /// What loot table does collecting this sprite draw from? + #[inline] pub fn collectible_id(&self) -> Option> { let item = |id: &'static str| LootSpec::Item(id); let table = |id: &'static str| LootSpec::LootTable(id); @@ -336,16 +338,19 @@ impl SpriteKind { } /// Can this sprite be picked up to yield an item without a tool? + #[inline] pub fn is_collectible(&self) -> bool { self.collectible_id().is_some() && self.mine_tool().is_none() } /// Is the sprite a container that will emit a mystery item? + #[inline] pub fn is_container(&self) -> bool { matches!(self.collectible_id(), Some(LootSpec::LootTable(_))) } /// Which tool (if any) is needed to collect this sprite? + #[inline] pub fn mine_tool(&self) -> Option { match self { SpriteKind::Velorite @@ -375,6 +380,7 @@ impl SpriteKind { } } + #[inline] pub fn has_ori(&self) -> bool { matches!( self, @@ -446,5 +452,6 @@ lazy_static! { impl<'a> TryFrom<&'a str> for SpriteKind { type Error = (); + #[inline] fn try_from(s: &'a str) -> Result { SPRITE_KINDS.get(s).copied().ok_or(()) } } diff --git a/common/src/vol.rs b/common/src/vol.rs index 34f66ab804..42aeecf0be 100644 --- a/common/src/vol.rs +++ b/common/src/vol.rs @@ -111,6 +111,23 @@ pub trait ReadVol: BaseVol { { Ray::new(self, from, to, |_| true) } + + /// Call provided closure with each block in the supplied Aabb + /// Portions of the Aabb outside the volume are ignored + fn for_each_in(&self, aabb: Aabb, mut f: impl FnMut(Vec3, Self::Vox)) + where + Self::Vox: Copy, + { + for z in aabb.min.z..aabb.max.z + 1 { + for y in aabb.min.y..aabb.max.y + 1 { + for x in aabb.min.x..aabb.max.x + 1 { + if let Ok(block) = self.get(Vec3::new(x, y, z)) { + f(Vec3::new(x, y, z), *block); + } + } + } + } + } } /// A volume that provides the ability to sample (i.e., clone a section of) its diff --git a/common/src/volumes/vol_grid_2d.rs b/common/src/volumes/vol_grid_2d.rs index fc558c9235..49c91f00fc 100644 --- a/common/src/volumes/vol_grid_2d.rs +++ b/common/src/volumes/vol_grid_2d.rs @@ -53,6 +53,40 @@ impl ReadVol for VolGrid2d { chunk.get(co).map_err(VolGrid2dError::ChunkError) }) } + + /// Call provided closure with each block in the supplied Aabb + /// Areas outside loaded chunks are ignored + fn for_each_in(&self, aabb: Aabb, mut f: impl FnMut(Vec3, Self::Vox)) + where + Self::Vox: Copy, + { + let min_chunk_key = self.pos_key(aabb.min); + let max_chunk_key = self.pos_key(aabb.max); + for key_x in min_chunk_key.x..max_chunk_key.x + 1 { + for key_y in min_chunk_key.y..max_chunk_key.y + 1 { + let key = Vec2::new(key_x, key_y); + let pos = self.key_pos(key); + // Calculate intersection of Aabb and this chunk + // TODO: should we do this more implicitly as part of the loop + // TODO: this probably has to be computed in the chunk.for_each_in() as well + // maybe remove here? + let intersection = aabb.intersection(Aabb { + min: pos.with_z(i32::MIN), + // -1 here since the Aabb is inclusive and chunk_offs below will wrap it if + // it's outside the range of the chunk + max: (pos + Self::chunk_size().map(|e| e as i32) - 1).with_z(i32::MAX), + }); + // Map intersection into chunk coordinates + let intersection = Aabb { + min: Self::chunk_offs(intersection.min), + max: Self::chunk_offs(intersection.max), + }; + if let Some(chonk) = self.get_key(key) { + chonk.for_each_in(intersection, |pos_offset, block| f(pos_offset + pos, block)); + } + } + } + } } // TODO: This actually breaks the API: samples are supposed to have an offset of @@ -117,12 +151,14 @@ impl VolGrid2d { } } + #[inline(always)] pub fn chunk_size() -> Vec2 { V::RECT_SIZE } pub fn insert(&mut self, key: Vec2, chunk: Arc) -> Option> { self.chunks.insert(key, chunk) } + #[inline(always)] pub fn get_key(&self, key: Vec2) -> Option<&V> { self.chunks.get(&key).map(|arc_chunk| arc_chunk.as_ref()) } @@ -135,8 +171,10 @@ impl VolGrid2d { pub fn remove(&mut self, key: Vec2) -> Option> { self.chunks.remove(&key) } + #[inline(always)] pub fn key_pos(&self, key: Vec2) -> Vec2 { key * V::RECT_SIZE.map(|e| e as i32) } + #[inline(always)] pub fn pos_key(&self, pos: Vec3) -> Vec2 { Self::chunk_key(pos) } pub fn iter(&self) -> ChunkIter { diff --git a/common/systems/src/phys.rs b/common/systems/src/phys.rs index 861dddc13e..77c9a509b5 100644 --- a/common/systems/src/phys.rs +++ b/common/systems/src/phys.rs @@ -1342,73 +1342,54 @@ fn box_voxel_collision<'a, T: BaseVol + ReadVol>( clippy::cast_possible_truncation, clippy::cast_sign_loss )] + //prof_span!("box_voxel_collision"); - // Function for iterating over the blocks the player at a specific position - // collides with - fn collision_iter<'a, T: BaseVol + ReadVol>( - pos: Vec3, - terrain: &'a T, - hit: &'a impl Fn(&Block) -> bool, - height: &'a impl Fn(&Block) -> f32, - near_iter: impl Iterator + 'a, - radius: f32, - z_range: Range, - ) -> impl Iterator> + 'a { - near_iter.filter_map(move |(i, j, k)| { - let block_pos = pos.map(|e| e.floor() as i32) + Vec3::new(i, j, k); + // Convience function to compute the player aabb + fn player_aabb(pos: Vec3, radius: f32, z_range: Range) -> Aabb { + Aabb { + min: pos + Vec3::new(-radius, -radius, z_range.start), + max: pos + Vec3::new(radius, radius, z_range.end), + } + } - // `near_iter` could be a few blocks too large due to being integer - // aligned and rounding up, so skip points outside of the tighter - // bounds before looking them up in the terrain - // (which incurs a hashmap cost for volgrids) - let player_aabb = Aabb { - min: pos + Vec3::new(-radius, -radius, z_range.start), - max: pos + Vec3::new(radius, radius, z_range.end), - }; - let block_approx = Aabb { - min: block_pos.as_(), - max: block_pos.as_() + Vec3::new(1.0, 1.0, Block::MAX_HEIGHT), - }; - if !player_aabb.collides_with_aabb(block_approx) { - return None; - } - - if let Some(block) = terrain.get(block_pos).ok().copied().filter(hit) { - let block_aabb = Aabb { - min: block_pos.map(|e| e as f32), - max: block_pos.map(|e| e as f32) + Vec3::new(1.0, 1.0, height(&block)), - }; - - if player_aabb.collides_with_aabb(block_aabb) { - return Some(block_aabb); - } - } - - None - }) + // Convience function to translate the near_aabb into the world space + fn move_aabb(aabb: Aabb, pos: Vec3) -> Aabb { + Aabb { + min: aabb.min + pos.map(|e| e.floor() as i32), + max: aabb.max + pos.map(|e| e.floor() as i32), + } } // Function for determining whether the player at a specific position collides // with blocks with the given criteria - fn collision_with<'a, T: BaseVol + ReadVol>( + fn collision_with + ReadVol>( pos: Vec3, - terrain: &'a T, + terrain: &T, hit: impl Fn(&Block) -> bool, - near_iter: impl Iterator + 'a, + near_aabb: Aabb, radius: f32, z_range: Range, ) -> bool { - collision_iter( - pos, - terrain, - &|block| block.is_solid() && hit(block), - &Block::solid_height, - near_iter, - radius, - z_range, - ) - .next() - .is_some() + let player_aabb = player_aabb(pos, radius, z_range); + + // Calculate the world space near aabb + let near_aabb = move_aabb(near_aabb, pos); + + let mut collision = false; + // TODO: could short-circuit here + terrain.for_each_in(near_aabb, |block_pos, block| { + if block.is_solid() && hit(&block) { + let block_aabb = Aabb { + min: block_pos.map(|e| e as f32), + max: block_pos.map(|e| e as f32) + Vec3::new(1.0, 1.0, block.solid_height()), + }; + if player_aabb.collides_with_aabb(block_aabb) { + collision = true; + } + } + }); + + collision } // Should be easy to just make clippy happy if we want? @@ -1420,26 +1401,23 @@ fn box_voxel_collision<'a, T: BaseVol + ReadVol>( // Probe distances let hdist = radius.ceil() as i32; - // Neighbouring blocks iterator - let near_iter = (-hdist..=hdist) - .flat_map(move |i| { - (-hdist..=hdist).map(move |j| { - let max_block_height = Block::MAX_HEIGHT.ceil() as i32; - let box_floor = z_min.floor() as i32; - let floor = 1 - max_block_height + box_floor; - let ceil = z_max.ceil() as i32; - - (floor..=ceil).map(move |k| (i, j, k)) - }) - }) - .flatten(); + // Neighbouring blocks Aabb + let near_aabb = Aabb { + min: Vec3::new( + -hdist, + -hdist, + 1 - Block::MAX_HEIGHT.ceil() as i32 + z_min.floor() as i32, + ), + max: Vec3::new(hdist, hdist, z_max.ceil() as i32), + }; let z_range = z_min..z_max; + // Setup values for the loop below physics_state.on_ground = None; physics_state.on_ceiling = false; - let mut on_ground = None; + let mut on_ground = None::; let mut on_ceiling = false; // Don't loop infinitely here let mut attempts = 0; @@ -1452,54 +1430,60 @@ fn box_voxel_collision<'a, T: BaseVol + ReadVol>( .clamped(1, MAX_INCREMENTS); let old_pos = pos.0; for _ in 0..increments { + //prof_span!("increment"); const MAX_ATTEMPTS: usize = 16; pos.0 += pos_delta / increments as f32; let try_colliding_block = |pos: &Pos| { + //prof_span!("most colliding check"); // Calculate the player's AABB - let player_aabb = Aabb { - min: pos.0 + Vec3::new(-radius, -radius, z_min), - max: pos.0 + Vec3::new(radius, radius, z_max), - }; + let player_aabb = player_aabb(pos.0, radius, z_range.clone()); // Determine the block that we are colliding with most // (based on minimum collision axis) // (if we are colliding with one) - // - // 1) Calculate the block's positions in world space - // 2) Make sure the block is actually solid - // 3) Calculate block AABB - // 4) Find the maximum of the minimum collision axes - // (this bit is weird, trust me that it works) - near_iter - .clone() - .map(|(i, j, k)| pos.0.map(|e| e.floor() as i32) + Vec3::new(i, j, k)) - .filter_map(|block_pos| { - terrain - .get(block_pos) - .ok() - .filter(|block| block.is_solid()) - .map(|block| (block_pos, block)) - }) - .map(|(block_pos, block)| { - ( - block_pos, - Aabb { - min: block_pos.map(|e| e as f32), - max: block_pos.map(|e| e as f32) - + Vec3::new(1.0, 1.0, block.solid_height()), - }, - block, - ) - }) - .filter(|(_, block_aabb, _)| block_aabb.collides_with_aabb(player_aabb)) - .min_by_key(|(_, block_aabb, _)| { - ordered_float::OrderedFloat( - (block_aabb.center() - player_aabb.center() - Vec3::unit_z() * 0.5) - .map(f32::abs) - .sum(), - ) - }) + let mut most_colliding = None; + // Calculate the world space near aabb + let near_aabb = move_aabb(near_aabb, pos.0); + let player_overlap = |block_aabb: Aabb| { + ordered_float::OrderedFloat( + (block_aabb.center() - player_aabb.center() - Vec3::unit_z() * 0.5) + .map(f32::abs) + .sum(), + ) + }; + + terrain.for_each_in(near_aabb, |block_pos, block| { + // Make sure the block is actually solid + if block.is_solid() { + // Calculate block AABB + let block_aabb = Aabb { + min: block_pos.map(|e| e as f32), + max: block_pos.map(|e| e as f32) + + Vec3::new(1.0, 1.0, block.solid_height()), + }; + + // Determine whether the block's AABB collides with the player's AABB + if block_aabb.collides_with_aabb(player_aabb) { + most_colliding = match most_colliding { + // Select the minimum of the value from `player_overlap` + other @ Some((_, other_block_aabb, _)) + if { + // TODO: comment below is outdated (as of ~1 year ago) + // Find the maximum of the minimum collision axes (this bit + // is weird, trust me that it works) + player_overlap(block_aabb) >= player_overlap(other_block_aabb) + } => + { + other + }, + _ => Some((block_pos, block_aabb, block)), + } + } + } + }); + + most_colliding }; // While the player is colliding with the terrain... @@ -1508,10 +1492,7 @@ fn box_voxel_collision<'a, T: BaseVol + ReadVol>( .flatten() { // Calculate the player's AABB - let player_aabb = Aabb { - min: pos.0 + Vec3::new(-radius, -radius, z_min), - max: pos.0 + Vec3::new(radius, radius, z_max), - }; + let player_aabb = player_aabb(pos.0, radius, z_range.clone()); // Find the intrusion vector of the collision let dir = player_aabb.collision_vector_with_aabb(block_aabb); @@ -1531,7 +1512,7 @@ fn box_voxel_collision<'a, T: BaseVol + ReadVol>( // ground /* if resolve_dir.z > 0.0 && vel.0.z <= 0.0 { */ if resolve_dir.z > 0.0 { - on_ground = Some(*block); + on_ground = Some(block); if !was_on_ground { land_on_ground(entity, *vel); @@ -1544,33 +1525,33 @@ fn box_voxel_collision<'a, T: BaseVol + ReadVol>( // with a wall // // If we're being pushed out horizontally... - let pushed_horizontaly = resolve_dir.z == 0.0; + if resolve_dir.z == 0.0 // ...and the vertical resolution direction is sufficiently great... - let vertical_resolution = dir.z < -0.1; + && dir.z < -0.1 // ...and the space above is free... - let space_above_is_free = !collision_with( - Vec3::new(pos.0.x, pos.0.y, (pos.0.z + 0.1).ceil()), - &terrain, - always_hits, - near_iter.clone(), - radius, - z_range.clone(), - ); + && { + //prof_span!("space above free"); + !collision_with( + Vec3::new(pos.0.x, pos.0.y, (pos.0.z + 0.1).ceil()), + &terrain, + always_hits, + near_aabb, + radius, + z_range.clone(), + ) + } // ...and there is a collision with a block beneath our current hitbox... - let block_beneath_collides = collision_with( - pos.0 + resolve_dir - Vec3::unit_z() * 1.25, - &terrain, - always_hits, - near_iter.clone(), - radius, - z_range.clone(), - ); - - if pushed_horizontaly - && vertical_resolution - && space_above_is_free - && block_beneath_collides - { + && { + //prof_span!("collision beneath"); + collision_with( + pos.0 + resolve_dir - Vec3::unit_z() * 1.25, + &terrain, + always_hits, + near_aabb, + radius, + z_range.clone(), + ) + } { // ...block-hop! pos.0.z = pos.0.z.max(block_aabb.max.z); vel.0.z = vel.0.z.max(0.0); @@ -1579,7 +1560,7 @@ fn box_voxel_collision<'a, T: BaseVol + ReadVol>( if (vel.0 * resolve_dir).xy().magnitude_squared() < 1.0_f32.powi(2) { pos.0 -= resolve_dir.normalized() * 0.05; } - on_ground = Some(*block); + on_ground = Some(block); break; } @@ -1606,6 +1587,7 @@ fn box_voxel_collision<'a, T: BaseVol + ReadVol>( } } + // Report on_ceiling state if on_ceiling { physics_state.on_ceiling = true; } @@ -1613,17 +1595,18 @@ fn box_voxel_collision<'a, T: BaseVol + ReadVol>( if on_ground.is_some() { physics_state.on_ground = on_ground; // If the space below us is free, then "snap" to the ground - } else if collision_with( - pos.0 - Vec3::unit_z() * 1.1, - &terrain, - always_hits, - near_iter.clone(), - radius, - z_range.clone(), - ) && vel.0.z <= 0.0 - && was_on_ground - && block_snap - { + } else if vel.0.z <= 0.0 && was_on_ground && block_snap && { + //prof_span!("snap check"); + collision_with( + pos.0 - Vec3::unit_z() * 1.1, + &terrain, + always_hits, + near_aabb, + radius, + z_range.clone(), + ) + } { + //prof_span!("snap!!"); let snap_height = terrain .get(Vec3::new(pos.0.x, pos.0.y, pos.0.z - 0.1).map(|e| e.floor() as i32)) .ok() @@ -1637,11 +1620,10 @@ fn box_voxel_collision<'a, T: BaseVol + ReadVol>( .copied(); } - let player_aabb = Aabb { - min: pos.0 + Vec3::new(-radius, -radius, z_range.start), - max: pos.0 + Vec3::new(radius, radius, z_range.end), - }; - let player_voxel_pos = pos.0.map(|e| e.floor() as i32); + // Find liquid immersion and wall collision all in one round of iteration + let player_aabb = player_aabb(pos.0, radius, z_range.clone()); + // Calculate the world space near_aabb + let near_aabb = move_aabb(near_aabb, pos.0); let dirs = [ Vec3::unit_x(), @@ -1649,6 +1631,8 @@ fn box_voxel_collision<'a, T: BaseVol + ReadVol>( -Vec3::unit_x(), -Vec3::unit_y(), ]; + + // Compute a list of aabbs to check for collision with nearby walls let player_wall_aabbs = dirs.map(|dir| { let pos = pos.0 + dir * 0.01; Aabb { @@ -1657,45 +1641,46 @@ fn box_voxel_collision<'a, T: BaseVol + ReadVol>( } }); - // Find liquid immersion and wall collision all in one round of iteration let mut liquid = None::<(LiquidKind, f32)>; let mut wall_dir_collisions = [false; 4]; - near_iter.for_each(|(i, j, k)| { - let block_pos = player_voxel_pos + Vec3::new(i, j, k); - - if let Some(block) = terrain.get(block_pos).ok().copied() { - // Check for liquid blocks - if let Some(block_liquid) = block.liquid_kind() { - let liquid_aabb = Aabb { - min: block_pos.map(|e| e as f32), - // The liquid part of a liquid block always extends 1 block high. - max: block_pos.map(|e| e as f32) + Vec3::one(), + //prof_span!(guard, "liquid/walls"); + terrain.for_each_in(near_aabb, |block_pos, block| { + // Check for liquid blocks + if let Some(block_liquid) = block.liquid_kind() { + let liquid_aabb = Aabb { + min: block_pos.map(|e| e as f32), + // The liquid part of a liquid block always extends 1 block high. + max: block_pos.map(|e| e as f32) + Vec3::one(), + }; + if player_aabb.collides_with_aabb(liquid_aabb) { + liquid = match liquid { + Some((kind, max_liquid_z)) => Some(( + // TODO: merging of liquid kinds and max_liquid_z are done + // independently which allows mix and + // matching them + kind.merge(block_liquid), + max_liquid_z.max(liquid_aabb.max.z), + )), + None => Some((block_liquid, liquid_aabb.max.z)), }; - if player_aabb.collides_with_aabb(liquid_aabb) { - liquid = match liquid { - Some((kind, max_liquid_z)) => Some(( - kind.merge(block_liquid), - max_liquid_z.max(liquid_aabb.max.z), - )), - None => Some((block_liquid, liquid_aabb.max.z)), - }; - } } - // Check for walls - if block.is_solid() { - let block_aabb = Aabb { - min: block_pos.map(|e| e as f32), - max: block_pos.map(|e| e as f32) + Vec3::new(1.0, 1.0, block.solid_height()), - }; + } - for dir in 0..4 { - if player_wall_aabbs[dir].collides_with_aabb(block_aabb) { - wall_dir_collisions[dir] = true; - } + // Check for walls + if block.is_solid() { + let block_aabb = Aabb { + min: block_pos.map(|e| e as f32), + max: block_pos.map(|e| e as f32) + Vec3::new(1.0, 1.0, block.solid_height()), + }; + + for dir in 0..4 { + if player_wall_aabbs[dir].collides_with_aabb(block_aabb) { + wall_dir_collisions[dir] = true; } } } }); + //drop(guard); // Use wall collision results to determine if we are against a wall let mut on_wall = None;