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:
+156
-48
@@ -1,9 +1,16 @@
|
||||
use bevy::prelude::*;
|
||||
use bevy_platform::collections::HashMap;
|
||||
use bevy_platform::sync::Mutex;
|
||||
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_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_TOTAL: f32 = Z_ABOVE + Z_BELOW;
|
||||
|
||||
const _: () = assert!(Z_TOTAL <= 255.0);
|
||||
const _: () = assert!((Z_BELOW + Z_ABOVE) as usize <= 255);
|
||||
|
||||
#[derive(Resource)]
|
||||
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>>,
|
||||
/// Chunks that need connectivity updates. Only these chunks are processed
|
||||
/// each frame instead of rebuilding the entire graph.
|
||||
pub dirty_chunks: HashSet<IVec2>,
|
||||
pub chunk_entity_index: HashMap<IVec2, Vec<Entity>>,
|
||||
}
|
||||
|
||||
impl Default for ChunkMap {
|
||||
@@ -56,6 +63,7 @@ impl Default for ChunkMap {
|
||||
loaded_chunks: HashMap::new(),
|
||||
chunk_connectivity: HashMap::new(),
|
||||
dirty_chunks: HashSet::new(),
|
||||
chunk_entity_index: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -104,13 +112,12 @@ pub fn handle_chunk_events(
|
||||
let mut any = false;
|
||||
for event in chunk_events.par_read() {
|
||||
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;
|
||||
}
|
||||
any = true;
|
||||
let chunk_map_updates: Mutex<HashMap<IVec2, bool>> = Mutex::new(HashMap::new());
|
||||
// Mark chunk as loaded in our thread-safe collection
|
||||
chunk_map_updates.lock().unwrap().insert(chunk_pos, true);
|
||||
chunk_map.loaded_chunks.insert(chunk_pos, ());
|
||||
chunk_map.dirty_chunks.insert(chunk_pos);
|
||||
terrain_event_writer.write(ChunkTerrainEvent {
|
||||
chunk_position: chunk_pos,
|
||||
});
|
||||
@@ -123,41 +130,12 @@ pub fn handle_chunk_events(
|
||||
fauna_event_writer.write(ChunkFaunaEvent {
|
||||
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 {
|
||||
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>) {
|
||||
if chunk_map.dirty_chunks.is_empty() {
|
||||
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();
|
||||
|
||||
for chunk_pos in dirty_chunks {
|
||||
let is_loaded = chunk_map
|
||||
.loaded_chunks
|
||||
.get(&chunk_pos)
|
||||
.map(|(loaded, _)| *loaded)
|
||||
.unwrap_or(false);
|
||||
|
||||
if !is_loaded {
|
||||
if !chunk_map.loaded_chunks.contains_key(&chunk_pos) {
|
||||
chunk_map.chunk_connectivity.remove(&chunk_pos);
|
||||
for neighbor in get_chunk_neighbors(chunk_pos) {
|
||||
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();
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -193,7 +165,7 @@ pub fn update_chunk_connectivity(mut chunk_map: ResMut<ChunkMap>) {
|
||||
.insert(chunk_pos, connected_chunks);
|
||||
|
||||
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) {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -12,7 +12,8 @@ use crate::{
|
||||
constants::{SEED, TILE_SIZE},
|
||||
world::{
|
||||
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>,
|
||||
texture_ids: Res<TextureIDs>,
|
||||
textures: Res<Textures>,
|
||||
mut chunk_map: ResMut<ChunkMap>,
|
||||
) {
|
||||
let start = Instant::now();
|
||||
let count = events.len();
|
||||
|
||||
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| {
|
||||
let floor_positions = &event.floor_tiles;
|
||||
@@ -71,6 +75,14 @@ pub fn generate_chunk_forrestry(
|
||||
let trunk_entity =
|
||||
FixtureTilePrefab::log(trunk_pos).spawn(&mut commands);
|
||||
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
|
||||
if let Some(texture_id) = texture_ids.refs.get(&500004) {
|
||||
if let Some(texture) = textures.handles.get(texture_id) {
|
||||
@@ -142,7 +154,14 @@ pub fn generate_chunk_forrestry(
|
||||
),
|
||||
))
|
||||
.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(
|
||||
(
|
||||
ivec,
|
||||
@@ -169,6 +188,16 @@ pub fn generate_chunk_forrestry(
|
||||
for (ivec, data) in collected_updates {
|
||||
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 {
|
||||
println!(
|
||||
"Forrestry update for {:?} chunks in {:.2?}",
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::{
|
||||
world::{
|
||||
tiles::{ChunkData, FloorTileData, TileMap, TerrainSpriteState, CurrentWorldSpriteState},
|
||||
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,
|
||||
blob_storage: Res<TerrainBlobStorage>,
|
||||
mut tilemap: ResMut<TileMap>,
|
||||
mut chunk_map: ResMut<ChunkMap>,
|
||||
mut forrestry_event_writer: MessageWriter<ChunkForrestryEvent>,
|
||||
mut occlusion_event_writer: MessageWriter<TileOcclusionEvent>,
|
||||
) {
|
||||
@@ -318,7 +319,13 @@ pub fn apply_terrain_blobs(
|
||||
tilemap.chunks.insert(blob.chunk_pos, blob.chunk_data);
|
||||
|
||||
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 {
|
||||
|
||||
+6
-2
@@ -37,15 +37,19 @@ impl Plugin for WorldPlugin {
|
||||
.add_systems(
|
||||
FixedUpdate,
|
||||
(
|
||||
// === CHUNK LOADING PIPELINE ===
|
||||
handle_chunk_events,
|
||||
update_chunk_connectivity,
|
||||
|
||||
// === CHUNK GENERATION (triggered by events above) ===
|
||||
spawn_terrain_tasks,
|
||||
apply_terrain_blobs,
|
||||
generate_chunk_weathering_and_precipitation,
|
||||
generate_chunk_forrestry,
|
||||
generate_chunk_foliage,
|
||||
generate_chunk_fauna,
|
||||
chunkmap_despawn_timer_system,
|
||||
update_chunk_connectivity,
|
||||
|
||||
// === DYNAMIC UNLOAD (disabled — see dynamic_chunk_unloading_system) ===
|
||||
),
|
||||
)
|
||||
.add_systems(
|
||||
|
||||
@@ -107,8 +107,10 @@ impl FloorTilePrefab {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn spawn(self, commands: &mut Commands) {
|
||||
commands.spawn((self.tile, self.transform, self.tile_state, self.visibility));
|
||||
pub fn spawn(self, commands: &mut Commands) -> Entity {
|
||||
commands
|
||||
.spawn((self.tile, self.transform, self.tile_state, self.visibility))
|
||||
.id()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ pub struct ChunkZKey {
|
||||
|
||||
impl ChunkZKey {
|
||||
#[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 {
|
||||
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,
|
||||
|
||||
@@ -232,14 +232,14 @@ impl TileMap {
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
let chunk_pos = world_to_chunk(world_pos);
|
||||
if let Some(chunk) = self.chunks.get(&chunk_pos) {
|
||||
let (local_x, local_y, z) = ChunkData::world_to_local(world_pos);
|
||||
return chunk.is_standable(local_x, local_y, z);
|
||||
}
|
||||
self.is_standable_slow(world_pos)
|
||||
let Some(chunk) = self.chunks.get(&chunk_pos) else {
|
||||
return false;
|
||||
};
|
||||
let (local_x, local_y, z) = ChunkData::world_to_local(world_pos);
|
||||
chunk.is_standable(local_x, local_y, z)
|
||||
}
|
||||
|
||||
/// Fallback standability check using HashMap lookups.
|
||||
@@ -283,4 +283,28 @@ impl TileMap {
|
||||
.map(|t| t.astar_weight)
|
||||
.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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user