feat: data-oriented chunks optimization with tile registry

- Phase 1: Bit-packed standability (ChunkData with bitsets)
- Phase 2: Reactive connectivity (dirty chunks)
- Phase 3: Async terrain baking (AsyncComputeTaskPool)
- Pathfinding weight system (rock=50 preferred, bedrock=150 avoided)
- Movement speed affected by tile weight
- External tiles.toml for hot-reloadable tile definitions
- TileRegistry singleton for async-safe tile lookups
- Fixed world_to_chunk to use CHUNK_SIZE_TILE (128) not CHUNK_SIZE (8)
- Fixed infinite spawner with Local<bool> state guards
- Fixed spawn coordinate grid alignment

Note: Zigzag pathfinding bug introduced - needs investigation
This commit is contained in:
2026-03-19 19:15:36 +00:00
parent 50788af3c7
commit 9535d65bd7
14 changed files with 423 additions and 84 deletions
+21
View File
@@ -46,6 +46,10 @@ pub struct ChunkData {
/// Tile IDs for rendering.
pub tile_ids: Vec<u8>,
/// A* pathfinding weights per tile. Lower = better path.
/// Weight 0 = impassable, weight 50 = fast (rock), weight 100 = normal (grass), weight 150 = slow (bedrock).
pub astar_weights: Vec<u8>,
}
impl ChunkData {
@@ -57,6 +61,7 @@ impl ChunkData {
stand_in_fixture: [0u32; BITSET_WORDS],
stand_on_fixture: [0u32; BITSET_WORDS],
tile_ids: vec![0u8; TOTAL_TILES],
astar_weights: vec![100u8; TOTAL_TILES],
}
}
@@ -184,6 +189,7 @@ impl ChunkData {
tile_id: u8,
can_stand_in: bool,
can_stand_on: bool,
astar_weight: u8,
) {
let idx = Self::pos_to_index(local_x, local_y, z);
let word = idx / 32;
@@ -203,6 +209,20 @@ impl ChunkData {
}
self.tile_ids[idx] = tile_id;
self.astar_weights[idx] = astar_weight;
}
/// Get A* weight at position. Returns 100 (default) if out of bounds.
#[inline]
pub fn get_astar_weight(&self, local_x: i32, local_y: i32, z: i32) -> u8 {
if local_x < 0 || local_x >= CHUNK_SIZE || local_y < 0 || local_y >= CHUNK_SIZE {
return 100;
}
if z < -(Z_BELOW as i32) || z > (Z_ABOVE as i32) {
return 100;
}
let idx = Self::pos_to_index(local_x, local_y, z);
self.astar_weights[idx]
}
/// Set only fixture standability bits (for forestry generation).
@@ -253,6 +273,7 @@ impl ChunkData {
self.stand_in_fixture.fill(0);
self.stand_on_fixture.fill(0);
self.tile_ids.fill(0);
self.astar_weights.fill(100);
}
}
+56 -29
View File
@@ -1,8 +1,11 @@
use bevy::prelude::*;
use crate::world::{
tiles::{FixtureTile, FloorTile, TileState},
FIXTURE_ID_OFFSET,
use crate::{
config::TileRegistry,
world::{
tiles::{FixtureTile, FloorTile, TileState},
FIXTURE_ID_OFFSET,
},
};
#[derive(Bundle)]
@@ -15,12 +18,15 @@ pub struct FloorTilePrefab {
impl FloorTilePrefab {
pub fn grass(position: Vec3) -> Self {
let def = TileRegistry::global().floor("grass");
FloorTilePrefab {
transform: Transform::from_translation(position),
tile: FloorTile {
id: 1,
astar_weight: 100,
..Default::default()
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),
@@ -30,12 +36,15 @@ impl FloorTilePrefab {
}
pub fn dirt(position: Vec3) -> Self {
let def = TileRegistry::global().floor("dirt");
FloorTilePrefab {
transform: Transform::from_translation(position),
tile: FloorTile {
id: 2,
astar_weight: 85,
..Default::default()
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),
@@ -45,12 +54,15 @@ impl FloorTilePrefab {
}
pub fn rock(position: Vec3) -> Self {
let def = TileRegistry::global().floor("rock");
FloorTilePrefab {
transform: Transform::from_translation(position),
tile: FloorTile {
id: 3,
astar_weight: 50,
..Default::default()
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),
@@ -60,13 +72,15 @@ impl FloorTilePrefab {
}
pub fn air(position: Vec3) -> Self {
let def = TileRegistry::global().floor("air");
FloorTilePrefab {
transform: Transform::from_translation(position),
tile: FloorTile {
id: 0,
opaque: false,
walkable: false,
..Default::default()
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),
@@ -76,12 +90,15 @@ impl FloorTilePrefab {
}
pub fn bedrock(position: Vec3) -> Self {
let def = TileRegistry::global().floor("bedrock");
FloorTilePrefab {
transform: Transform::from_translation(position),
tile: FloorTile {
id: 4,
astar_weight: 150,
..Default::default()
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),
@@ -104,55 +121,65 @@ pub struct FixtureTilePrefab {
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 + 1,
..Default::default()
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 + 2,
..Default::default()
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 + 4,
..Default::default()
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 + 5,
..Default::default()
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: 0,
..Default::default()
id: FIXTURE_ID_OFFSET + def.id,
solid: def.solid,
visible_range: [0; 8],
},
visibility: Visibility::Hidden,
}
+14
View File
@@ -269,4 +269,18 @@ impl TileMap {
(can_stand_in_floor || can_stand_in_fixture) && (can_stand_on_floor || can_stand_on_fixture)
}
/// Get A* pathfinding weight for a tile position.
/// Returns 100 (default) if tile not found. Lower is better.
pub fn get_astar_weight(&self, world_pos: IVec3) -> u8 {
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.get_astar_weight(local_x, local_y, z);
}
self.floor_tiles
.get(&world_pos)
.map(|t| t.astar_weight)
.unwrap_or(100)
}
}