This commit is contained in:
2026-03-22 00:52:06 +00:00
parent cafe376841
commit 71653350d8
4 changed files with 42 additions and 36 deletions
+3 -3
View File
@@ -9,7 +9,7 @@ use crate::world::VisibleGameEntity;
use bevy::prelude::*; use bevy::prelude::*;
use bevy_rand::prelude::*; use bevy_rand::prelude::*;
use rand::RngExt; use rand::RngExt;
use smallvec::smallvec; use std::collections::VecDeque;
#[derive(Bundle)] #[derive(Bundle)]
pub struct Dorf { pub struct Dorf {
@@ -53,11 +53,11 @@ impl Dorf {
haul_slot: HaulSlot::default(), haul_slot: HaulSlot::default(),
carry_visual: CarryVisualState::new(normal_sprite, carry_sprite), carry_visual: CarryVisualState::new(normal_sprite, carry_sprite),
task_queue: TaskQueue { task_queue: TaskQueue {
tasks: smallvec![Task::Idle { tasks: VecDeque::from([Task::Idle {
target: origin, target: origin,
wander_radius: 10, wander_radius: 10,
origin, origin,
}], }]),
}, },
task_state: TaskState::Pending, task_state: TaskState::Pending,
} }
+9 -13
View File
@@ -7,12 +7,12 @@
//! - All task data is owned — no references to world state — for cache locality. //! - All task data is owned — no references to world state — for cache locality.
//! //!
//! # Performance //! # Performance
//! - SmallVec<[Task; 4]> for queue — avoids heap alloc for typical short queues. //! - VecDeque for queue — O(1) push/pop at both ends.
//! - Task enum is #[repr(u8)] for compact storage and fast matching. //! - Task enum is #[repr(u8)] for compact storage and fast matching.
//! - No dynamic dispatch — match on Task variant directly in executor. //! - No dynamic dispatch — match on Task variant directly in executor.
use bevy::prelude::*; use bevy::prelude::*;
use smallvec::SmallVec; use std::collections::VecDeque;
/// A single task an entity can execute. /// A single task an entity can execute.
/// ///
@@ -88,10 +88,10 @@ impl Task {
/// Tasks are pushed to the back; executor pops from front when complete. /// Tasks are pushed to the back; executor pops from front when complete.
/// Higher-priority tasks can be inserted at front via `push_front`. /// Higher-priority tasks can be inserted at front via `push_front`.
/// ///
/// Uses SmallVec to avoid heap allocation for typical queue lengths (<4 tasks). /// Uses VecDeque for O(1) push/pop at both ends.
#[derive(Component, Debug, Default)] #[derive(Component, Debug, Default)]
pub struct TaskQueue { pub struct TaskQueue {
pub tasks: SmallVec<[Task; 4]>, pub tasks: VecDeque<Task>,
} }
impl TaskQueue { impl TaskQueue {
@@ -102,31 +102,27 @@ impl TaskQueue {
#[inline] #[inline]
pub fn current(&self) -> Option<&Task> { pub fn current(&self) -> Option<&Task> {
self.tasks.first() self.tasks.front()
} }
#[inline] #[inline]
pub fn current_mut(&mut self) -> Option<&mut Task> { pub fn current_mut(&mut self) -> Option<&mut Task> {
self.tasks.first_mut() self.tasks.front_mut()
} }
#[inline] #[inline]
pub fn push(&mut self, task: Task) { pub fn push(&mut self, task: Task) {
self.tasks.push(task); self.tasks.push_back(task);
} }
#[inline] #[inline]
pub fn push_front(&mut self, task: Task) { pub fn push_front(&mut self, task: Task) {
self.tasks.insert(0, task); self.tasks.push_front(task);
} }
#[inline] #[inline]
pub fn pop(&mut self) -> Option<Task> { pub fn pop(&mut self) -> Option<Task> {
if self.tasks.is_empty() { self.tasks.pop_front()
None
} else {
Some(self.tasks.remove(0))
}
} }
#[inline] #[inline]
+5 -2
View File
@@ -33,8 +33,11 @@ pub fn task_executor_system(
mut completed_writer: MessageWriter<TaskCompleted>, mut completed_writer: MessageWriter<TaskCompleted>,
mut failed_writer: MessageWriter<TaskFailed>, mut failed_writer: MessageWriter<TaskFailed>,
) { ) {
let Ok(mut rng) = rng_q.single_mut() else { // Get mutable access to the single GlobalRng entity
return; // This must be done once per frame, not per entity
let mut rng = match rng_q.single_mut() {
Ok(r) => r,
Err(_) => return, // No GlobalRng entity exists yet
}; };
for (entity, mut queue, mut state, mut ambulatory, transform) in query.iter_mut() { for (entity, mut queue, mut state, mut ambulatory, transform) in query.iter_mut() {
+17 -10
View File
@@ -31,14 +31,20 @@ pub(super) fn execute_idle(
let entity_pos = (transform.translation / ITILE_SIZE as f32).as_ivec3(); let entity_pos = (transform.translation / ITILE_SIZE as f32).as_ivec3();
// Track if target actually changed this tick
let mut target_changed = false;
// If reached target or target is no longer standable, pick new target // If reached target or target is no longer standable, pick new target
if entity_pos == *target || !tilemap.is_standable(*target) { if entity_pos == *target || !tilemap.is_standable(*target) {
if let Some(new_target) = pick_wander_target(origin, *wander_radius, tilemap, rng) { if let Some(new_target) = pick_wander_target(origin, *wander_radius, tilemap, rng) {
*target = new_target; *target = new_target;
target_changed = true;
} }
} }
// Set ambulatory target - pathfinding system will compute path // Only reset path if target changed or no path exists
// This avoids forcing pathfinding to recompute every frame
if target_changed || ambulatory.current_path.is_none() {
ambulatory.target = Some(Vec3::new( ambulatory.target = Some(Vec3::new(
target.x as f32, target.x as f32,
target.y as f32, target.y as f32,
@@ -47,17 +53,20 @@ pub(super) fn execute_idle(
ambulatory.current_path = None; ambulatory.current_path = None;
ambulatory.path_index = 0; ambulatory.path_index = 0;
} }
// Otherwise: keep existing path, let movement system continue along it
}
/// Pick a random standable tile within wander radius of origin. /// Pick a random standable tile within wander radius of origin.
/// Uses reservoir sampling to avoid heap allocation.
fn pick_wander_target( fn pick_wander_target(
origin: &IVec3, origin: &IVec3,
radius: i32, radius: i32,
tilemap: &TileMap, tilemap: &TileMap,
rng: &mut WyRand, rng: &mut WyRand,
) -> Option<IVec3> { ) -> Option<IVec3> {
let mut candidates = smallvec::SmallVec::<[IVec3; 16]>::new(); let mut chosen = None;
let mut count = 0u32;
// Collect valid tiles within radius
for dx in -radius..=radius { for dx in -radius..=radius {
for dy in -radius..=radius { for dy in -radius..=radius {
let candidate = IVec3::new( let candidate = IVec3::new(
@@ -66,15 +75,13 @@ fn pick_wander_target(
origin.z, origin.z,
); );
if tilemap.is_standable(candidate) { if tilemap.is_standable(candidate) {
candidates.push(candidate); count += 1;
// Reservoir sampling: 1/count chance to replace chosen
if rng.random_range(0..count) == 0 {
chosen = Some(candidate);
} }
} }
} }
if candidates.is_empty() {
return None;
} }
chosen
let idx = rng.random_range(0..candidates.len());
Some(candidates[idx])
} }