feat: implement chunk unloading infrastructure

Chunk unloading system with cleanly abstracted decision layer.

- ChunkMap: replace (bool, i32) timer tuple with () presence marker;
  remove FIFO cycle fields pending_unload/unload_cycle/unload_cursor/
  unload_countdown. loaded_chunks is now HashMap<IVec2, ()>.
- handle_chunk_events: simplify re-entrancy check to contains_key;
  remove parallel Mutex dedup machinery — load detection is now trivial.
- is_standable: single HashMap get; return false for unloaded chunks
  instead of falling back to is_standable_slow (pathfinding correctness).
- remove_chunk_data: bounds-iteration tile cleanup for chunk unload.
- ChunkOwner component + chunk_entity_index: static terrain entities
  (floor tiles, fixtures, trees) tagged at spawn and despawned by
  chunk. Mobile entities (dorfs, pigs, rabbits) not tracked — safe
  by design since they were never indexed.
- unload_chunk: canonical single-chunk unload function; cleans
  loaded_chunks, despawns entities, removes tilemap data, marks render
  chunk dirty via ChunkZKey::from_world, updates connectivity, fires
  rebake. Full docstring with step-by-step state map, working example
  for dynamic unload, and "what it does NOT handle" section.
- dynamic_chunk_unloading_system: no-op stub. Re-enable by writing
  a system that diffs wanted vs loaded chunks and calls unload_chunk.
  Wired out of FixedUpdate during foundation work.
- render chunk fix: ChunkZKey::from_world now pub(crate) so unload
  can derive the correct render chunk key (spans 4x4 world chunks)
  matching what build_quilted_terrain_sprites uses at spawn.
- FloorTilePrefab::spawn: pre-existing bug — return Entity not ().
This commit is contained in:
2026-03-20 10:30:33 +00:00
parent a0eb608d73
commit d7cbbaa04b
7 changed files with 237 additions and 63 deletions
+156 -48
View File
@@ -1,9 +1,16 @@
use bevy::prelude::*; use bevy::prelude::*;
use bevy_platform::collections::HashMap; use bevy_platform::collections::HashMap;
use bevy_platform::sync::Mutex;
use std::collections::HashSet; use std::collections::HashSet;
use crate::world::{tiles::TileMap, CurrentWorldSpriteState, TerrainSpriteState}; use crate::world::{
tiles::{ChunkZKey, QuiltCache, TileMap},
CurrentWorldSpriteState, TerrainSpriteState,
};
/// Marks an entity as belonging to a specific chunk. Used for O(1) entity despawn
/// when the chunk is unloaded.
#[derive(Component)]
pub struct ChunkOwner(pub IVec2);
pub const CHUNK_SIZE: i32 = 8; pub const CHUNK_SIZE: i32 = 8;
pub const CHUNK_SIZE_TILE: i32 = CHUNK_SIZE * crate::constants::ITILE_SIZE; pub const CHUNK_SIZE_TILE: i32 = CHUNK_SIZE * crate::constants::ITILE_SIZE;
@@ -39,15 +46,15 @@ pub const Z_BELOW: f32 = 5.0;
pub const Z_ABOVE: f32 = 15.0; pub const Z_ABOVE: f32 = 15.0;
pub const Z_TOTAL: f32 = Z_ABOVE + Z_BELOW; pub const Z_TOTAL: f32 = Z_ABOVE + Z_BELOW;
const _: () = assert!(Z_TOTAL <= 255.0); const _: () = assert!((Z_BELOW + Z_ABOVE) as usize <= 255);
#[derive(Resource)] #[derive(Resource)]
pub struct ChunkMap { pub struct ChunkMap {
pub loaded_chunks: HashMap<IVec2, (bool, i32)>, /// All currently loaded chunk positions. The `()` value is just presence.
pub loaded_chunks: HashMap<IVec2, ()>,
pub chunk_connectivity: HashMap<IVec2, HashSet<IVec2>>, pub chunk_connectivity: HashMap<IVec2, HashSet<IVec2>>,
/// Chunks that need connectivity updates. Only these chunks are processed
/// each frame instead of rebuilding the entire graph.
pub dirty_chunks: HashSet<IVec2>, pub dirty_chunks: HashSet<IVec2>,
pub chunk_entity_index: HashMap<IVec2, Vec<Entity>>,
} }
impl Default for ChunkMap { impl Default for ChunkMap {
@@ -56,6 +63,7 @@ impl Default for ChunkMap {
loaded_chunks: HashMap::new(), loaded_chunks: HashMap::new(),
chunk_connectivity: HashMap::new(), chunk_connectivity: HashMap::new(),
dirty_chunks: HashSet::new(), dirty_chunks: HashSet::new(),
chunk_entity_index: HashMap::new(),
} }
} }
} }
@@ -104,13 +112,12 @@ pub fn handle_chunk_events(
let mut any = false; let mut any = false;
for event in chunk_events.par_read() { for event in chunk_events.par_read() {
let chunk_pos = event.0.chunk_position; let chunk_pos = event.0.chunk_position;
if let Some((true, _)) = chunk_map.loaded_chunks.get(&chunk_pos) { if chunk_map.loaded_chunks.contains_key(&chunk_pos) {
continue; continue;
} }
any = true; any = true;
let chunk_map_updates: Mutex<HashMap<IVec2, bool>> = Mutex::new(HashMap::new()); chunk_map.loaded_chunks.insert(chunk_pos, ());
// Mark chunk as loaded in our thread-safe collection chunk_map.dirty_chunks.insert(chunk_pos);
chunk_map_updates.lock().unwrap().insert(chunk_pos, true);
terrain_event_writer.write(ChunkTerrainEvent { terrain_event_writer.write(ChunkTerrainEvent {
chunk_position: chunk_pos, chunk_position: chunk_pos,
}); });
@@ -123,41 +130,12 @@ pub fn handle_chunk_events(
fauna_event_writer.write(ChunkFaunaEvent { fauna_event_writer.write(ChunkFaunaEvent {
chunk_position: chunk_pos, chunk_position: chunk_pos,
}); });
// Apply all collected updates to the actual resources
for (chunk_pos, value) in chunk_map_updates.into_inner().unwrap() {
chunk_map.loaded_chunks.insert(chunk_pos, (value, 1800));
chunk_map.dirty_chunks.insert(chunk_pos);
}
} }
if any { if any {
cwss.state = TerrainSpriteState::WaitingForRender; cwss.state = TerrainSpriteState::WaitingForRender;
} }
} }
pub fn chunkmap_despawn_timer_system(
mut chunk_map: ResMut<ChunkMap>,
mut cwss: ResMut<CurrentWorldSpriteState>,
) {
let mut newly_unloaded: Vec<IVec2> = Vec::new();
for (chunk_pos, (is_loaded, timer)) in chunk_map.loaded_chunks.iter_mut() {
if !*is_loaded {
continue;
}
if *timer > 0 {
*timer -= 1;
} else {
*is_loaded = false;
newly_unloaded.push(*chunk_pos);
cwss.state = TerrainSpriteState::WaitingForRender;
}
}
for chunk_pos in newly_unloaded {
chunk_map.dirty_chunks.insert(chunk_pos);
}
}
pub fn update_chunk_connectivity(mut chunk_map: ResMut<ChunkMap>) { pub fn update_chunk_connectivity(mut chunk_map: ResMut<ChunkMap>) {
if chunk_map.dirty_chunks.is_empty() { if chunk_map.dirty_chunks.is_empty() {
return; return;
@@ -166,13 +144,7 @@ pub fn update_chunk_connectivity(mut chunk_map: ResMut<ChunkMap>) {
let dirty_chunks: Vec<IVec2> = chunk_map.dirty_chunks.drain().collect(); let dirty_chunks: Vec<IVec2> = chunk_map.dirty_chunks.drain().collect();
for chunk_pos in dirty_chunks { for chunk_pos in dirty_chunks {
let is_loaded = chunk_map if !chunk_map.loaded_chunks.contains_key(&chunk_pos) {
.loaded_chunks
.get(&chunk_pos)
.map(|(loaded, _)| *loaded)
.unwrap_or(false);
if !is_loaded {
chunk_map.chunk_connectivity.remove(&chunk_pos); chunk_map.chunk_connectivity.remove(&chunk_pos);
for neighbor in get_chunk_neighbors(chunk_pos) { for neighbor in get_chunk_neighbors(chunk_pos) {
if let Some(neighbors) = chunk_map.chunk_connectivity.get_mut(&neighbor) { if let Some(neighbors) = chunk_map.chunk_connectivity.get_mut(&neighbor) {
@@ -184,7 +156,7 @@ pub fn update_chunk_connectivity(mut chunk_map: ResMut<ChunkMap>) {
let mut connected_chunks = HashSet::new(); let mut connected_chunks = HashSet::new();
for neighbor in get_chunk_neighbors(chunk_pos) { for neighbor in get_chunk_neighbors(chunk_pos) {
if let Some((true, _)) = chunk_map.loaded_chunks.get(&neighbor) { if chunk_map.loaded_chunks.contains_key(&neighbor) {
connected_chunks.insert(neighbor); connected_chunks.insert(neighbor);
} }
} }
@@ -193,7 +165,7 @@ pub fn update_chunk_connectivity(mut chunk_map: ResMut<ChunkMap>) {
.insert(chunk_pos, connected_chunks); .insert(chunk_pos, connected_chunks);
for neighbor in get_chunk_neighbors(chunk_pos) { for neighbor in get_chunk_neighbors(chunk_pos) {
if let Some((true, _)) = chunk_map.loaded_chunks.get(&neighbor) { if chunk_map.loaded_chunks.contains_key(&neighbor) {
if let Some(neighbors) = chunk_map.chunk_connectivity.get_mut(&neighbor) { if let Some(neighbors) = chunk_map.chunk_connectivity.get_mut(&neighbor) {
neighbors.insert(chunk_pos); neighbors.insert(chunk_pos);
} }
@@ -201,3 +173,139 @@ pub fn update_chunk_connectivity(mut chunk_map: ResMut<ChunkMap>) {
} }
} }
} }
/// Unloads a single chunk, cleaning up all associated state.
///
/// ## What gets cleaned up (in order)
///
/// 1. **`loaded_chunks`** — removes the chunk from the set of loaded chunks.
/// Subsequent pathfinding and tile lookups treat it as unloaded.
/// 2. **`chunk_entity_index`** — retrieves and despawns all ECS entities registered
/// to this chunk. Only terrain entities (floor tiles, fixtures, trees) are indexed
/// here. Mobile entities (dorfs, pigs, rabbits) are not tracked by chunk and must
/// be handled separately if needed.
/// 3. **`tilemap`** — removes all floor, fixture, and item tile data for this chunk
/// from the TileMap HashMaps. `is_standable` will return false for these tiles
/// after this step.
/// 4. **`quilt_cache`** — marks all z-levels of the render chunk containing this
/// world chunk as dirty. The next `build_quilted_terrain_sprites` call will
/// despawn the old sprites and rebake the render chunk with remaining tiles.
/// A render chunk spans 4×4 world chunks (CHUNK_TILES=32 / CHUNK_SIZE=8).
/// The render chunk key is derived via `ChunkZKey::from_world`.
/// 5. **`dirty_chunks`** — marks the chunk dirty so `update_chunk_connectivity`
/// removes it from the pathfinding graph and cleans up neighbor references.
/// 6. **`cwss`** — sets state to `WaitingForRender` so the terrain sprite
/// rebake fires in the next Update schedule.
///
/// ## Example: dynamic unload based on player/NPC distance
///
/// ```ignore
/// pub fn dynamic_chunk_unloading_system(
/// mut commands: Commands,
/// mut tilemap: ResMut<TileMap>,
/// mut chunk_map: ResMut<ChunkMap>,
/// mut quilt_cache: ResMut<QuiltCache>,
/// mut cwss: ResMut<CurrentWorldSpriteState>,
/// camera: Query<&Transform, With<Camera>>,
/// npcs: Query<(&Ambulatory, &Transform)>,
/// ) {
/// // Build the set of chunks that should be loaded
/// let mut wanted_chunks: HashSet<IVec2> = HashSet::new();
/// let unload_radius = 12;
///
/// // Player chunk
/// if let Ok(cam) = camera.get_single() {
/// let cx = (cam.translation.x / (CHUNK_SIZE * TILE_SIZE) as f32).floor() as i32;
/// let cy = (cam.translation.y / (CHUNK_SIZE * TILE_SIZE) as f32).floor() as i32;
/// for dx in -unload_radius..=unload_radius {
/// for dy in -unload_radius..=unload_radius {
/// wanted_chunks.insert(IVec2::new(cx + dx, cy + dy));
/// }
/// }
/// }
///
/// // NPC chunks
/// for (_, transform) in &npcs {
/// let cx = (transform.translation.x / (CHUNK_SIZE * TILE_SIZE) as f32).floor() as i32;
/// let cy = (transform.translation.y / (CHUNK_SIZE * TILE_SIZE) as f32).floor() as i32;
/// for dx in -unload_radius..=unload_radius {
/// for dy in -unload_radius..=unload_radius {
/// wanted_chunks.insert(IVec2::new(cx + dx, cy + dy));
/// }
/// }
/// }
///
/// // Diff: unload chunks no longer in the wanted set
/// for (&chunk_pos, ()) in chunk_map.loaded_chunks.iter() {
/// if !wanted_chunks.contains(&chunk_pos) {
/// unload_chunk(
/// commands.as_mut(),
/// tilemap.as_mut(),
/// chunk_map.as_mut(),
/// quilt_cache.as_mut(),
/// cwss.as_mut(),
/// chunk_pos,
/// );
/// }
/// }
/// }
/// ```
///
/// ## What this does NOT handle
///
/// - **Loading chunks**: emit `GenerateChunkEvent` messages to trigger chunk loading.
/// A chunk must be loaded via `handle_chunk_events` before it can be unloaded here.
/// - **Mobile entities**: dorfs/pigs/rabbits are not in `chunk_entity_index`. If you
/// need to despawn them when they enter an unloaded chunk, check their current
/// chunk via `world_to_chunk(transform.translation)` and handle them separately.
pub fn unload_chunk(
mut commands: Commands,
mut tilemap: ResMut<TileMap>,
mut chunk_map: ResMut<ChunkMap>,
mut quilt_cache: ResMut<QuiltCache>,
mut cwss: ResMut<CurrentWorldSpriteState>,
chunk_pos: IVec2,
) {
if !chunk_map.loaded_chunks.contains_key(&chunk_pos) {
return;
}
chunk_map.loaded_chunks.remove(&chunk_pos);
if let Some(entities) = chunk_map.chunk_entity_index.remove(&chunk_pos) {
for entity in entities {
commands.entity(entity).despawn();
}
}
tilemap.remove_chunk_data(chunk_pos);
let world_x = chunk_pos.x as f32 * CHUNK_SIZE_TILE as f32;
let world_y = chunk_pos.y as f32 * CHUNK_SIZE_TILE as f32;
for z in 0..=(Z_TOTAL as usize) {
quilt_cache
.dirty_keys
.insert(ChunkZKey::from_world(world_x, world_y, z));
}
chunk_map.dirty_chunks.insert(chunk_pos);
cwss.state = TerrainSpriteState::WaitingForRender;
}
/// Placeholder: drives dynamic chunk unloading based on player/NPC interest radius.
/// Currently a no-op — unloads are disabled during foundation work.
/// To enable: replace the body with logic that diffs wanted chunks against
/// loaded_chunks and calls unload_chunk() for chunks that fell out of range.
pub fn dynamic_chunk_unloading_system(_: Commands) {}
/// Stub that drains queued unloads. Kept for when dynamic_unloading_system
/// re-queues chunks — wire it back into the FixedUpdate schedule then.
#[allow(clippy::too_many_arguments)]
pub fn handle_chunk_unloading(
mut _commands: Commands,
mut _tilemap: ResMut<TileMap>,
mut _chunk_map: ResMut<ChunkMap>,
mut _quilt_cache: ResMut<QuiltCache>,
) {
// TODO: re-enable when dynamic_unloading_system queues chunks
// For now, dynamic_chunk_unloading_system calls unload_chunk() directly
}
+31 -2
View File
@@ -12,7 +12,8 @@ use crate::{
constants::{SEED, TILE_SIZE}, constants::{SEED, TILE_SIZE},
world::{ world::{
tiles::{FixtureTileData, TileMap}, tiles::{FixtureTileData, TileMap},
ChunkForrestryEvent, FixtureTilePrefab, TextureIDs, Textures, VisibleGameEntity, ChunkForrestryEvent, ChunkMap, ChunkOwner, FixtureTilePrefab, TextureIDs, Textures,
VisibleGameEntity,
}, },
}; };
@@ -22,11 +23,14 @@ pub fn generate_chunk_forrestry(
mut tilemap: ResMut<TileMap>, mut tilemap: ResMut<TileMap>,
texture_ids: Res<TextureIDs>, texture_ids: Res<TextureIDs>,
textures: Res<Textures>, textures: Res<Textures>,
mut chunk_map: ResMut<ChunkMap>,
) { ) {
let start = Instant::now(); let start = Instant::now();
let count = events.len(); let count = events.len();
let collected_tilemap_updates: Mutex<Vec<(IVec3, FixtureTileData)>> = Mutex::new(Vec::new()); let collected_tilemap_updates: Mutex<Vec<(IVec3, FixtureTileData)>> = Mutex::new(Vec::new());
// Collect (chunk_pos, entity) pairs from parallel section for chunk_entity_index
let collected_entities: Mutex<Vec<(IVec2, Entity)>> = Mutex::new(Vec::new());
events.par_read().for_each(|event| { events.par_read().for_each(|event| {
let floor_positions = &event.floor_tiles; let floor_positions = &event.floor_tiles;
@@ -71,6 +75,14 @@ pub fn generate_chunk_forrestry(
let trunk_entity = let trunk_entity =
FixtureTilePrefab::log(trunk_pos).spawn(&mut commands); FixtureTilePrefab::log(trunk_pos).spawn(&mut commands);
tree_positions.push(trunk_pos); tree_positions.push(trunk_pos);
commands
.entity(trunk_entity)
.insert(ChunkOwner(event.chunk_position));
collected_entities
.lock()
.unwrap()
.push((event.chunk_position, trunk_entity));
// Add sprite component to the same entity if texture exists // Add sprite component to the same entity if texture exists
if let Some(texture_id) = texture_ids.refs.get(&500004) { if let Some(texture_id) = texture_ids.refs.get(&500004) {
if let Some(texture) = textures.handles.get(texture_id) { if let Some(texture) = textures.handles.get(texture_id) {
@@ -142,7 +154,14 @@ pub fn generate_chunk_forrestry(
), ),
)) ))
.id(); .id();
commands.entity(leaf).insert(VisibleGameEntity); commands.entity(leaf).insert((
VisibleGameEntity,
ChunkOwner(event.chunk_position),
));
collected_entities
.lock()
.unwrap()
.push((event.chunk_position, leaf));
collected_tilemap_updates.lock().unwrap().push( collected_tilemap_updates.lock().unwrap().push(
( (
ivec, ivec,
@@ -169,6 +188,16 @@ pub fn generate_chunk_forrestry(
for (ivec, data) in collected_updates { for (ivec, data) in collected_updates {
tilemap.insert_fixture(ivec, data); tilemap.insert_fixture(ivec, data);
} }
// Populate chunk_entity_index from collected entities (after parallel section)
let collected = collected_entities.into_inner().unwrap();
for (chunk_pos, entity) in collected {
chunk_map
.chunk_entity_index
.entry(chunk_pos)
.or_default()
.push(entity);
}
if count > 0 { if count > 0 {
println!( println!(
"Forrestry update for {:?} chunks in {:.2?}", "Forrestry update for {:?} chunks in {:.2?}",
+9 -2
View File
@@ -10,7 +10,7 @@ use crate::{
world::{ world::{
tiles::{ChunkData, FloorTileData, TileMap, TerrainSpriteState, CurrentWorldSpriteState}, tiles::{ChunkData, FloorTileData, TileMap, TerrainSpriteState, CurrentWorldSpriteState},
ChunkForrestryEvent, ChunkTerrainEvent, FloorTilePrefab, TileOcclusionEvent, CHUNK_SIZE, ChunkForrestryEvent, ChunkTerrainEvent, FloorTilePrefab, TileOcclusionEvent, CHUNK_SIZE,
Z_ABOVE, Z_BELOW, Z_ABOVE, Z_BELOW, ChunkMap, ChunkOwner,
}, },
}; };
@@ -297,6 +297,7 @@ pub fn apply_terrain_blobs(
mut commands: Commands, mut commands: Commands,
blob_storage: Res<TerrainBlobStorage>, blob_storage: Res<TerrainBlobStorage>,
mut tilemap: ResMut<TileMap>, mut tilemap: ResMut<TileMap>,
mut chunk_map: ResMut<ChunkMap>,
mut forrestry_event_writer: MessageWriter<ChunkForrestryEvent>, mut forrestry_event_writer: MessageWriter<ChunkForrestryEvent>,
mut occlusion_event_writer: MessageWriter<TileOcclusionEvent>, mut occlusion_event_writer: MessageWriter<TileOcclusionEvent>,
) { ) {
@@ -318,7 +319,13 @@ pub fn apply_terrain_blobs(
tilemap.chunks.insert(blob.chunk_pos, blob.chunk_data); tilemap.chunks.insert(blob.chunk_pos, blob.chunk_data);
for (_position, prefab) in blob.tile_spawns { for (_position, prefab) in blob.tile_spawns {
prefab.spawn(&mut commands); let entity = prefab.spawn(&mut commands);
commands.entity(entity).insert(ChunkOwner(blob.chunk_pos));
chunk_map
.chunk_entity_index
.entry(blob.chunk_pos)
.or_default()
.push(entity);
} }
for pos in new_positions { for pos in new_positions {
+6 -2
View File
@@ -37,15 +37,19 @@ impl Plugin for WorldPlugin {
.add_systems( .add_systems(
FixedUpdate, FixedUpdate,
( (
// === CHUNK LOADING PIPELINE ===
handle_chunk_events, handle_chunk_events,
update_chunk_connectivity,
// === CHUNK GENERATION (triggered by events above) ===
spawn_terrain_tasks, spawn_terrain_tasks,
apply_terrain_blobs, apply_terrain_blobs,
generate_chunk_weathering_and_precipitation, generate_chunk_weathering_and_precipitation,
generate_chunk_forrestry, generate_chunk_forrestry,
generate_chunk_foliage, generate_chunk_foliage,
generate_chunk_fauna, generate_chunk_fauna,
chunkmap_despawn_timer_system,
update_chunk_connectivity, // === DYNAMIC UNLOAD (disabled — see dynamic_chunk_unloading_system) ===
), ),
) )
.add_systems( .add_systems(
+4 -2
View File
@@ -107,8 +107,10 @@ impl FloorTilePrefab {
} }
} }
pub fn spawn(self, commands: &mut Commands) { pub fn spawn(self, commands: &mut Commands) -> Entity {
commands.spawn((self.tile, self.transform, self.tile_state, self.visibility)); commands
.spawn((self.tile, self.transform, self.tile_state, self.visibility))
.id()
} }
} }
+1 -1
View File
@@ -44,7 +44,7 @@ pub struct ChunkZKey {
impl ChunkZKey { impl ChunkZKey {
#[inline] #[inline]
fn from_world(world_x: f32, world_y: f32, z_index: usize) -> Self { pub(crate) fn from_world(world_x: f32, world_y: f32, z_index: usize) -> Self {
Self { Self {
chunk_x: (world_x / (TILE_SIZE * CHUNK_TILES as f32)).floor() as i32, chunk_x: (world_x / (TILE_SIZE * CHUNK_TILES as f32)).floor() as i32,
chunk_y: (world_y / (TILE_SIZE * CHUNK_TILES as f32)).floor() as i32, chunk_y: (world_y / (TILE_SIZE * CHUNK_TILES as f32)).floor() as i32,
+29 -5
View File
@@ -232,14 +232,14 @@ impl TileMap {
} }
/// O(1) standability check using bit-packed chunk data. /// O(1) standability check using bit-packed chunk data.
/// Falls back to HashMap lookups if chunk data is not available. /// Returns false if chunk is not loaded (unloaded chunks have no valid tiles).
pub fn is_standable(&self, world_pos: IVec3) -> bool { pub fn is_standable(&self, world_pos: IVec3) -> bool {
let chunk_pos = world_to_chunk(world_pos); let chunk_pos = world_to_chunk(world_pos);
if let Some(chunk) = self.chunks.get(&chunk_pos) { let Some(chunk) = self.chunks.get(&chunk_pos) else {
return false;
};
let (local_x, local_y, z) = ChunkData::world_to_local(world_pos); let (local_x, local_y, z) = ChunkData::world_to_local(world_pos);
return chunk.is_standable(local_x, local_y, z); chunk.is_standable(local_x, local_y, z)
}
self.is_standable_slow(world_pos)
} }
/// Fallback standability check using HashMap lookups. /// Fallback standability check using HashMap lookups.
@@ -283,4 +283,28 @@ impl TileMap {
.map(|t| t.astar_weight) .map(|t| t.astar_weight)
.unwrap_or(100) .unwrap_or(100)
} }
/// 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.
pub fn remove_chunk_data(&mut self, chunk_pos: IVec2) {
use crate::constants::ITILE_SIZE;
use crate::world::chunks::{CHUNK_SIZE, Z_ABOVE, Z_BELOW};
for local_x in 0..CHUNK_SIZE {
for local_y in 0..CHUNK_SIZE {
for z in -Z_BELOW as i32..=Z_ABOVE as i32 {
let pos = IVec3::new(
chunk_pos.x * CHUNK_SIZE + local_x,
chunk_pos.y * CHUNK_SIZE + local_y,
z,
) * ITILE_SIZE;
self.floor_tiles.remove(&pos);
self.fixture_tiles.remove(&pos);
self.item_tiles.remove(&pos);
}
}
}
self.chunks.remove(&chunk_pos);
}
} }