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::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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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?}",
|
||||||
|
|||||||
@@ -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
@@ -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(
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user