- Add StandableBitGrid: O(1) bit-packed snapshot (~6KB per 50k tiles vs HashMap overhead) - Implement two-tier pathfinding: sync for short paths (<64 tiles), provisional+async for long paths - calculate_provisional_path: capped A* returning path to best heuristic node - calculate_async_path: A* using bit-grid (Send+Sync, no thread_local) - prepare_paths system: dispatches provisional paths immediately, spawns async for full paths - poll_async_paths + splice_completed_async_paths: seamless path transition when async completes - Entities start walking immediately on provisional path while full path computes in background Architecture: FixedUpdate: prepare_paths → update_wandering_targets → movement PostUpdate: poll_async_paths → splice_completed_async_paths Priority: DF-like pathing (immediate movement) > performance > memory
293 lines
7.8 KiB
Rust
293 lines
7.8 KiB
Rust
use bevy::prelude::*;
|
|
use rustc_hash::FxHashMap;
|
|
|
|
use crate::constants::ITILE_SIZE;
|
|
|
|
/// Packed floor tile data for efficient storage. ~35 bytes vs 76 bytes tuple.
|
|
#[derive(Clone, Copy, Debug)]
|
|
pub struct FloorTileData {
|
|
pub id: u8,
|
|
/// bit0=can_stand_in, bit1=can_stand_on, bit2=visibly_transparent
|
|
pub flags: u8,
|
|
pub astar_weight: u8,
|
|
pub visible_range: [u32; 8],
|
|
}
|
|
|
|
impl Default for FloorTileData {
|
|
fn default() -> Self {
|
|
Self {
|
|
id: 0,
|
|
flags: 0b001,
|
|
astar_weight: 0,
|
|
visible_range: [0; 8],
|
|
}
|
|
}
|
|
}
|
|
|
|
impl FloorTileData {
|
|
pub fn new(
|
|
id: u8,
|
|
can_stand_in: bool,
|
|
can_stand_on: bool,
|
|
visibly_transparent: bool,
|
|
astar_weight: u8,
|
|
visible_range: [u32; 8],
|
|
) -> Self {
|
|
let mut flags = 0u8;
|
|
if can_stand_in {
|
|
flags |= 0b001;
|
|
}
|
|
if can_stand_on {
|
|
flags |= 0b010;
|
|
}
|
|
if visibly_transparent {
|
|
flags |= 0b100;
|
|
}
|
|
Self {
|
|
id,
|
|
flags,
|
|
astar_weight,
|
|
visible_range,
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
pub fn can_stand_in(&self) -> bool {
|
|
self.flags & 0b001 != 0
|
|
}
|
|
#[inline]
|
|
pub fn can_stand_on(&self) -> bool {
|
|
self.flags & 0b010 != 0
|
|
}
|
|
#[inline]
|
|
pub fn visibly_transparent(&self) -> bool {
|
|
self.flags & 0b100 != 0
|
|
}
|
|
|
|
#[inline]
|
|
pub fn set_can_stand_in(&mut self, value: bool) {
|
|
if value {
|
|
self.flags |= 0b001;
|
|
} else {
|
|
self.flags &= !0b001;
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
pub fn set_can_stand_on(&mut self, value: bool) {
|
|
if value {
|
|
self.flags |= 0b010;
|
|
} else {
|
|
self.flags &= !0b010;
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
pub fn set_visibly_transparent(&mut self, value: bool) {
|
|
if value {
|
|
self.flags |= 0b100;
|
|
} else {
|
|
self.flags &= !0b100;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Packed fixture tile data. ~18 bytes vs 48 bytes tuple.
|
|
#[derive(Clone, Copy, Debug)]
|
|
pub struct FixtureTileData {
|
|
pub id: u8,
|
|
/// bit0=can_stand_in, bit1=can_stand_on
|
|
pub flags: u8,
|
|
pub visible_range: [u32; 8],
|
|
}
|
|
|
|
impl Default for FixtureTileData {
|
|
fn default() -> Self {
|
|
Self {
|
|
id: 0,
|
|
flags: 0,
|
|
visible_range: [0; 8],
|
|
}
|
|
}
|
|
}
|
|
|
|
impl FixtureTileData {
|
|
pub fn new(id: u8, can_stand_in: bool, can_stand_on: bool, visible_range: [u32; 8]) -> Self {
|
|
let mut flags = 0u8;
|
|
if can_stand_in {
|
|
flags |= 0b001;
|
|
}
|
|
if can_stand_on {
|
|
flags |= 0b010;
|
|
}
|
|
Self {
|
|
id,
|
|
flags,
|
|
visible_range,
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
pub fn can_stand_in(&self) -> bool {
|
|
self.flags & 0b001 != 0
|
|
}
|
|
#[inline]
|
|
pub fn can_stand_on(&self) -> bool {
|
|
self.flags & 0b010 != 0
|
|
}
|
|
}
|
|
|
|
/// Tile map using FxHashMap for fast lookups. No Arc wrapper - single-threaded access.
|
|
#[derive(Resource, Default)]
|
|
pub struct TileMap {
|
|
pub floor_tiles: FxHashMap<IVec3, FloorTileData>,
|
|
pub fixture_tiles: FxHashMap<IVec3, FixtureTileData>,
|
|
pub item_tiles: FxHashMap<IVec3, Vec<u32>>,
|
|
}
|
|
|
|
impl TileMap {
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
#[inline]
|
|
pub fn get_floor(&self, pos: &IVec3) -> Option<&FloorTileData> {
|
|
self.floor_tiles.get(pos)
|
|
}
|
|
|
|
#[inline]
|
|
pub fn get_fixture(&self, pos: &IVec3) -> Option<&FixtureTileData> {
|
|
self.fixture_tiles.get(pos)
|
|
}
|
|
|
|
#[inline]
|
|
pub fn has_floor(&self, pos: &IVec3) -> bool {
|
|
self.floor_tiles.contains_key(pos)
|
|
}
|
|
|
|
#[inline]
|
|
pub fn has_fixture(&self, pos: &IVec3) -> bool {
|
|
self.fixture_tiles.contains_key(pos)
|
|
}
|
|
|
|
#[inline]
|
|
pub fn insert_floor(&mut self, pos: IVec3, tile: FloorTileData) {
|
|
self.floor_tiles.insert(pos, tile);
|
|
}
|
|
|
|
#[inline]
|
|
pub fn insert_fixture(&mut self, pos: IVec3, tile: FixtureTileData) {
|
|
self.fixture_tiles.insert(pos, tile);
|
|
}
|
|
|
|
#[inline]
|
|
pub fn insert_item(&mut self, pos: IVec3, entity_id: u32) {
|
|
self.item_tiles.entry(pos).or_default().push(entity_id);
|
|
}
|
|
|
|
#[inline]
|
|
pub fn remove_item(&mut self, pos: &IVec3) -> Option<Vec<u32>> {
|
|
self.item_tiles.remove(pos)
|
|
}
|
|
|
|
#[inline]
|
|
pub fn get_floor_mut(&mut self, pos: &IVec3) -> Option<&mut FloorTileData> {
|
|
self.floor_tiles.get_mut(pos)
|
|
}
|
|
}
|
|
|
|
/// Bit-packed bounding-box snapshot for async pathfinding.
|
|
/// 1 bit per tile = ~6KB for 50,000 tiles vs HashMap overhead.
|
|
/// Must be Send+Sync — no RefCell, no Arc.
|
|
#[derive(Clone, Debug)]
|
|
pub struct StandableBitGrid {
|
|
pub origin: IVec3,
|
|
pub size: UVec3,
|
|
pub bits: Vec<u64>,
|
|
}
|
|
|
|
impl StandableBitGrid {
|
|
/// Create a bit-grid snapshot of all standable tiles within bounding box.
|
|
/// origin: min corner (inclusive), snapped to ITILE_SIZE
|
|
/// size: dimensions in tiles (not pixels)
|
|
pub fn new(origin: IVec3, size: UVec3, tilemap: &TileMap) -> Self {
|
|
let total_bits = (size.x * size.y * size.z) as usize;
|
|
let words = (total_bits + 63) / 64;
|
|
let mut bits = vec![0u64; words];
|
|
|
|
for bz in 0..size.z {
|
|
for by in 0..size.y {
|
|
for bx in 0..size.x {
|
|
let pos = IVec3::new(
|
|
origin.x + (bx as i32) * ITILE_SIZE,
|
|
origin.y + (by as i32) * ITILE_SIZE,
|
|
origin.z + (bz as i32) * ITILE_SIZE,
|
|
);
|
|
if Self::tile_is_standable(tilemap, pos) {
|
|
let idx = ((bz * size.y * size.x) + (by * size.x) + bx) as usize;
|
|
bits[idx / 64] |= 1u64 << (idx % 64);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Self { origin, size, bits }
|
|
}
|
|
|
|
/// Standable check using TileMap (mirrors pathfinding.rs::is_standable_tile)
|
|
#[inline]
|
|
fn tile_is_standable(tilemap: &TileMap, pos: IVec3) -> bool {
|
|
let can_stand_in_tile = tilemap
|
|
.floor_tiles
|
|
.get(&pos)
|
|
.map(|t| t.can_stand_in())
|
|
.unwrap_or(false);
|
|
let can_stand_in_fixture = tilemap
|
|
.fixture_tiles
|
|
.get(&pos)
|
|
.map(|t| t.can_stand_in())
|
|
.unwrap_or(false);
|
|
let pos_below = pos - IVec3::new(0, 0, ITILE_SIZE);
|
|
let can_stand_on_tile_below = tilemap
|
|
.floor_tiles
|
|
.get(&pos_below)
|
|
.map(|t| t.can_stand_on())
|
|
.unwrap_or(false);
|
|
let can_stand_on_fixture_below = tilemap
|
|
.fixture_tiles
|
|
.get(&pos_below)
|
|
.map(|t| t.can_stand_on())
|
|
.unwrap_or(false);
|
|
|
|
(can_stand_in_tile || can_stand_in_fixture)
|
|
&& (can_stand_on_tile_below || can_stand_on_fixture_below)
|
|
}
|
|
|
|
/// O(1) standable check using bit-grid coordinates.
|
|
#[inline]
|
|
pub fn is_standable_at(&self, bx: u32, by: u32, bz: u32) -> bool {
|
|
if bx >= self.size.x || by >= self.size.y || bz >= self.size.z {
|
|
return false;
|
|
}
|
|
let idx = ((bz * self.size.y * self.size.x) + (by * self.size.x) + bx) as usize;
|
|
self.bits[idx / 64] & (1u64 << (idx % 64)) != 0
|
|
}
|
|
|
|
/// Convert IVec3 world position to bit-grid coordinates.
|
|
/// Returns None if position is outside the grid bounds.
|
|
#[inline]
|
|
pub fn to_bit_coords(&self, pos: IVec3) -> Option<(u32, u32, u32)> {
|
|
let local = pos - self.origin;
|
|
if local.x < 0 || local.y < 0 || local.z < 0 {
|
|
return None;
|
|
}
|
|
let bx = (local.x / ITILE_SIZE) as u32;
|
|
let by = (local.y / ITILE_SIZE) as u32;
|
|
let bz = (local.z / ITILE_SIZE) as u32;
|
|
if bx >= self.size.x || by >= self.size.y || bz >= self.size.z {
|
|
return None;
|
|
}
|
|
Some((bx, by, bz))
|
|
}
|
|
}
|