feat: soft entity collision, dead code removal, leaf standability fix

- Add TileOccupancy resource and rebuild_tile_occupancy system to track
  per-tile entity counts for soft collision avoidance
- Add collision_delay to Ambulatory; entities try relative-left step on
  occupied tiles, wait 1 tick, then push through
- Fix leaf canopy standability: leaves are now can_stand_in=true,
  can_stand_on=false (walkable, not standable-on)
- Delete FloorTilePrefab / FixtureTilePrefab / FloorTile / FixtureTile /
  TileState (all fully dead — TileMap + ChunkData are sole truth)
- Delete tile_spawns from TerrainBlob (populated but never consumed)
- Delete leaf ghost entity spawn in forestry (orphaned invisible ECS entity)
- Replace log prefab spawn with inline commands.spawn(Transform, Visibility)
- Add TileMap::remove_fixture for future digging/explosion use
- Skip collision avoidance when current tile has >2 entities (handles spawn
  cluster deadlock)
This commit is contained in:
2026-03-21 01:36:38 +00:00
parent 1fe4781bab
commit 90a6196443
25 changed files with 1909 additions and 498 deletions
+8 -6
View File
@@ -12,8 +12,7 @@ use crate::{
constants::{SEED, TILE_SIZE},
world::{
tiles::{FixtureTileData, TileMap},
ChunkForrestryEvent, ChunkMap, ChunkOwner, FixtureTilePrefab, TextureIDs, Textures,
VisibleGameEntity,
ChunkForrestryEvent, ChunkMap, ChunkOwner, TextureIDs, Textures, VisibleGameEntity,
},
};
@@ -72,8 +71,12 @@ pub fn generate_chunk_forrestry(
continue;
}
let trunk_entity =
FixtureTilePrefab::log(trunk_pos).spawn(&mut commands);
let trunk_entity = commands
.spawn((
Transform::from_translation(trunk_pos),
Visibility::Hidden,
))
.id();
tree_positions.push(trunk_pos);
commands
.entity(trunk_entity)
@@ -136,7 +139,6 @@ pub fn generate_chunk_forrestry(
* (1.0 + (rng.random::<f32>() * 0.35 - 0.1));
if x_f * x_f + y_f * y_f + z_f * z_f <= radius * radius {
FixtureTilePrefab::leaves(pos).spawn(&mut commands);
if let Some(texture_id) = texture_ids.refs.get(&500005)
{
if let Some(texture) =
@@ -166,7 +168,7 @@ pub fn generate_chunk_forrestry(
(
ivec,
FixtureTileData::new(
5, false, true, [0; 8],
5, true, false, [0; 8],
),
),
);
+1 -10
View File
@@ -9,7 +9,7 @@ use crate::{
constants::{SEED, TILE_SIZE},
world::{
tiles::{ChunkData, FloorTileData, TileMap},
ChunkForrestryEvent, ChunkMap, ChunkTerrainEvent, FloorTilePrefab, TileOcclusionEvent,
ChunkForrestryEvent, ChunkMap, ChunkTerrainEvent, TileOcclusionEvent,
CHUNK_SIZE, Z_ABOVE, Z_BELOW,
},
};
@@ -47,7 +47,6 @@ pub struct TerrainBlob {
pub chunk_data: ChunkData,
pub tile_updates: Vec<(IVec3, FloorTileData)>,
pub surface_positions: Vec<(Vec3, String)>,
pub tile_spawns: Vec<(Vec3, FloorTilePrefab)>,
}
pub fn generate_surface_terrain(x: i32, y: i32) -> f32 {
@@ -75,7 +74,6 @@ fn generate_terrain_blob(chunk_pos: IVec2) -> TerrainBlob {
let mut chunk_data = ChunkData::new(chunk_pos);
let mut tile_updates: Vec<(IVec3, FloorTileData)> = Vec::new();
let mut surface_positions: Vec<(Vec3, String)> = Vec::new();
let mut tile_spawns: Vec<(Vec3, FloorTilePrefab)> = Vec::new();
for local_y in 0..CHUNK_SIZE {
for local_x in 0..CHUNK_SIZE {
@@ -107,7 +105,6 @@ fn generate_terrain_blob(chunk_pos: IVec2) -> TerrainBlob {
]);
if cave_value < -0.75 {
let tile = registry.floor("air");
tile_spawns.push((position, FloorTilePrefab::air(position)));
tile_updates.push((
pos_ivec,
FloorTileData::new(
@@ -130,7 +127,6 @@ fn generate_terrain_blob(chunk_pos: IVec2) -> TerrainBlob {
);
} else if cave_value < 0.8 {
let tile = registry.floor("rock");
tile_spawns.push((position, FloorTilePrefab::rock(position)));
tile_updates.push((
pos_ivec,
FloorTileData::new(
@@ -153,7 +149,6 @@ fn generate_terrain_blob(chunk_pos: IVec2) -> TerrainBlob {
);
} else {
let tile = registry.floor("dirt");
tile_spawns.push((position, FloorTilePrefab::dirt(position)));
tile_updates.push((
pos_ivec,
FloorTileData::new(
@@ -178,7 +173,6 @@ fn generate_terrain_blob(chunk_pos: IVec2) -> TerrainBlob {
} else if noise_position.z > position.z {
if surface_height <= position.z + TILE_SIZE {
let tile = registry.floor("grass");
tile_spawns.push((position, FloorTilePrefab::grass(position)));
tile_updates.push((
pos_ivec,
FloorTileData::new(
@@ -202,7 +196,6 @@ fn generate_terrain_blob(chunk_pos: IVec2) -> TerrainBlob {
surface_positions.push((position, "grass".to_string()));
} else {
let tile = registry.floor("dirt");
tile_spawns.push((position, FloorTilePrefab::dirt(position)));
tile_updates.push((
pos_ivec,
FloorTileData::new(
@@ -226,7 +219,6 @@ fn generate_terrain_blob(chunk_pos: IVec2) -> TerrainBlob {
}
} else {
let tile = registry.floor("air");
tile_spawns.push((position, FloorTilePrefab::air(position)));
tile_updates.push((
pos_ivec,
FloorTileData::new(
@@ -257,7 +249,6 @@ fn generate_terrain_blob(chunk_pos: IVec2) -> TerrainBlob {
chunk_data,
tile_updates,
surface_positions,
tile_spawns,
}
}
+4 -7
View File
@@ -2,8 +2,8 @@ use crate::{
camera,
config::{GameConfig, VsyncMode},
world::generation::{
generate_chunk_fauna, generate_chunk_foliage, generate_chunk_forrestry,
generate_chunk_weathering_and_precipitation, apply_terrain_blobs, spawn_terrain_tasks,
apply_terrain_blobs, generate_chunk_fauna, generate_chunk_foliage,
generate_chunk_forrestry, generate_chunk_weathering_and_precipitation, spawn_terrain_tasks,
TerrainBlobStorage,
},
};
@@ -16,12 +16,9 @@ pub mod tiles;
pub use chunks::management::*;
pub use textures::management::*;
pub use tiles::{prefabs::*, rendering::*, visibility::*};
pub use tiles::{rendering::*, visibility::*};
pub fn apply_vsync_setting(
config: Res<GameConfig>,
mut windows: Query<&mut Window>,
) {
pub fn apply_vsync_setting(config: Res<GameConfig>, mut windows: Query<&mut Window>) {
if config.is_changed() {
if let Ok(mut window) = windows.single_mut() {
window.present_mode = match config.display.vsync {
+2 -6
View File
@@ -151,14 +151,10 @@ impl TilemapBenchmark {
}
/// Runs every frame — samples entity counts and frame timing.
pub fn track_benchmark(
mut bench: ResMut<TilemapBenchmark>,
time: Res<Time>,
floor_tiles: Query<(), With<super::FloorTile>>,
) {
pub fn track_benchmark(mut bench: ResMut<TilemapBenchmark>, time: Res<Time>) {
bench.frame_count += 1;
bench.frame_times.push(time.delta());
bench.floor_tile_count = floor_tiles.iter().count() as u32;
bench.floor_tile_count = 0;
// Trim rolling window to last 300 frames for p99 calculation
if bench.frame_times.len() > 300 {
-4
View File
@@ -1,7 +1,5 @@
pub mod benchmark;
pub mod chunk_data;
pub mod components;
pub mod prefabs;
pub mod rendering;
pub mod tilemap;
pub mod tilemap_chunk;
@@ -9,8 +7,6 @@ pub mod visibility;
pub use benchmark::*;
pub use chunk_data::*;
pub use components::*;
pub use prefabs::*;
pub use tilemap::*;
pub use tilemap_chunk::*;
pub use visibility::*;
+4 -195
View File
@@ -1,195 +1,4 @@
use bevy::prelude::*;
use crate::{
config::TileRegistry,
world::{
tiles::{FixtureTile, FloorTile, TileState},
FIXTURE_ID_OFFSET,
},
};
#[derive(Bundle)]
pub struct FloorTilePrefab {
transform: Transform,
tile: FloorTile,
tile_state: TileState,
visibility: Visibility,
}
impl FloorTilePrefab {
pub fn grass(position: Vec3) -> Self {
let def = TileRegistry::global().floor("grass");
FloorTilePrefab {
transform: Transform::from_translation(position),
tile: FloorTile {
id: def.id as u32,
opaque: !def.transparent,
walkable: def.can_stand_on,
astar_weight: def.astar_weight,
visible_range: [0; 8],
},
tile_state: TileState {
timer: Timer::from_seconds(1.0, TimerMode::Repeating),
},
visibility: Visibility::Hidden,
}
}
pub fn dirt(position: Vec3) -> Self {
let def = TileRegistry::global().floor("dirt");
FloorTilePrefab {
transform: Transform::from_translation(position),
tile: FloorTile {
id: def.id as u32,
opaque: !def.transparent,
walkable: def.can_stand_on,
astar_weight: def.astar_weight,
visible_range: [0; 8],
},
tile_state: TileState {
timer: Timer::from_seconds(1.0, TimerMode::Repeating),
},
visibility: Visibility::Hidden,
}
}
pub fn rock(position: Vec3) -> Self {
let def = TileRegistry::global().floor("rock");
FloorTilePrefab {
transform: Transform::from_translation(position),
tile: FloorTile {
id: def.id as u32,
opaque: !def.transparent,
walkable: def.can_stand_on,
astar_weight: def.astar_weight,
visible_range: [0; 8],
},
tile_state: TileState {
timer: Timer::from_seconds(1.0, TimerMode::Repeating),
},
visibility: Visibility::Hidden,
}
}
pub fn air(position: Vec3) -> Self {
let def = TileRegistry::global().floor("air");
FloorTilePrefab {
transform: Transform::from_translation(position),
tile: FloorTile {
id: def.id as u32,
opaque: !def.transparent,
walkable: def.can_stand_on,
astar_weight: def.astar_weight,
visible_range: [0; 8],
},
tile_state: TileState {
timer: Timer::from_seconds(1.0, TimerMode::Repeating),
},
visibility: Visibility::Hidden,
}
}
pub fn bedrock(position: Vec3) -> Self {
let def = TileRegistry::global().floor("bedrock");
FloorTilePrefab {
transform: Transform::from_translation(position),
tile: FloorTile {
id: def.id as u32,
opaque: !def.transparent,
walkable: def.can_stand_on,
astar_weight: def.astar_weight,
visible_range: [0; 8],
},
tile_state: TileState {
timer: Timer::from_seconds(1.0, TimerMode::Repeating),
},
visibility: Visibility::Hidden,
}
}
pub fn spawn(self, commands: &mut Commands) -> Entity {
commands
.spawn((self.tile, self.transform, self.tile_state, self.visibility))
.id()
}
}
#[derive(Bundle)]
pub struct FixtureTilePrefab {
transform: Transform,
tile: FixtureTile,
visibility: Visibility,
}
impl FixtureTilePrefab {
pub fn dirt_wall(position: Vec3) -> Self {
let def = TileRegistry::global().fixture("dirt_wall");
FixtureTilePrefab {
transform: Transform::from_translation(position),
tile: FixtureTile {
id: FIXTURE_ID_OFFSET + def.id,
solid: def.solid,
visible_range: [0; 8],
},
visibility: Visibility::Hidden,
}
}
pub fn rock_wall(position: Vec3) -> Self {
let def = TileRegistry::global().fixture("rock_wall");
FixtureTilePrefab {
transform: Transform::from_translation(position),
tile: FixtureTile {
id: FIXTURE_ID_OFFSET + def.id,
solid: def.solid,
visible_range: [0; 8],
},
visibility: Visibility::Hidden,
}
}
pub fn log(position: Vec3) -> Self {
let def = TileRegistry::global().fixture("log");
FixtureTilePrefab {
transform: Transform::from_translation(position),
tile: FixtureTile {
id: FIXTURE_ID_OFFSET + def.id,
solid: def.solid,
visible_range: [0; 8],
},
visibility: Visibility::Hidden,
}
}
pub fn leaves(position: Vec3) -> Self {
let def = TileRegistry::global().fixture("leaves");
FixtureTilePrefab {
transform: Transform::from_translation(position),
tile: FixtureTile {
id: FIXTURE_ID_OFFSET + def.id,
solid: def.solid,
visible_range: [0; 8],
},
visibility: Visibility::Hidden,
}
}
pub fn bedrock_wall(position: Vec3) -> Self {
let def = TileRegistry::global().fixture("bedrock_wall");
FixtureTilePrefab {
transform: Transform::from_translation(position),
tile: FixtureTile {
id: FIXTURE_ID_OFFSET + def.id,
solid: def.solid,
visible_range: [0; 8],
},
visibility: Visibility::Hidden,
}
}
pub fn spawn(self, commands: &mut Commands) -> Entity {
commands
.spawn((self.tile, self.transform, self.visibility))
.id()
}
}
// Prefab types (FloorTilePrefab, FixtureTilePrefab) were deleted — they were
// entirely dead code. FloorTile and FixtureTile ECS components (in the former
// components.rs) were also never read. The TileMap + ChunkData bitsets are the
// sole source of truth for standability.
+18
View File
@@ -237,6 +237,24 @@ impl TileMap {
.unwrap_or(100)
}
/// Remove a fixture tile, clearing both the HashMap entry and ChunkData bitsets.
/// BOTH must be cleared — leaving ChunkData stale causes is_standable bugs.
pub fn remove_fixture(&mut self, pos: &IVec3) -> Option<FixtureTileData> {
use super::chunk_data::ChunkData;
let removed = self.fixture_tiles.remove(pos);
let chunk_pos = world_to_chunk(*pos);
if let Some(chunk) = self.chunks.get_mut(&chunk_pos) {
let (lx, ly, z) = ChunkData::world_to_local(*pos);
let idx = ChunkData::pos_to_index(lx, ly, z);
let word = idx / 32;
let clear_mask = !(1u32 << (idx % 32));
chunk.stand_in_fixture[word] &= clear_mask;
chunk.stand_on_fixture[word] &= clear_mask;
}
removed
}
/// Remove all tile data for a specific chunk from the TileMap.
/// Iterates all positions in the chunk volume and removes from HashMaps.
/// Used during chunk unloading to clean up tile data.
+1 -1
View File
@@ -280,7 +280,7 @@ pub fn spawn_tilemap_chunks(
let half_chunk = (CHUNK_SIZE_TILE as f32) / 2.0 - TILE_SIZE / 2.0;
let world_x = (key.chunk_pos.x as f32) * (CHUNK_SIZE_TILE as f32) + half_chunk;
let world_y = (key.chunk_pos.y as f32) * (CHUNK_SIZE_TILE as f32) + half_chunk;
let z_depth = -(Z_BELOW - key.z_index as f32) * TILE_SIZE;
let z_depth = (key.z_index as f32 - Z_BELOW) * TILE_SIZE;
let visible = z_diff >= 0 && z_diff <= 8;