1. CSV Overflow Panic: Use saturating_sub to prevent underflow when total_failed_paths > n 2. Heuristic Scale Mismatch: octile_distance_3d now divides by ITILE_SIZE to match g-score units 3. Goal Z Offset: Correct target.as_ivec3() - ivec3(0,0,1) not ITILE_SIZE (target already has +1.0) 4. Gravity Yo-Yo: Clear current_path and target when entity falls to prevent teleportation loop
1901 lines
67 KiB
Plaintext
1901 lines
67 KiB
Plaintext
diff --git a/Cargo.lock b/Cargo.lock
|
||
index d58b8d3..440fc3a 100644
|
||
--- a/Cargo.lock
|
||
+++ b/Cargo.lock
|
||
@@ -400,9 +400,9 @@ dependencies = [
|
||
|
||
[[package]]
|
||
name = "avif-serialize"
|
||
-version = "0.8.3"
|
||
+version = "0.8.8"
|
||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||
-checksum = "98922d6a4cfbcb08820c69d8eeccc05bb1f29bfa06b4f5b1dbfe9a868bd7608e"
|
||
+checksum = "375082f007bd67184fb9c0374614b29f9aaa604ec301635f72338bb65386a53d"
|
||
dependencies = [
|
||
"arrayvec",
|
||
]
|
||
@@ -415,9 +415,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||
|
||
[[package]]
|
||
name = "bevy"
|
||
-version = "0.18.0"
|
||
+version = "0.18.1"
|
||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||
-checksum = "ec689b5a79452b6f777b889bbff22d3216b82a8d2ab7814d4a0eb571e9938d97"
|
||
+checksum = "1fd310426290cec560221f9750c2f4484be4a8eeea7de3483c423329b465c40e"
|
||
dependencies = [
|
||
"bevy_internal",
|
||
]
|
||
@@ -1175,9 +1175,9 @@ dependencies = [
|
||
|
||
[[package]]
|
||
name = "bevy_platform"
|
||
-version = "0.18.0"
|
||
+version = "0.18.1"
|
||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||
-checksum = "9b29ea749a8e85f98186ab662f607b885b97c804bb14cdb0cdf838164496d474"
|
||
+checksum = "ec6b36504169b644acd26a5469fd8d371aa6f1d73ee5c01b1b1181ae1cefbf9b"
|
||
dependencies = [
|
||
"critical-section",
|
||
"foldhash 0.2.0",
|
||
@@ -1249,9 +1249,9 @@ checksum = "4f98cbc6d34bbdb58240b72ed1731931b4991a893b3a3238bb7c42ae054aa676"
|
||
|
||
[[package]]
|
||
name = "bevy_rand"
|
||
-version = "0.14.1"
|
||
+version = "0.14.2"
|
||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||
-checksum = "c76e5db2c274081fdff32a525609436bd7ac2c24b91c1a5d3bc04e43d7558ddf"
|
||
+checksum = "526ba09324b96fb64f275a0cb4f6112d36b6c916ce2f14e38d5a8739dd2ac7f9"
|
||
dependencies = [
|
||
"bevy_app",
|
||
"bevy_ecs",
|
||
@@ -2331,13 +2331,17 @@ dependencies = [
|
||
name = "dorf"
|
||
version = "0.1.0"
|
||
dependencies = [
|
||
+ "ahash",
|
||
"bevy",
|
||
"bevy_platform",
|
||
"bevy_rand",
|
||
+ "futures-lite",
|
||
"image",
|
||
+ "nohash-hasher",
|
||
"noise",
|
||
"rand 0.10.0",
|
||
"rayon",
|
||
+ "rustc-hash 2.1.1",
|
||
]
|
||
|
||
[[package]]
|
||
@@ -3023,9 +3027,9 @@ checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
|
||
|
||
[[package]]
|
||
name = "image"
|
||
-version = "0.25.9"
|
||
+version = "0.25.10"
|
||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||
-checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a"
|
||
+checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
|
||
dependencies = [
|
||
"bytemuck",
|
||
"byteorder-lite",
|
||
@@ -3041,8 +3045,8 @@ dependencies = [
|
||
"rayon",
|
||
"rgb",
|
||
"tiff",
|
||
- "zune-core 0.5.1",
|
||
- "zune-jpeg 0.5.12",
|
||
+ "zune-core",
|
||
+ "zune-jpeg",
|
||
]
|
||
|
||
[[package]]
|
||
@@ -3057,9 +3061,9 @@ dependencies = [
|
||
|
||
[[package]]
|
||
name = "imgref"
|
||
-version = "1.11.0"
|
||
+version = "1.12.0"
|
||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||
-checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408"
|
||
+checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8"
|
||
|
||
[[package]]
|
||
name = "indexmap"
|
||
@@ -3431,9 +3435,9 @@ dependencies = [
|
||
|
||
[[package]]
|
||
name = "moxcms"
|
||
-version = "0.7.5"
|
||
+version = "0.8.1"
|
||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||
-checksum = "ddd32fa8935aeadb8a8a6b6b351e40225570a37c43de67690383d87ef170cd08"
|
||
+checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b"
|
||
dependencies = [
|
||
"num-traits",
|
||
"pxfm",
|
||
@@ -3566,6 +3570,12 @@ dependencies = [
|
||
"libc",
|
||
]
|
||
|
||
+[[package]]
|
||
+name = "nohash-hasher"
|
||
+version = "0.2.0"
|
||
+source = "registry+https://github.com/rust-lang/crates.io-index"
|
||
+checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451"
|
||
+
|
||
[[package]]
|
||
name = "noise"
|
||
version = "0.9.0"
|
||
@@ -4415,9 +4425,9 @@ dependencies = [
|
||
|
||
[[package]]
|
||
name = "ravif"
|
||
-version = "0.12.0"
|
||
+version = "0.13.0"
|
||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||
-checksum = "ef69c1990ceef18a116855938e74793a5f7496ee907562bd0857b6ac734ab285"
|
||
+checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45"
|
||
dependencies = [
|
||
"avif-serialize",
|
||
"imgref",
|
||
@@ -4536,9 +4546,9 @@ checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832"
|
||
|
||
[[package]]
|
||
name = "rgb"
|
||
-version = "0.8.50"
|
||
+version = "0.8.53"
|
||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||
-checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a"
|
||
+checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4"
|
||
|
||
[[package]]
|
||
name = "rodio"
|
||
@@ -4993,16 +5003,16 @@ dependencies = [
|
||
|
||
[[package]]
|
||
name = "tiff"
|
||
-version = "0.10.3"
|
||
+version = "0.11.3"
|
||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||
-checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f"
|
||
+checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52"
|
||
dependencies = [
|
||
"fax",
|
||
"flate2",
|
||
"half",
|
||
"quick-error",
|
||
"weezl",
|
||
- "zune-jpeg 0.4.21",
|
||
+ "zune-jpeg",
|
||
]
|
||
|
||
[[package]]
|
||
@@ -6468,12 +6478,6 @@ dependencies = [
|
||
"syn",
|
||
]
|
||
|
||
-[[package]]
|
||
-name = "zune-core"
|
||
-version = "0.4.12"
|
||
-source = "registry+https://github.com/rust-lang/crates.io-index"
|
||
-checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a"
|
||
-
|
||
[[package]]
|
||
name = "zune-core"
|
||
version = "0.5.1"
|
||
@@ -6489,20 +6493,11 @@ dependencies = [
|
||
"simd-adler32",
|
||
]
|
||
|
||
-[[package]]
|
||
-name = "zune-jpeg"
|
||
-version = "0.4.21"
|
||
-source = "registry+https://github.com/rust-lang/crates.io-index"
|
||
-checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713"
|
||
-dependencies = [
|
||
- "zune-core 0.4.12",
|
||
-]
|
||
-
|
||
[[package]]
|
||
name = "zune-jpeg"
|
||
version = "0.5.12"
|
||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||
checksum = "410e9ecef634c709e3831c2cfdb8d9c32164fae1c67496d5b68fff728eec37fe"
|
||
dependencies = [
|
||
- "zune-core 0.5.1",
|
||
+ "zune-core",
|
||
]
|
||
diff --git a/Cargo.toml b/Cargo.toml
|
||
index ae71471..8522f33 100644
|
||
--- a/Cargo.toml
|
||
+++ b/Cargo.toml
|
||
@@ -4,13 +4,17 @@ version = "0.1.0"
|
||
edition = "2021"
|
||
|
||
[dependencies]
|
||
-bevy = { version = "0.18.0", features = ["wayland"] }
|
||
+bevy = { version = "0.18.1", features = ["wayland"] }
|
||
noise = "0.9.0"
|
||
rand = "0.10.0"
|
||
-bevy_rand = { version = "0.14.1", features = ["wyrand"] }
|
||
-image = "0.25.9"
|
||
-bevy_platform = "0.18.0"
|
||
+bevy_rand = { version = "0.14.2", features = ["wyrand"] }
|
||
+image = "0.25.10"
|
||
+bevy_platform = "0.18.1"
|
||
rayon = "1.11.0"
|
||
+rustc-hash = "2.1.1"
|
||
+ahash = "0.8.12"
|
||
+nohash-hasher = "0.2.0"
|
||
+futures-lite = "2.6"
|
||
|
||
# Enable max optimizations for dependencies, but not for our code:
|
||
[profile.dev.package."*"]
|
||
diff --git a/src/constants.rs b/src/constants.rs
|
||
index a0c34b2..7a22b57 100644
|
||
--- a/src/constants.rs
|
||
+++ b/src/constants.rs
|
||
@@ -3,3 +3,8 @@ pub const TILE_PIXELS: u32 = 16;
|
||
pub const TILE_SIZE: f32 = TILE_PIXELS as f32 * PIXEL_RATIO;
|
||
pub const ITILE_SIZE: i32 = TILE_SIZE as i32;
|
||
pub const SEED: u32 = 420;
|
||
+
|
||
+pub const PATHFINDER_SHORT_PATH_MAX_TILES: i32 = 64;
|
||
+pub const PATHFINDER_MAX_NODES: usize = 5000;
|
||
+pub const PATHFINDER_WAYPOINT_THRESHOLD_TILES: i32 = 100;
|
||
+pub const PATHFINDER_PROVISIONAL_NODE_LIMIT: usize = 64;
|
||
diff --git a/src/entities/item/prefabs/misc/misc_prefabs.rs b/src/entities/item/prefabs/misc/misc_prefabs.rs
|
||
index 39d9e16..f5d9706 100644
|
||
--- a/src/entities/item/prefabs/misc/misc_prefabs.rs
|
||
+++ b/src/entities/item/prefabs/misc/misc_prefabs.rs
|
||
@@ -41,15 +41,7 @@ pub fn spawn_prefab(
|
||
},
|
||
})
|
||
.id();
|
||
- let mut items = tilemap
|
||
- .item_tiles
|
||
- .get(&position.as_ivec3())
|
||
- .unwrap_or(&Vec::new())
|
||
- .clone();
|
||
- items.push(meat.index_u32());
|
||
- tilemap
|
||
- .item_tiles
|
||
- .insert(position.as_ivec3(), items.clone());
|
||
+ tilemap.insert_item(position.as_ivec3(), meat.index_u32());
|
||
return commands
|
||
.entity(meat)
|
||
.insert(VisibleGameEntity)
|
||
@@ -76,15 +68,7 @@ pub fn spawn_prefab(
|
||
},
|
||
})
|
||
.id();
|
||
- let mut items = tilemap
|
||
- .item_tiles
|
||
- .get(&position.as_ivec3())
|
||
- .unwrap_or(&Vec::new())
|
||
- .clone();
|
||
- items.push(coin.index_u32());
|
||
- tilemap
|
||
- .item_tiles
|
||
- .insert(position.as_ivec3(), items.clone());
|
||
+ tilemap.insert_item(position.as_ivec3(), coin.index_u32());
|
||
commands
|
||
.entity(coin)
|
||
.insert(VisibleGameEntity)
|
||
diff --git a/src/entities/item/systems.rs b/src/entities/item/systems.rs
|
||
index 5bc0eef..fc3f536 100644
|
||
--- a/src/entities/item/systems.rs
|
||
+++ b/src/entities/item/systems.rs
|
||
@@ -116,13 +116,13 @@ pub fn item_tile_management_system(
|
||
|
||
for position in positions {
|
||
let items = match tilemap.item_tiles.get(&position) {
|
||
- Some(i) => i,
|
||
+ Some(i) => i.clone(),
|
||
None => continue,
|
||
};
|
||
|
||
// Clean up empty tiles
|
||
if items.is_empty() {
|
||
- tilemap.item_tiles.remove(&position);
|
||
+ tilemap.remove_item(&position);
|
||
rotation_timer.current_indices.remove(&position);
|
||
continue;
|
||
}
|
||
@@ -146,8 +146,8 @@ pub fn item_tile_management_system(
|
||
.copied()
|
||
.unwrap_or(0);
|
||
|
||
- for &entity_id in items {
|
||
- if let Some(entity) = Entity::from_raw_u32(entity_id) {
|
||
+ for entity_id in &items {
|
||
+ if let Some(entity) = Entity::from_raw_u32(*entity_id) {
|
||
if let Ok(mut rotation_state) = item_query.get_mut(entity) {
|
||
rotation_state.should_be_visible = false;
|
||
}
|
||
diff --git a/src/entities/shared_components/ambulatory.rs b/src/entities/shared_components/ambulatory.rs
|
||
index 8c7ae65..5b41470 100644
|
||
--- a/src/entities/shared_components/ambulatory.rs
|
||
+++ b/src/entities/shared_components/ambulatory.rs
|
||
@@ -9,3 +9,21 @@ pub struct Ambulatory {
|
||
pub target: Option<Vec3>,
|
||
pub step_recovery: u32,
|
||
}
|
||
+
|
||
+#[derive(Component)]
|
||
+pub struct PendingPath {
|
||
+ pub start: IVec3,
|
||
+ pub goal: IVec3,
|
||
+ pub waypoint_path: Vec<Vec3>,
|
||
+ pub request_id: u64,
|
||
+}
|
||
+
|
||
+#[derive(Resource, Default)]
|
||
+pub struct PathRequestCounter {
|
||
+ pub next_id: u64,
|
||
+}
|
||
+
|
||
+#[derive(Resource, Default)]
|
||
+pub struct CompletedPaths {
|
||
+ pub paths: Vec<(u64, Vec<Vec3>)>,
|
||
+}
|
||
diff --git a/src/entities/shared_systems/pathfinding.rs b/src/entities/shared_systems/pathfinding.rs
|
||
index ee8c897..36437a0 100644
|
||
--- a/src/entities/shared_systems/pathfinding.rs
|
||
+++ b/src/entities/shared_systems/pathfinding.rs
|
||
@@ -1,14 +1,91 @@
|
||
-use crate::constants::TILE_SIZE;
|
||
+use bevy::prelude::*;
|
||
+use rayon::prelude::*;
|
||
+use rustc_hash::FxHashMap;
|
||
+use rustc_hash::FxHashSet;
|
||
+use std::{cell::RefCell, collections::BinaryHeap, collections::VecDeque, time::Instant};
|
||
+
|
||
+use crate::constants::{
|
||
+ ITILE_SIZE, PATHFINDER_MAX_NODES, PATHFINDER_PROVISIONAL_NODE_LIMIT, TILE_SIZE,
|
||
+};
|
||
use crate::world::tiles::TileMap;
|
||
use crate::world::{chunks::ChunkMap, chunks::CHUNK_SIZE};
|
||
use crate::{constants::*, entities::shared_components::Ambulatory};
|
||
-use bevy::{math::ivec3, prelude::*};
|
||
+use bevy::math::ivec3;
|
||
use bevy_rand::prelude::*;
|
||
use rand::RngExt;
|
||
-use std::{
|
||
- collections::{BinaryHeap, HashMap, HashSet},
|
||
- process::exit,
|
||
-};
|
||
+
|
||
+thread_local! {
|
||
+ static LOCAL_PATH_TIMES: RefCell<Vec<u128>> = const { RefCell::new(Vec::new()) };
|
||
+ static LOCAL_PATH_LENGTHS: RefCell<Vec<usize>> = const { RefCell::new(Vec::new()) };
|
||
+ static LOCAL_NODES_EXPANDED: RefCell<Vec<usize>> = const { RefCell::new(Vec::new()) };
|
||
+ static LOCAL_FAILED_PATHS: RefCell<u64> = const { RefCell::new(0) };
|
||
+}
|
||
+
|
||
+/// Single consolidated scratchpad for A* pathfinding.
|
||
+/// One RefCell borrow instead of multiple nested borrows.
|
||
+struct AStarScratchpad {
|
||
+ g_scores: FxHashMap<IVec3, i32>,
|
||
+ came_from: FxHashMap<IVec3, IVec3>,
|
||
+ closed_set: FxHashSet<IVec3>,
|
||
+ open_set: BinaryHeap<PathNode>,
|
||
+}
|
||
+
|
||
+impl Default for AStarScratchpad {
|
||
+ fn default() -> Self {
|
||
+ Self {
|
||
+ g_scores: FxHashMap::default(),
|
||
+ came_from: FxHashMap::default(),
|
||
+ closed_set: FxHashSet::default(),
|
||
+ open_set: BinaryHeap::new(),
|
||
+ }
|
||
+ }
|
||
+}
|
||
+
|
||
+impl AStarScratchpad {
|
||
+ fn clear_and_reserve(&mut self, capacity: usize) {
|
||
+ self.g_scores.clear();
|
||
+ self.came_from.clear();
|
||
+ self.closed_set.clear();
|
||
+ self.open_set.clear();
|
||
+
|
||
+ if self.g_scores.capacity() < capacity {
|
||
+ self.g_scores.reserve(capacity);
|
||
+ self.came_from.reserve(capacity);
|
||
+ self.closed_set.reserve(capacity);
|
||
+ }
|
||
+ }
|
||
+}
|
||
+
|
||
+thread_local! {
|
||
+ static SCRATCHPAD: RefCell<AStarScratchpad> = RefCell::new(AStarScratchpad::default());
|
||
+}
|
||
+
|
||
+const ALLOWED_MOVES: [IVec3; 24] = [
|
||
+ IVec3::new(-ITILE_SIZE, 0, 0),
|
||
+ IVec3::new(ITILE_SIZE, 0, 0),
|
||
+ IVec3::new(0, -ITILE_SIZE, 0),
|
||
+ IVec3::new(0, ITILE_SIZE, 0),
|
||
+ IVec3::new(-ITILE_SIZE, -ITILE_SIZE, 0),
|
||
+ IVec3::new(-ITILE_SIZE, ITILE_SIZE, 0),
|
||
+ IVec3::new(ITILE_SIZE, -ITILE_SIZE, 0),
|
||
+ IVec3::new(ITILE_SIZE, ITILE_SIZE, 0),
|
||
+ IVec3::new(-ITILE_SIZE, 0, ITILE_SIZE),
|
||
+ IVec3::new(-ITILE_SIZE, 0, -ITILE_SIZE),
|
||
+ IVec3::new(ITILE_SIZE, 0, ITILE_SIZE),
|
||
+ IVec3::new(ITILE_SIZE, 0, -ITILE_SIZE),
|
||
+ IVec3::new(0, -ITILE_SIZE, ITILE_SIZE),
|
||
+ IVec3::new(0, -ITILE_SIZE, -ITILE_SIZE),
|
||
+ IVec3::new(0, ITILE_SIZE, ITILE_SIZE),
|
||
+ IVec3::new(0, ITILE_SIZE, -ITILE_SIZE),
|
||
+ IVec3::new(-ITILE_SIZE, -ITILE_SIZE, ITILE_SIZE),
|
||
+ IVec3::new(-ITILE_SIZE, -ITILE_SIZE, -ITILE_SIZE),
|
||
+ IVec3::new(-ITILE_SIZE, ITILE_SIZE, ITILE_SIZE),
|
||
+ IVec3::new(-ITILE_SIZE, ITILE_SIZE, -ITILE_SIZE),
|
||
+ IVec3::new(ITILE_SIZE, -ITILE_SIZE, ITILE_SIZE),
|
||
+ IVec3::new(ITILE_SIZE, -ITILE_SIZE, -ITILE_SIZE),
|
||
+ IVec3::new(ITILE_SIZE, ITILE_SIZE, ITILE_SIZE),
|
||
+ IVec3::new(ITILE_SIZE, ITILE_SIZE, -ITILE_SIZE),
|
||
+];
|
||
|
||
#[derive(Clone, Eq, PartialEq, Debug)]
|
||
struct PathNode {
|
||
@@ -19,7 +96,10 @@ struct PathNode {
|
||
|
||
impl Ord for PathNode {
|
||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||
- other.f_score.cmp(&self.f_score)
|
||
+ other
|
||
+ .f_score
|
||
+ .cmp(&self.f_score)
|
||
+ .then_with(|| other.g_score.cmp(&self.g_score))
|
||
}
|
||
}
|
||
|
||
@@ -29,58 +109,233 @@ impl PartialOrd for PathNode {
|
||
}
|
||
}
|
||
|
||
+#[derive(Resource, Default)]
|
||
+pub struct PathfindingBenchmark {
|
||
+ pub path_calc_times_us: Vec<u128>,
|
||
+ pub path_lengths: Vec<usize>,
|
||
+ pub nodes_expanded: Vec<usize>,
|
||
+ pub movement_system_times_us: Vec<u128>,
|
||
+ pub wander_system_times_us: Vec<u128>,
|
||
+ pub total_paths_calculated: u64,
|
||
+ pub total_failed_paths: u64,
|
||
+ pub report_every_n: u32,
|
||
+ pub sample_count: u32,
|
||
+}
|
||
+
|
||
+impl PathfindingBenchmark {
|
||
+ pub fn new(report_every_n: u32) -> Self {
|
||
+ Self {
|
||
+ report_every_n,
|
||
+ ..Default::default()
|
||
+ }
|
||
+ }
|
||
+}
|
||
+
|
||
+#[derive(Resource, Default)]
|
||
+pub struct PathRequestQueue {
|
||
+ pub pending: VecDeque<(Entity, IVec3, IVec3)>,
|
||
+}
|
||
+
|
||
+const MAX_PATHS_PER_FRAME: usize = 8;
|
||
+
|
||
pub struct PathfindingPlugin;
|
||
|
||
impl Plugin for PathfindingPlugin {
|
||
fn build(&self, app: &mut App) {
|
||
- app.add_systems(FixedUpdate, (update_wandering_targets, movement).chain());
|
||
+ app.insert_resource(PathfindingBenchmark::new(100))
|
||
+ .insert_resource(crate::entities::shared_components::CompletedPaths::default())
|
||
+ .insert_resource(crate::entities::shared_components::PathRequestCounter::default())
|
||
+ .insert_resource(PathRequestQueue::default())
|
||
+ .add_systems(
|
||
+ FixedUpdate,
|
||
+ (prepare_paths, update_wandering_targets, movement).chain(),
|
||
+ )
|
||
+ .add_systems(
|
||
+ PostUpdate,
|
||
+ (
|
||
+ merge_benchmark_stats,
|
||
+ process_completed_paths,
|
||
+ process_path_queue,
|
||
+ ),
|
||
+ )
|
||
+ .add_systems(Update, bench_report_system);
|
||
+ }
|
||
+}
|
||
+
|
||
+pub fn prepare_paths(
|
||
+ mut commands: Commands,
|
||
+ mut queue: ResMut<PathRequestQueue>,
|
||
+ mut query: Query<
|
||
+ (
|
||
+ Entity,
|
||
+ &mut crate::entities::shared_components::Ambulatory,
|
||
+ &Transform,
|
||
+ ),
|
||
+ Without<crate::entities::shared_components::PendingPath>,
|
||
+ >,
|
||
+ tilemap: Res<TileMap>,
|
||
+) {
|
||
+ for (entity, mut ambulatory, transform) in query.iter_mut() {
|
||
+ if ambulatory.current_path.is_some() || ambulatory.target.is_none() {
|
||
+ continue;
|
||
+ }
|
||
+ let Some(target) = ambulatory.target else {
|
||
+ continue;
|
||
+ };
|
||
+ let start = transform.translation.as_ivec3();
|
||
+ let goal = target.as_ivec3() - ivec3(0, 0, ITILE_SIZE);
|
||
+ let distance = octile_distance_3d(start, goal) / ITILE_SIZE;
|
||
+
|
||
+ if distance <= PATHFINDER_SHORT_PATH_MAX_TILES {
|
||
+ let path = calculate_path_benchmarked(&tilemap, start, goal);
|
||
+ ambulatory.current_path = Some(path);
|
||
+ ambulatory.path_index = 0;
|
||
+ } else {
|
||
+ let provisional = calculate_provisional_path(
|
||
+ &tilemap,
|
||
+ start,
|
||
+ goal,
|
||
+ PATHFINDER_PROVISIONAL_NODE_LIMIT,
|
||
+ );
|
||
+ if !provisional.is_empty() {
|
||
+ ambulatory.current_path = Some(provisional);
|
||
+ ambulatory.path_index = 0;
|
||
+
|
||
+ queue.pending.push_back((entity, start, goal));
|
||
+ commands
|
||
+ .entity(entity)
|
||
+ .insert(crate::entities::shared_components::PendingPath {
|
||
+ start,
|
||
+ goal,
|
||
+ waypoint_path: Vec::new(),
|
||
+ request_id: 0,
|
||
+ });
|
||
+ } else {
|
||
+ let path = calculate_path_benchmarked(&tilemap, start, goal);
|
||
+ ambulatory.current_path = Some(path);
|
||
+ ambulatory.path_index = 0;
|
||
+ }
|
||
+ }
|
||
+ }
|
||
+}
|
||
+
|
||
+pub fn process_path_queue(
|
||
+ mut commands: Commands,
|
||
+ mut queue: ResMut<PathRequestQueue>,
|
||
+ tilemap: Res<TileMap>,
|
||
+ mut query: Query<
|
||
+ (Entity, &mut Ambulatory, &Transform),
|
||
+ With<crate::entities::shared_components::PendingPath>,
|
||
+ >,
|
||
+) {
|
||
+ let mut processed = 0;
|
||
+ while processed < MAX_PATHS_PER_FRAME {
|
||
+ if let Some((entity, _old_start, goal)) = queue.pending.pop_front() {
|
||
+ processed += 1;
|
||
+
|
||
+ if let Ok((_, mut ambulatory, transform)) = query.get_mut(entity) {
|
||
+ let actual_start = transform.translation.as_ivec3();
|
||
+ let full_path = calculate_path_benchmarked(&tilemap, actual_start, goal);
|
||
+
|
||
+ if !full_path.is_empty() {
|
||
+ ambulatory.current_path = Some(full_path);
|
||
+ ambulatory.path_index = 0;
|
||
+ }
|
||
+ commands
|
||
+ .entity(entity)
|
||
+ .remove::<crate::entities::shared_components::PendingPath>();
|
||
+ }
|
||
+ } else {
|
||
+ break;
|
||
+ }
|
||
+ }
|
||
+}
|
||
+
|
||
+pub fn process_completed_paths(
|
||
+ mut completed: ResMut<crate::entities::shared_components::CompletedPaths>,
|
||
+ mut query: Query<(
|
||
+ Entity,
|
||
+ &mut crate::entities::shared_components::Ambulatory,
|
||
+ &mut crate::entities::shared_components::PendingPath,
|
||
+ )>,
|
||
+ mut commands: Commands,
|
||
+) {
|
||
+ for (request_id, path) in completed.paths.drain(..) {
|
||
+ for (entity, mut ambulatory, pending) in query.iter_mut() {
|
||
+ if pending.request_id == request_id {
|
||
+ ambulatory.current_path = Some(path.clone());
|
||
+ commands
|
||
+ .entity(entity)
|
||
+ .remove::<crate::entities::shared_components::PendingPath>();
|
||
+ }
|
||
+ }
|
||
}
|
||
}
|
||
|
||
+pub fn merge_benchmark_stats(mut bench: ResMut<PathfindingBenchmark>) {
|
||
+ LOCAL_PATH_TIMES.with(|t| {
|
||
+ let mut times = t.borrow_mut();
|
||
+ bench.path_calc_times_us.extend(times.iter());
|
||
+ bench.total_paths_calculated += times.len() as u64;
|
||
+ times.clear();
|
||
+ });
|
||
+ LOCAL_PATH_LENGTHS.with(|l| {
|
||
+ let mut lengths = l.borrow_mut();
|
||
+ bench.path_lengths.extend(lengths.iter());
|
||
+ lengths.clear();
|
||
+ });
|
||
+ LOCAL_NODES_EXPANDED.with(|n| {
|
||
+ let mut nodes = n.borrow_mut();
|
||
+ bench.nodes_expanded.extend(nodes.iter());
|
||
+ nodes.clear();
|
||
+ });
|
||
+ LOCAL_FAILED_PATHS.with(|f| {
|
||
+ let mut failed = f.borrow_mut();
|
||
+ bench.total_failed_paths += *failed;
|
||
+ *failed = 0;
|
||
+ });
|
||
+}
|
||
+
|
||
pub fn update_wandering_targets(
|
||
- mut query: Query<(&mut Ambulatory, &Transform)>, // add a 'with' here when behaviours are implemented
|
||
+ mut query: Query<(&mut Ambulatory, &Transform)>,
|
||
tilemap: Res<TileMap>,
|
||
chunk_map: Res<ChunkMap>,
|
||
mut rng_q: Query<&mut WyRand, With<GlobalRng>>,
|
||
) {
|
||
- if let Ok(mut rng) = rng_q.single_mut() {
|
||
- for (mut ambulatory, _) in query.iter_mut() {
|
||
- if ambulatory.target.is_none()
|
||
- || (ambulatory.current_path.is_some()
|
||
- && ambulatory.path_index >= ambulatory.current_path.as_ref().unwrap().len())
|
||
- {
|
||
- // Find a random loaded chunk
|
||
- let loaded_chunks: Vec<&IVec2> = chunk_map.loaded_chunks.keys().collect();
|
||
- if !loaded_chunks.is_empty() {
|
||
- let random_index = rng.random_range(0..loaded_chunks.len());
|
||
- if let Some(&chunk_pos) = loaded_chunks.get(random_index) {
|
||
- // Generate random position within chunk
|
||
- let chunk_x = chunk_pos.x * CHUNK_SIZE;
|
||
- let chunk_y = chunk_pos.y * CHUNK_SIZE;
|
||
-
|
||
- let target_x = chunk_x + rng.random_range(0..CHUNK_SIZE);
|
||
- let target_y = chunk_y + rng.random_range(0..CHUNK_SIZE);
|
||
-
|
||
- // Get height at position
|
||
- let surface_height = 0;
|
||
-
|
||
- // Find a valid z-level near the surface
|
||
- for z in (surface_height - 3)..=(surface_height + 4) {
|
||
- let mut target_pos = IVec3::new(target_x, target_y, z) * ITILE_SIZE;
|
||
- if let Some(_) = tilemap.floor_tiles.get(&target_pos) {
|
||
- target_pos.z += ITILE_SIZE;
|
||
- if let Some(_base_texture) = tilemap.floor_tiles.get(&target_pos) {
|
||
- if is_standable_tile(&tilemap, target_pos) {
|
||
- ambulatory.target = Some(Vec3::new(
|
||
- target_pos.x as f32,
|
||
- target_pos.y as f32,
|
||
- target_pos.z as f32 + 1.0,
|
||
- ));
|
||
- ambulatory.current_path = None;
|
||
- ambulatory.path_index = 0;
|
||
- break;
|
||
- }
|
||
- }
|
||
+ let Ok(mut rng) = rng_q.single_mut() else {
|
||
+ return;
|
||
+ };
|
||
+
|
||
+ for (mut ambulatory, _) in query.iter_mut() {
|
||
+ if ambulatory.target.is_none()
|
||
+ || (ambulatory.current_path.is_some()
|
||
+ && ambulatory.path_index >= ambulatory.current_path.as_ref().unwrap().len())
|
||
+ {
|
||
+ let loaded_chunks: Vec<&IVec2> = chunk_map.loaded_chunks.keys().collect();
|
||
+ if !loaded_chunks.is_empty() {
|
||
+ let random_index = rng.random_range(0..loaded_chunks.len());
|
||
+ if let Some(&chunk_pos) = loaded_chunks.get(random_index) {
|
||
+ let chunk_x = chunk_pos.x * CHUNK_SIZE;
|
||
+ let chunk_y = chunk_pos.y * CHUNK_SIZE;
|
||
+
|
||
+ let target_x = chunk_x + rng.random_range(0..CHUNK_SIZE);
|
||
+ let target_y = chunk_y + rng.random_range(0..CHUNK_SIZE);
|
||
+
|
||
+ for z in -3..=4 {
|
||
+ let mut target_pos = IVec3::new(target_x, target_y, z) * ITILE_SIZE;
|
||
+ if tilemap.floor_tiles.get(&target_pos).is_some() {
|
||
+ target_pos.z += ITILE_SIZE;
|
||
+ if tilemap.floor_tiles.get(&target_pos).is_some()
|
||
+ && is_standable_tile(&tilemap, target_pos)
|
||
+ {
|
||
+ ambulatory.target = Some(Vec3::new(
|
||
+ target_pos.x as f32,
|
||
+ target_pos.y as f32,
|
||
+ target_pos.z as f32 + 1.0,
|
||
+ ));
|
||
+ ambulatory.current_path = None;
|
||
+ ambulatory.path_index = 0;
|
||
+ break;
|
||
}
|
||
}
|
||
}
|
||
@@ -95,289 +350,435 @@ pub fn movement(mut query: Query<(&mut Ambulatory, &mut Transform)>, tilemap: Re
|
||
.par_iter_mut()
|
||
.for_each(|(mut ambulatory, mut transform)| {
|
||
let current_pos = transform.translation;
|
||
- // Apply gravity if in air
|
||
if !is_standable_tile(&tilemap, current_pos.as_ivec3()) {
|
||
transform.translation.z -= TILE_SIZE;
|
||
return;
|
||
}
|
||
|
||
- if let Some(target) = ambulatory.target {
|
||
- // Calculate path if needed
|
||
- if ambulatory.current_path.is_none() {
|
||
- ambulatory.current_path = Some(calculate_path(
|
||
- &tilemap,
|
||
- transform.translation.as_ivec3(),
|
||
- target.as_ivec3() - ivec3(0, 0, 1),
|
||
- ));
|
||
- ambulatory.path_index = 0;
|
||
- }
|
||
- if ambulatory.walk_speed > 0. {
|
||
- if ambulatory.step_recovery <= ambulatory.walk_speed as u32 {
|
||
- ambulatory.step_recovery += 1;
|
||
- return;
|
||
- } else {
|
||
- ambulatory.step_recovery = 0;
|
||
- }
|
||
- }
|
||
+ if ambulatory.current_path.is_none() {
|
||
+ return;
|
||
+ }
|
||
|
||
- // Follow the current path
|
||
- if let Some(path) = &ambulatory.current_path {
|
||
- if ambulatory.path_index < path.len() {
|
||
- let next_point = path[ambulatory.path_index];
|
||
+ if ambulatory.walk_speed > 0. {
|
||
+ if ambulatory.step_recovery <= ambulatory.walk_speed as u32 {
|
||
+ ambulatory.step_recovery += 1;
|
||
+ return;
|
||
+ } else {
|
||
+ ambulatory.step_recovery = 0;
|
||
+ }
|
||
+ }
|
||
|
||
- let direction = (next_point - transform.translation).normalize();
|
||
- transform.translation = next_point;
|
||
+ if let Some(path) = &ambulatory.current_path {
|
||
+ if ambulatory.path_index < path.len() {
|
||
+ let next_point = path[ambulatory.path_index];
|
||
+ let direction = (next_point - transform.translation).normalize();
|
||
+ transform.translation = next_point;
|
||
|
||
- // Update sprite direction (only for x movement)
|
||
- if direction.x > 0.0 {
|
||
- transform.scale.x = PIXEL_RATIO;
|
||
- } else if direction.x < 0.0 {
|
||
- transform.scale.x = -PIXEL_RATIO;
|
||
- }
|
||
+ if direction.x > 0.0 {
|
||
+ transform.scale.x = PIXEL_RATIO;
|
||
+ } else if direction.x < 0.0 {
|
||
+ transform.scale.x = -PIXEL_RATIO;
|
||
+ }
|
||
|
||
- // Check if we've reached the next point
|
||
- if transform.translation.distance(next_point) < TILE_SIZE {
|
||
- ambulatory.path_index += 1;
|
||
- }
|
||
- } else {
|
||
- ambulatory.current_path = None;
|
||
- ambulatory.target = None;
|
||
+ if transform.translation.distance(next_point) < TILE_SIZE {
|
||
+ ambulatory.path_index += 1;
|
||
}
|
||
+ } else {
|
||
+ ambulatory.current_path = None;
|
||
+ ambulatory.target = None;
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
fn is_standable_tile(tilemap: &TileMap, pos: IVec3) -> bool {
|
||
- let mut can_i_stand_in_tile: bool = false;
|
||
- let mut can_i_stand_on_tile_bellow: bool = false;
|
||
- let mut can_i_stand_in_fixture: bool = false;
|
||
- let mut can_i_stand_on_fixture_bellow: bool = false;
|
||
-
|
||
- // Check if current position has a blocking floor tile
|
||
- if let Some(current_floor_tile) = tilemap.floor_tiles.get(&pos) {
|
||
- can_i_stand_in_tile = current_floor_tile.1;
|
||
- }
|
||
- // Check if current position has a solid fixture tile (e.g., log)
|
||
- if let Some(current_fixture_tile) = tilemap.fixture_tiles.get(&pos) {
|
||
- can_i_stand_in_fixture = current_fixture_tile.1;
|
||
- }
|
||
-
|
||
- // Check if there's solid ground below (fixture or floor)
|
||
+ 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)
|
||
+}
|
||
|
||
- if let Some(below_floor_tile) = tilemap.floor_tiles.get(&pos_below) {
|
||
- can_i_stand_on_tile_bellow = below_floor_tile.2;
|
||
+fn calculate_movement_cost(move_dir: IVec3) -> i32 {
|
||
+ match (
|
||
+ move_dir.x.abs() / ITILE_SIZE,
|
||
+ move_dir.y.abs() / ITILE_SIZE,
|
||
+ move_dir.z.abs() / ITILE_SIZE,
|
||
+ ) {
|
||
+ (1, 0, 0) | (0, 1, 0) => 10,
|
||
+ (1, 1, 0) => 14,
|
||
+ (1, 0, 1) | (0, 1, 1) => 42,
|
||
+ (1, 1, 1) => 56,
|
||
+ _ => 0,
|
||
}
|
||
+}
|
||
|
||
- if let Some(below_fixture_tile) = tilemap.fixture_tiles.get(&pos_below) {
|
||
- can_i_stand_on_fixture_bellow = below_fixture_tile.2;
|
||
- }
|
||
- return (can_i_stand_in_tile || can_i_stand_in_fixture)
|
||
- && (can_i_stand_on_tile_bellow || can_i_stand_on_fixture_bellow);
|
||
+fn octile_distance_3d(a: IVec3, b: IVec3) -> i32 {
|
||
+ let dx = (a.x - b.x).abs();
|
||
+ let dy = (a.y - b.y).abs();
|
||
+ let dz = (a.z - b.z).abs();
|
||
+ let (dmax, dmid, dmin) = sorted_desc(dx, dy, dz);
|
||
+ 10 * dmax + 4 * dmid + dmin
|
||
}
|
||
|
||
-fn calculate_path(tilemap: &TileMap, start: IVec3, goal: IVec3) -> Vec<Vec3> {
|
||
- if !is_standable_tile(tilemap, start) {
|
||
- println!("Start pos invalid: {}", start);
|
||
- println!("Bugger (1)");
|
||
- exit(0);
|
||
+fn sorted_desc(a: i32, b: i32, c: i32) -> (i32, i32, i32) {
|
||
+ let mut arr = [a, b, c];
|
||
+ arr.sort_unstable_by(|x, y| y.cmp(x));
|
||
+ (arr[0], arr[1], arr[2])
|
||
+}
|
||
+
|
||
+fn reconstruct_path(came_from: &FxHashMap<IVec3, IVec3>, mut current: IVec3) -> Vec<Vec3> {
|
||
+ let mut path = vec![Vec3::new(
|
||
+ current.x as f32,
|
||
+ current.y as f32,
|
||
+ current.z as f32,
|
||
+ )];
|
||
+ while let Some(&prev) = came_from.get(¤t) {
|
||
+ path.push(Vec3::new(prev.x as f32, prev.y as f32, prev.z as f32));
|
||
+ current = prev;
|
||
}
|
||
- if !is_standable_tile(tilemap, goal) {
|
||
- println!("Goal pos invalid: {}", goal);
|
||
- println!("Bugger (2)");
|
||
- exit(0);
|
||
+ path.reverse();
|
||
+ path
|
||
+}
|
||
+
|
||
+pub fn calculate_path_benchmarked(tilemap: &TileMap, start: IVec3, goal: IVec3) -> Vec<Vec3> {
|
||
+ let timer = Instant::now();
|
||
+
|
||
+ if !is_standable_tile(tilemap, start) || !is_standable_tile(tilemap, goal) {
|
||
+ LOCAL_FAILED_PATHS.with(|f| {
|
||
+ *f.borrow_mut() += 1;
|
||
+ });
|
||
+ return Vec::new();
|
||
}
|
||
|
||
- let mut open_set = BinaryHeap::new();
|
||
- let mut came_from = HashMap::new();
|
||
- let mut g_scores = HashMap::new();
|
||
- let mut closed_set = HashSet::new();
|
||
- let mut in_open_set = HashSet::new();
|
||
+ let estimated_tiles = octile_distance_3d(start, goal) / ITILE_SIZE;
|
||
+ let (result, nodes_expanded) =
|
||
+ calculate_path_with_scratchpad(tilemap, start, goal, estimated_tiles);
|
||
+
|
||
+ let elapsed = timer.elapsed().as_micros();
|
||
+ LOCAL_PATH_TIMES.with(|t| {
|
||
+ t.borrow_mut().push(elapsed);
|
||
+ });
|
||
+ LOCAL_PATH_LENGTHS.with(|l| {
|
||
+ l.borrow_mut().push(result.len());
|
||
+ });
|
||
+ LOCAL_NODES_EXPANDED.with(|n| {
|
||
+ n.borrow_mut().push(nodes_expanded);
|
||
+ });
|
||
+
|
||
+ if result.is_empty() {
|
||
+ vec![Vec3::new(start.x as f32, start.y as f32, start.z as f32)]
|
||
+ } else {
|
||
+ result
|
||
+ }
|
||
+}
|
||
|
||
- let start_node = PathNode {
|
||
- position: start,
|
||
- f_score: octile_distance_3d(start, goal),
|
||
- g_score: 0,
|
||
- };
|
||
+fn calculate_path_with_scratchpad(
|
||
+ tilemap: &TileMap,
|
||
+ start: IVec3,
|
||
+ goal: IVec3,
|
||
+ estimated_tiles: i32,
|
||
+) -> (Vec<Vec3>, usize) {
|
||
+ SCRATCHPAD.with(|s| {
|
||
+ let mut scratch = s.borrow_mut();
|
||
+ let capacity = ((estimated_tiles as usize).max(64)).min(4096);
|
||
+ scratch.clear_and_reserve(capacity);
|
||
+
|
||
+ let h = octile_distance_3d(start, goal);
|
||
+ scratch.open_set.push(PathNode {
|
||
+ position: start,
|
||
+ f_score: h,
|
||
+ g_score: 0,
|
||
+ });
|
||
+ scratch.g_scores.insert(start, 0);
|
||
|
||
- open_set.push(start_node);
|
||
- in_open_set.insert(start);
|
||
- g_scores.insert(start, 0);
|
||
-
|
||
- let allowed_moves = vec![
|
||
- // Orthogonal moves
|
||
- IVec3::new(-ITILE_SIZE, 0, 0),
|
||
- IVec3::new(ITILE_SIZE, 0, 0),
|
||
- IVec3::new(0, -ITILE_SIZE, 0),
|
||
- IVec3::new(0, ITILE_SIZE, 0),
|
||
- // Diagonal moves
|
||
- IVec3::new(-ITILE_SIZE, -ITILE_SIZE, 0),
|
||
- IVec3::new(-ITILE_SIZE, ITILE_SIZE, 0),
|
||
- IVec3::new(ITILE_SIZE, -ITILE_SIZE, 0),
|
||
- IVec3::new(ITILE_SIZE, ITILE_SIZE, 0),
|
||
- // Diagonal with vertical moves (left/negative preference)
|
||
- IVec3::new(-ITILE_SIZE, 0, ITILE_SIZE),
|
||
- IVec3::new(-ITILE_SIZE, 0, -ITILE_SIZE),
|
||
- IVec3::new(ITILE_SIZE, 0, ITILE_SIZE),
|
||
- IVec3::new(ITILE_SIZE, 0, -ITILE_SIZE),
|
||
- IVec3::new(0, -ITILE_SIZE, ITILE_SIZE),
|
||
- IVec3::new(0, -ITILE_SIZE, -ITILE_SIZE),
|
||
- IVec3::new(0, ITILE_SIZE, ITILE_SIZE),
|
||
- IVec3::new(0, ITILE_SIZE, -ITILE_SIZE),
|
||
- // Full 3D diagonal moves
|
||
- IVec3::new(-ITILE_SIZE, -ITILE_SIZE, ITILE_SIZE),
|
||
- IVec3::new(-ITILE_SIZE, -ITILE_SIZE, -ITILE_SIZE),
|
||
- IVec3::new(-ITILE_SIZE, ITILE_SIZE, ITILE_SIZE),
|
||
- IVec3::new(-ITILE_SIZE, ITILE_SIZE, -ITILE_SIZE),
|
||
- IVec3::new(ITILE_SIZE, -ITILE_SIZE, ITILE_SIZE),
|
||
- IVec3::new(ITILE_SIZE, -ITILE_SIZE, -ITILE_SIZE),
|
||
- IVec3::new(ITILE_SIZE, ITILE_SIZE, ITILE_SIZE),
|
||
- IVec3::new(ITILE_SIZE, ITILE_SIZE, -ITILE_SIZE),
|
||
- ];
|
||
-
|
||
- while let Some(current_node) = open_set.pop() {
|
||
- let current = current_node.position;
|
||
-
|
||
- in_open_set.remove(¤t);
|
||
-
|
||
- if current == goal {
|
||
- // println!("path found");
|
||
- return reconstruct_path(came_from, current);
|
||
- }
|
||
+ let mut nodes_expanded: usize = 0;
|
||
|
||
- closed_set.insert(current);
|
||
+ while let Some(current_node) = scratch.open_set.pop() {
|
||
+ let current = current_node.position;
|
||
+ nodes_expanded += 1;
|
||
|
||
- for &move_dir in &allowed_moves {
|
||
- let neighbor_pos = current + move_dir;
|
||
+ if nodes_expanded > PATHFINDER_MAX_NODES {
|
||
+ return (
|
||
+ reconstruct_path(&scratch.came_from, current),
|
||
+ nodes_expanded,
|
||
+ );
|
||
+ }
|
||
|
||
- if !is_standable_tile(tilemap, neighbor_pos) || closed_set.contains(&neighbor_pos) {
|
||
- continue;
|
||
+ if current == goal {
|
||
+ return (
|
||
+ reconstruct_path(&scratch.came_from, current),
|
||
+ nodes_expanded,
|
||
+ );
|
||
}
|
||
|
||
- // TODO: Add terrain-based cost modifiers
|
||
- // movement_cost = apply_terrain_modifier(movement_cost, neighbor_pos, tilemap);
|
||
- // Examples:
|
||
- // - Mud/sand: +50% cost
|
||
- // - Ice: +100% cost
|
||
- // - Designated high-traffic areas: -25% cost
|
||
- // - Designated restricted areas: +500% cost
|
||
- // - Etc
|
||
-
|
||
- let movement_cost = match (
|
||
- move_dir.x.abs() / ITILE_SIZE,
|
||
- move_dir.y.abs() / ITILE_SIZE,
|
||
- move_dir.z.abs() / ITILE_SIZE,
|
||
- ) {
|
||
- // 2D Movement (Dwarf Fortress style)
|
||
- (1, 0, 0) | (0, 1, 0) => 10, // Orthogonal movement
|
||
- (1, 1, 0) => 14, // Diagonal movement (~√2 × 10)
|
||
-
|
||
- // Vertical Movement (Raw climbing - very expensive)
|
||
- // (0, 0, 1) => 50, // Pure vertical climb/fall
|
||
-
|
||
- // 3D Movement (Climbing diagonally - even more expensive)
|
||
- (1, 0, 1) | (0, 1, 1) => 52, // Orthogonal + vertical climb
|
||
- (1, 1, 1) => 56, // Diagonal + vertical climb
|
||
-
|
||
- // TODO: Implement stairs and ramps for efficient vertical movement
|
||
- // Stairs would reduce vertical costs significantly:
|
||
- // (0, 0, 1) => 20 if has_stairs(current, neighbor_pos), // Stairs: 2× horizontal cost
|
||
- // (1, 0, 1) | (0, 1, 1) => 24 if has_stairs(current, neighbor_pos), // Stairs + horizontal
|
||
- // (1, 1, 1) => 28 if has_stairs(current, neighbor_pos), // Stairs + diagonal
|
||
-
|
||
- // TODO: Implement ramps for even smoother vertical movement
|
||
- // Ramps would be cheaper than stairs:
|
||
- // (0, 0, 1) => 15 if has_ramp(current, neighbor_pos), // Ramps: 1.5× horizontal cost
|
||
- // (1, 0, 1) | (0, 1, 1) => 18 if has_ramp(current, neighbor_pos), // Ramps + horizontal
|
||
- // (1, 1, 1) => 21 if has_ramp(current, neighbor_pos), // Ramps + diagonal
|
||
- _ => continue,
|
||
- };
|
||
-
|
||
- let new_g = g_scores.get(¤t).unwrap_or(&i32::MAX) + movement_cost;
|
||
-
|
||
- if new_g < *g_scores.get(&neighbor_pos).unwrap_or(&i32::MAX) {
|
||
- came_from.insert(neighbor_pos, current);
|
||
- g_scores.insert(neighbor_pos, new_g);
|
||
- let h = octile_distance_3d(neighbor_pos, goal);
|
||
- let f = new_g + h;
|
||
-
|
||
- // Only add to open set if not already there
|
||
- if !in_open_set.contains(&neighbor_pos) {
|
||
- let neighbor_node = PathNode {
|
||
+ scratch.closed_set.insert(current);
|
||
+
|
||
+ for &move_dir in &ALLOWED_MOVES {
|
||
+ let neighbor_pos = current + move_dir;
|
||
+
|
||
+ if !is_standable_tile(tilemap, neighbor_pos)
|
||
+ || scratch.closed_set.contains(&neighbor_pos)
|
||
+ {
|
||
+ continue;
|
||
+ }
|
||
+
|
||
+ let movement_cost = calculate_movement_cost(move_dir);
|
||
+ if movement_cost == 0 {
|
||
+ continue;
|
||
+ }
|
||
+
|
||
+ let new_g = *scratch.g_scores.get(¤t).unwrap_or(&i32::MAX) + movement_cost;
|
||
+
|
||
+ if new_g < *scratch.g_scores.get(&neighbor_pos).unwrap_or(&i32::MAX) {
|
||
+ scratch.came_from.insert(neighbor_pos, current);
|
||
+ scratch.g_scores.insert(neighbor_pos, new_g);
|
||
+ let f = new_g + octile_distance_3d(neighbor_pos, goal);
|
||
+ scratch.open_set.push(PathNode {
|
||
position: neighbor_pos,
|
||
f_score: f,
|
||
g_score: new_g,
|
||
- };
|
||
- open_set.push(neighbor_node);
|
||
- in_open_set.insert(neighbor_pos);
|
||
- } else {
|
||
- let neighbor_node = PathNode {
|
||
+ });
|
||
+ }
|
||
+ }
|
||
+ }
|
||
+
|
||
+ (Vec::new(), nodes_expanded)
|
||
+ })
|
||
+}
|
||
+
|
||
+pub fn calculate_provisional_path(
|
||
+ tilemap: &TileMap,
|
||
+ start: IVec3,
|
||
+ goal: IVec3,
|
||
+ node_limit: usize,
|
||
+) -> Vec<Vec3> {
|
||
+ let timer = Instant::now();
|
||
+
|
||
+ if !is_standable_tile(tilemap, start) {
|
||
+ LOCAL_FAILED_PATHS.with(|f| {
|
||
+ *f.borrow_mut() += 1;
|
||
+ });
|
||
+ return vec![Vec3::new(start.x as f32, start.y as f32, start.z as f32)];
|
||
+ }
|
||
+
|
||
+ let estimated_tiles = octile_distance_3d(start, goal) / ITILE_SIZE;
|
||
+
|
||
+ let result = SCRATCHPAD.with(|s| {
|
||
+ let mut scratch = s.borrow_mut();
|
||
+ let capacity = ((estimated_tiles as usize).max(64)).min(4096);
|
||
+ scratch.clear_and_reserve(capacity);
|
||
+
|
||
+ let initial_h = octile_distance_3d(start, goal);
|
||
+ scratch.open_set.push(PathNode {
|
||
+ position: start,
|
||
+ f_score: initial_h,
|
||
+ g_score: 0,
|
||
+ });
|
||
+ scratch.g_scores.insert(start, 0);
|
||
+
|
||
+ let mut nodes_expanded: usize = 0;
|
||
+ let mut best_node = start;
|
||
+ let mut best_h = initial_h;
|
||
+
|
||
+ while let Some(current_node) = scratch.open_set.pop() {
|
||
+ let current = current_node.position;
|
||
+ nodes_expanded += 1;
|
||
+
|
||
+ let h = octile_distance_3d(current, goal);
|
||
+ if h < best_h {
|
||
+ best_h = h;
|
||
+ best_node = current;
|
||
+ }
|
||
+
|
||
+ if current == goal {
|
||
+ return (
|
||
+ reconstruct_path(&scratch.came_from, current),
|
||
+ nodes_expanded,
|
||
+ );
|
||
+ }
|
||
+
|
||
+ if nodes_expanded >= node_limit {
|
||
+ return (
|
||
+ reconstruct_path(&scratch.came_from, best_node),
|
||
+ nodes_expanded,
|
||
+ );
|
||
+ }
|
||
+
|
||
+ scratch.closed_set.insert(current);
|
||
+
|
||
+ for &move_dir in &ALLOWED_MOVES {
|
||
+ let neighbor_pos = current + move_dir;
|
||
+
|
||
+ if !is_standable_tile(tilemap, neighbor_pos)
|
||
+ || scratch.closed_set.contains(&neighbor_pos)
|
||
+ {
|
||
+ continue;
|
||
+ }
|
||
+
|
||
+ let movement_cost = calculate_movement_cost(move_dir);
|
||
+ if movement_cost == 0 {
|
||
+ continue;
|
||
+ }
|
||
+
|
||
+ let new_g = *scratch.g_scores.get(¤t).unwrap_or(&i32::MAX) + movement_cost;
|
||
+
|
||
+ if new_g < *scratch.g_scores.get(&neighbor_pos).unwrap_or(&i32::MAX) {
|
||
+ scratch.came_from.insert(neighbor_pos, current);
|
||
+ scratch.g_scores.insert(neighbor_pos, new_g);
|
||
+ let f = new_g + octile_distance_3d(neighbor_pos, goal);
|
||
+ scratch.open_set.push(PathNode {
|
||
position: neighbor_pos,
|
||
f_score: f,
|
||
g_score: new_g,
|
||
- };
|
||
- open_set.push(neighbor_node);
|
||
+ });
|
||
}
|
||
}
|
||
}
|
||
- }
|
||
|
||
- Vec::new()
|
||
+ (
|
||
+ reconstruct_path(&scratch.came_from, best_node),
|
||
+ nodes_expanded,
|
||
+ )
|
||
+ });
|
||
+
|
||
+ let elapsed = timer.elapsed().as_micros();
|
||
+ LOCAL_PATH_TIMES.with(|t| {
|
||
+ t.borrow_mut().push(elapsed);
|
||
+ });
|
||
+ LOCAL_PATH_LENGTHS.with(|l| {
|
||
+ l.borrow_mut().push(result.0.len());
|
||
+ });
|
||
+ LOCAL_NODES_EXPANDED.with(|n| {
|
||
+ n.borrow_mut().push(result.1);
|
||
+ });
|
||
+
|
||
+ result.0
|
||
}
|
||
|
||
-fn octile_distance_3d(a: IVec3, b: IVec3) -> i32 {
|
||
- let dx = (a.x - b.x).abs();
|
||
- let dy = (a.y - b.y).abs();
|
||
- let dz = (a.z - b.z).abs();
|
||
+pub fn bench_report_system(
|
||
+ keys: Res<ButtonInput<KeyCode>>,
|
||
+ mut bench: ResMut<PathfindingBenchmark>,
|
||
+) {
|
||
+ if keys.just_pressed(KeyCode::F8) {
|
||
+ println!("\n=== PATHFINDING BENCHMARK REPORT ===");
|
||
+ report_stat("path_calc", &bench.path_calc_times_us);
|
||
+ report_stat(
|
||
+ "path_length",
|
||
+ &bench
|
||
+ .path_lengths
|
||
+ .iter()
|
||
+ .map(|&l| l as u128)
|
||
+ .collect::<Vec<_>>(),
|
||
+ );
|
||
+ report_stat(
|
||
+ "nodes_expanded",
|
||
+ &bench
|
||
+ .nodes_expanded
|
||
+ .iter()
|
||
+ .map(|&n| n as u128)
|
||
+ .collect::<Vec<_>>(),
|
||
+ );
|
||
+
|
||
+ if !bench.movement_system_times_us.is_empty() {
|
||
+ report_stat("movement_system", &bench.movement_system_times_us);
|
||
+ }
|
||
+ if !bench.wander_system_times_us.is_empty() {
|
||
+ report_stat("wander_system", &bench.wander_system_times_us);
|
||
+ }
|
||
|
||
- // Dwarf Fortress style costs
|
||
- let cost_orthogonal = 10; // Horizontal orthogonal
|
||
- let cost_diagonal = 14; // Horizontal diagonal (~√2 × 10)
|
||
- let cost_climb = 50; // Raw vertical movement (climbing)
|
||
-
|
||
- let mut diffs = [dx, dy, dz];
|
||
- diffs.sort_unstable();
|
||
- let dmin = diffs[0];
|
||
- let dmax = diffs[2];
|
||
-
|
||
- if dz == 0 {
|
||
- // Pure 2D movement
|
||
- let diagonal_moves = dmin / ITILE_SIZE;
|
||
- let orthogonal_moves = (dmax - dmin) / ITILE_SIZE;
|
||
- cost_diagonal * diagonal_moves + cost_orthogonal * orthogonal_moves
|
||
- } else {
|
||
- // Movement involves Z - assume raw climbing for now
|
||
- // TODO: Modify this when stairs/ramps are implemented
|
||
- let z_moves = dz / ITILE_SIZE;
|
||
- let xy_distance = ((dx * dx + dy * dy) as f32).sqrt() as i32;
|
||
- let remaining_2d_diagonal = (xy_distance.min(dz)) / ITILE_SIZE;
|
||
- let remaining_2d_orthogonal =
|
||
- (xy_distance - remaining_2d_diagonal * ITILE_SIZE) / ITILE_SIZE;
|
||
-
|
||
- // Raw climbing cost + remaining 2D movement
|
||
- cost_climb * z_moves
|
||
- + cost_diagonal * remaining_2d_diagonal
|
||
- + cost_orthogonal * remaining_2d_orthogonal
|
||
+ let total = bench.total_paths_calculated;
|
||
+ let failed = bench.total_failed_paths;
|
||
+ println!(
|
||
+ "[BENCH] total_paths={} failed_paths={} success_rate={:.1}%",
|
||
+ total,
|
||
+ failed,
|
||
+ if total > 0 {
|
||
+ 100.0 * (total - failed) as f64 / total as f64
|
||
+ } else {
|
||
+ 100.0
|
||
+ }
|
||
+ );
|
||
+
|
||
+ if let Err(e) = write_benchmark_csv(&bench, "pathfinding_benchmark_current.csv") {
|
||
+ eprintln!("Failed to write benchmark CSV: {}", e);
|
||
+ }
|
||
+ println!("=====================================\n");
|
||
}
|
||
}
|
||
|
||
-fn reconstruct_path(came_from: HashMap<IVec3, IVec3>, mut current: IVec3) -> Vec<Vec3> {
|
||
- let mut path = vec![Vec3::new(
|
||
- current.x as f32,
|
||
- current.y as f32,
|
||
- current.z as f32,
|
||
- )];
|
||
+fn report_stat(label: &str, times: &[u128]) {
|
||
+ if times.is_empty() {
|
||
+ return;
|
||
+ }
|
||
+ let sum: u128 = times.iter().sum();
|
||
+ let avg = sum / times.len() as u128;
|
||
+ let min = *times.iter().min().unwrap();
|
||
+ let max = *times.iter().max().unwrap();
|
||
+ let mut sorted = times.to_vec();
|
||
+ sorted.sort_unstable();
|
||
+ let median = sorted[sorted.len() / 2];
|
||
+ let p95_idx = (sorted.len() as f64 * 0.95) as usize;
|
||
+ let p95 = sorted[p95_idx.min(sorted.len().saturating_sub(1))];
|
||
+
|
||
+ println!(
|
||
+ "[BENCH][{}] n={} avg={}µs median={}µs min={}µs max={}µs p95={}µs",
|
||
+ label,
|
||
+ times.len(),
|
||
+ avg,
|
||
+ median,
|
||
+ min,
|
||
+ max,
|
||
+ p95
|
||
+ );
|
||
+}
|
||
|
||
- while let Some(&previous) = came_from.get(¤t) {
|
||
- path.push(Vec3::new(
|
||
- previous.x as f32,
|
||
- previous.y as f32,
|
||
- previous.z as f32,
|
||
- ));
|
||
- current = previous;
|
||
+fn write_benchmark_csv(bench: &PathfindingBenchmark, filename: &str) -> std::io::Result<()> {
|
||
+ use std::fs::File;
|
||
+ use std::io::Write;
|
||
+
|
||
+ let mut file = File::create(filename)?;
|
||
+ writeln!(
|
||
+ file,
|
||
+ "sample,path_duration_us,path_length,nodes_expanded,success"
|
||
+ )?;
|
||
+
|
||
+ let n = bench.path_calc_times_us.len();
|
||
+ for i in 0..n {
|
||
+ let duration = bench.path_calc_times_us.get(i).copied().unwrap_or(0);
|
||
+ let length = bench.path_lengths.get(i).copied().unwrap_or(0);
|
||
+ let nodes = bench.nodes_expanded.get(i).copied().unwrap_or(0);
|
||
+ let success = i < (n - bench.total_failed_paths as usize);
|
||
+ writeln!(file, "{},{},{},{},{}", i, duration, length, nodes, success)?;
|
||
}
|
||
|
||
- path.reverse();
|
||
- path
|
||
+ writeln!(file, "# Summary")?;
|
||
+ if !bench.path_calc_times_us.is_empty() {
|
||
+ let avg: u128 =
|
||
+ bench.path_calc_times_us.iter().sum::<u128>() / bench.path_calc_times_us.len() as u128;
|
||
+ writeln!(file, "# avg_duration_us,{}", avg)?;
|
||
+ }
|
||
+ writeln!(file, "# total_paths,{}", bench.total_paths_calculated)?;
|
||
+ writeln!(file, "# failed_paths,{}", bench.total_failed_paths)?;
|
||
+
|
||
+ Ok(())
|
||
}
|
||
diff --git a/src/world/chunks/management.rs b/src/world/chunks/management.rs
|
||
index f066456..300c4c4 100644
|
||
--- a/src/world/chunks/management.rs
|
||
+++ b/src/world/chunks/management.rs
|
||
@@ -6,7 +6,7 @@ use crate::world::{tiles::TileMap, CurrentWorldSpriteState, TerrainSpriteState};
|
||
|
||
pub const CHUNK_SIZE: i32 = 8;
|
||
|
||
-pub const Z_BELOW: f32 = 45.0;
|
||
+pub const Z_BELOW: f32 = 5.0;
|
||
pub const Z_ABOVE: f32 = 15.0;
|
||
pub const Z_TOTAL: f32 = Z_ABOVE + Z_BELOW;
|
||
|
||
diff --git a/src/world/generation/forestry.rs b/src/world/generation/forestry.rs
|
||
index 9fc597c..4decbb3 100644
|
||
--- a/src/world/generation/forestry.rs
|
||
+++ b/src/world/generation/forestry.rs
|
||
@@ -11,8 +11,8 @@ use std::hash::{Hash, Hasher};
|
||
use crate::{
|
||
constants::{SEED, TILE_SIZE},
|
||
world::{
|
||
- tiles::TileMap, ChunkForrestryEvent, FixtureTilePrefab, TextureIDs, Textures,
|
||
- VisibleGameEntity,
|
||
+ tiles::{FixtureTileData, TileMap},
|
||
+ ChunkForrestryEvent, FixtureTilePrefab, TextureIDs, Textures, VisibleGameEntity,
|
||
},
|
||
};
|
||
|
||
@@ -26,8 +26,7 @@ pub fn generate_chunk_forrestry(
|
||
let start = Instant::now();
|
||
let count = events.len();
|
||
|
||
- let collected_tilemap_updates: Mutex<Vec<(IVec3, (i32, bool, bool, [u32; 8]))>> =
|
||
- Mutex::new(Vec::<(IVec3, (i32, bool, bool, [u32; 8]))>::new());
|
||
+ let collected_tilemap_updates: Mutex<Vec<(IVec3, FixtureTileData)>> = Mutex::new(Vec::new());
|
||
|
||
events.par_read().for_each(|event| {
|
||
let floor_positions = &event.floor_tiles;
|
||
@@ -87,10 +86,10 @@ pub fn generate_chunk_forrestry(
|
||
}
|
||
}
|
||
|
||
- collected_tilemap_updates
|
||
- .lock()
|
||
- .unwrap()
|
||
- .push((trunk_ivec, (1, false, true, [0; 8])));
|
||
+ collected_tilemap_updates.lock().unwrap().push((
|
||
+ trunk_ivec,
|
||
+ FixtureTileData::new(1, false, true, [0; 8]),
|
||
+ ));
|
||
|
||
log_positions.insert(trunk_ivec);
|
||
}
|
||
@@ -144,10 +143,14 @@ pub fn generate_chunk_forrestry(
|
||
))
|
||
.id();
|
||
commands.entity(leaf).insert(VisibleGameEntity);
|
||
- collected_tilemap_updates
|
||
- .lock()
|
||
- .unwrap()
|
||
- .push((ivec, (5, false, true, [0; 8])));
|
||
+ collected_tilemap_updates.lock().unwrap().push(
|
||
+ (
|
||
+ ivec,
|
||
+ FixtureTileData::new(
|
||
+ 5, false, true, [0; 8],
|
||
+ ),
|
||
+ ),
|
||
+ );
|
||
}
|
||
}
|
||
}
|
||
@@ -164,7 +167,7 @@ pub fn generate_chunk_forrestry(
|
||
|
||
let collected_updates = collected_tilemap_updates.into_inner().unwrap();
|
||
for (ivec, data) in collected_updates {
|
||
- tilemap.fixture_tiles.insert(ivec, data);
|
||
+ tilemap.insert_fixture(ivec, data);
|
||
}
|
||
if count > 0 {
|
||
println!(
|
||
diff --git a/src/world/generation/terrain.rs b/src/world/generation/terrain.rs
|
||
index fb46146..673ee7e 100644
|
||
--- a/src/world/generation/terrain.rs
|
||
+++ b/src/world/generation/terrain.rs
|
||
@@ -7,8 +7,9 @@ use noise::{NoiseFn, Perlin};
|
||
use crate::{
|
||
constants::{SEED, TILE_SIZE},
|
||
world::{
|
||
- tiles::TileMap, ChunkForrestryEvent, ChunkTerrainEvent, FloorTilePrefab,
|
||
- TileOcclusionEvent, CHUNK_SIZE, Z_ABOVE, Z_BELOW,
|
||
+ tiles::{FloorTileData, TileMap},
|
||
+ ChunkForrestryEvent, ChunkTerrainEvent, FloorTilePrefab, TileOcclusionEvent, CHUNK_SIZE,
|
||
+ Z_ABOVE, Z_BELOW,
|
||
},
|
||
};
|
||
|
||
@@ -50,8 +51,7 @@ pub fn generate_chunk_terrain(
|
||
let start_y = chunk_pos.y * CHUNK_SIZE;
|
||
|
||
let mut surface_positions: Vec<(Vec3, String)> = Vec::new();
|
||
- let mut local_tilemap_updates: HashMap<IVec3, (i32, bool, bool, bool, i32, [u32; 8])> =
|
||
- HashMap::new();
|
||
+ let mut local_tilemap_updates: HashMap<IVec3, FloorTileData> = HashMap::new();
|
||
|
||
// Generate tiles for this chunk
|
||
for local_y in 0..CHUNK_SIZE {
|
||
@@ -84,23 +84,26 @@ pub fn generate_chunk_terrain(
|
||
commands.command_scope(|mut cmd| {
|
||
FloorTilePrefab::air(position).spawn(&mut cmd);
|
||
});
|
||
- local_tilemap_updates
|
||
- .insert(pos_ivec, (0, true, false, true, 0, [0; 8]));
|
||
- // Air tile
|
||
+ local_tilemap_updates.insert(
|
||
+ pos_ivec,
|
||
+ FloorTileData::new(0, true, false, true, 0, [0; 8]),
|
||
+ );
|
||
} else if cave_value < 0.8 {
|
||
commands.command_scope(|mut cmd| {
|
||
FloorTilePrefab::rock(position).spawn(&mut cmd);
|
||
});
|
||
- local_tilemap_updates
|
||
- .insert(pos_ivec, (2, false, true, false, 50, [0; 8]));
|
||
- // Rock tile
|
||
+ local_tilemap_updates.insert(
|
||
+ pos_ivec,
|
||
+ FloorTileData::new(2, false, true, false, 50, [0; 8]),
|
||
+ );
|
||
} else {
|
||
commands.command_scope(|mut cmd| {
|
||
FloorTilePrefab::dirt(position).spawn(&mut cmd);
|
||
});
|
||
- local_tilemap_updates
|
||
- .insert(pos_ivec, (1, false, true, false, 85, [0; 8]));
|
||
- // Dirt tile
|
||
+ local_tilemap_updates.insert(
|
||
+ pos_ivec,
|
||
+ FloorTileData::new(1, false, true, false, 85, [0; 8]),
|
||
+ );
|
||
}
|
||
} else if noise_position.z > position.z {
|
||
if (generate_surface_terrain(world_x, world_y) * TILE_SIZE).round()
|
||
@@ -109,23 +112,28 @@ pub fn generate_chunk_terrain(
|
||
commands.command_scope(|mut cmd| {
|
||
FloorTilePrefab::grass(position).spawn(&mut cmd);
|
||
});
|
||
- local_tilemap_updates
|
||
- .insert(pos_ivec, (1, false, true, false, 100, [0; 8])); // Dirt tile (grass)
|
||
- surface_positions.push((position, ("grass").to_string()));
|
||
+ local_tilemap_updates.insert(
|
||
+ pos_ivec,
|
||
+ FloorTileData::new(1, false, true, false, 100, [0; 8]),
|
||
+ );
|
||
+ surface_positions.push((position, "grass".to_string()));
|
||
} else {
|
||
commands.command_scope(|mut cmd| {
|
||
FloorTilePrefab::dirt(position).spawn(&mut cmd);
|
||
});
|
||
- local_tilemap_updates
|
||
- .insert(pos_ivec, (1, false, true, false, 85, [0; 8]));
|
||
- // Dirt tile
|
||
+ local_tilemap_updates.insert(
|
||
+ pos_ivec,
|
||
+ FloorTileData::new(1, false, true, false, 85, [0; 8]),
|
||
+ );
|
||
}
|
||
} else {
|
||
commands.command_scope(|mut cmd| {
|
||
FloorTilePrefab::air(position).spawn(&mut cmd);
|
||
});
|
||
- local_tilemap_updates.insert(pos_ivec, (0, true, false, true, 0, [0; 8]));
|
||
- // Air tile
|
||
+ local_tilemap_updates.insert(
|
||
+ pos_ivec,
|
||
+ FloorTileData::new(0, true, false, true, 0, [0; 8]),
|
||
+ );
|
||
}
|
||
}
|
||
}
|
||
@@ -151,7 +159,7 @@ pub fn generate_chunk_terrain(
|
||
.unwrap()
|
||
.into_iter()
|
||
.map(|(pos, data)| {
|
||
- tilemap.floor_tiles.insert(pos, data);
|
||
+ tilemap.insert_floor(pos, data);
|
||
pos
|
||
})
|
||
.collect();
|
||
diff --git a/src/world/mod.rs b/src/world/mod.rs
|
||
index a0cb751..d94211f 100644
|
||
--- a/src/world/mod.rs
|
||
+++ b/src/world/mod.rs
|
||
@@ -69,8 +69,8 @@ impl Plugin for WorldPlugin {
|
||
}
|
||
|
||
fn setup_initial_chunks(mut event_writer: MessageWriter<GenerateChunkEvent>) {
|
||
- for x in -5..=5 {
|
||
- for y in -5..=5 {
|
||
+ for x in -15..=15 {
|
||
+ for y in -15..=15 {
|
||
event_writer.write(GenerateChunkEvent {
|
||
chunk_position: IVec2::new(x, y),
|
||
});
|
||
diff --git a/src/world/tiles/tilemap.rs b/src/world/tiles/tilemap.rs
|
||
index 8b3d811..028c005 100644
|
||
--- a/src/world/tiles/tilemap.rs
|
||
+++ b/src/world/tiles/tilemap.rs
|
||
@@ -1,9 +1,197 @@
|
||
use bevy::prelude::*;
|
||
-use bevy_platform::collections::hash_map::HashMap;
|
||
+use rustc_hash::FxHashMap;
|
||
|
||
-#[derive(Resource, Default, Clone)]
|
||
+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: HashMap<IVec3, (i32, bool, bool, bool, i32, [u32; 8])>, //id, canStandIn, canStandOn, visiblyTransparent, astar_weight, visible_range
|
||
- pub fixture_tiles: HashMap<IVec3, (i32, bool, bool, [u32; 8])>, // id, canStandIn, canStandOn, visible_range
|
||
- pub item_tiles: HashMap<IVec3, Vec<u32>>, // Entity.id's of items on this tile
|
||
+ 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)
|
||
+ }
|
||
}
|
||
diff --git a/src/world/tiles/visibility.rs b/src/world/tiles/visibility.rs
|
||
index 5c9a041..c56ce8b 100644
|
||
--- a/src/world/tiles/visibility.rs
|
||
+++ b/src/world/tiles/visibility.rs
|
||
@@ -100,8 +100,8 @@ pub fn handle_tile_occlusion_updates(
|
||
for (_, mut tile, pos) in floor_tiles.iter_mut() {
|
||
if let Some(visibility) = update_map.get(&pos.translation.as_ivec3()) {
|
||
tile.visible_range = *visibility;
|
||
- if let Some(tile_data) = tilemap.floor_tiles.get_mut(&pos.translation.as_ivec3()) {
|
||
- tile_data.5 = *visibility;
|
||
+ if let Some(tile_data) = tilemap.get_floor_mut(&pos.translation.as_ivec3()) {
|
||
+ tile_data.visible_range = *visibility;
|
||
}
|
||
}
|
||
}
|
||
@@ -134,7 +134,7 @@ pub fn calculate_visibility(pos: IVec3, tilemap: &TileMap) -> [u32; 8] {
|
||
pos.z + z_offset * ITILE_SIZE,
|
||
);
|
||
match tilemap.floor_tiles.get(&neighbor_pos) {
|
||
- Some(&(id, _, _, _, _, _)) if id == 0 => break 'neighbor_check true,
|
||
+ Some(tile) if tile.id == 0 => break 'neighbor_check true,
|
||
None => {}
|
||
_ => {}
|
||
}
|
||
@@ -163,8 +163,8 @@ pub fn calculate_visibility(pos: IVec3, tilemap: &TileMap) -> [u32; 8] {
|
||
let camera_z_index = (check_pos.z / ITILE_SIZE) + z_below;
|
||
|
||
if camera_z_index < 0 {
|
||
- if let Some(&(_, _, _, vt, _, _)) = tilemap.floor_tiles.get(&check_pos) {
|
||
- occluded = !vt;
|
||
+ if let Some(tile) = tilemap.floor_tiles.get(&check_pos) {
|
||
+ occluded = !tile.visibly_transparent();
|
||
} else {
|
||
occluded = false;
|
||
}
|
||
@@ -180,8 +180,8 @@ pub fn calculate_visibility(pos: IVec3, tilemap: &TileMap) -> [u32; 8] {
|
||
visible_range[z2 / 32] |= 1 << (z2 % 32);
|
||
}
|
||
|
||
- if let Some(&(_, _, _, vt, _, _)) = tilemap.floor_tiles.get(&check_pos) {
|
||
- occluded = !vt;
|
||
+ if let Some(tile) = tilemap.floor_tiles.get(&check_pos) {
|
||
+ occluded = !tile.visibly_transparent();
|
||
} else {
|
||
occluded = false;
|
||
}
|
||
diff --git a/verify_bench.py b/verify_bench.py
|
||
new file mode 100644
|
||
index 0000000..fea2e64
|
||
--- /dev/null
|
||
+++ b/verify_bench.py
|
||
@@ -0,0 +1,36 @@
|
||
+import csv
|
||
+import sys
|
||
+
|
||
+def analyze(filename):
|
||
+ durations = []
|
||
+ failed = 0
|
||
+ total = 0
|
||
+ stutters = 0
|
||
+ try:
|
||
+ with open(filename, 'r') as f:
|
||
+ reader = csv.reader(f)
|
||
+ next(reader) # skip header
|
||
+ for row in reader:
|
||
+ if not row or row[0].startswith('#'): continue
|
||
+ total += 1
|
||
+ try:
|
||
+ d = int(row[1])
|
||
+ durations.append(d)
|
||
+ if d > 1000: stutters += 1
|
||
+ if row[4] == 'false': failed += 1
|
||
+ except:
|
||
+ pass
|
||
+
|
||
+ durations.sort()
|
||
+ if not durations: return
|
||
+ p50 = durations[len(durations)//2]
|
||
+ p90 = durations[int(len(durations)*0.90)]
|
||
+ p99 = durations[int(len(durations)*0.99)]
|
||
+ max_d = durations[-1]
|
||
+ print(f"[{filename}]")
|
||
+ print(f"P50={p50} P90={p90} P99={p99} Max={max_d} Stutters={stutters} Failed={failed}/{total}")
|
||
+ except Exception as e:
|
||
+ print(f"Error reading {filename}: {e}")
|
||
+
|
||
+analyze("/home/popertots/bench/pathfinding_benchmark_baseline_release.csv")
|
||
+analyze("pathfinding_benchmark_current.csv")
|