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
+9 -13
View File
@@ -7,12 +7,12 @@
//! - All task data is owned — no references to world state — for cache locality.
//!
//! # 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.
//! - No dynamic dispatch — match on Task variant directly in executor.
use bevy::prelude::*;
use smallvec::SmallVec;
use std::collections::VecDeque;
/// 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.
/// 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)]
pub struct TaskQueue {
pub tasks: SmallVec<[Task; 4]>,
pub tasks: VecDeque<Task>,
}
impl TaskQueue {
@@ -102,31 +102,27 @@ impl TaskQueue {
#[inline]
pub fn current(&self) -> Option<&Task> {
self.tasks.first()
self.tasks.front()
}
#[inline]
pub fn current_mut(&mut self) -> Option<&mut Task> {
self.tasks.first_mut()
self.tasks.front_mut()
}
#[inline]
pub fn push(&mut self, task: Task) {
self.tasks.push(task);
self.tasks.push_back(task);
}
#[inline]
pub fn push_front(&mut self, task: Task) {
self.tasks.insert(0, task);
self.tasks.push_front(task);
}
#[inline]
pub fn pop(&mut self) -> Option<Task> {
if self.tasks.is_empty() {
None
} else {
Some(self.tasks.remove(0))
}
self.tasks.pop_front()
}
#[inline]
+5 -2
View File
@@ -33,8 +33,11 @@ pub fn task_executor_system(
mut completed_writer: MessageWriter<TaskCompleted>,
mut failed_writer: MessageWriter<TaskFailed>,
) {
let Ok(mut rng) = rng_q.single_mut() else {
return;
// Get mutable access to the single GlobalRng entity
// 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() {
+25 -18
View File
@@ -31,33 +31,42 @@ pub(super) fn execute_idle(
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 entity_pos == *target || !tilemap.is_standable(*target) {
if let Some(new_target) = pick_wander_target(origin, *wander_radius, tilemap, rng) {
*target = new_target;
target_changed = true;
}
}
// Set ambulatory target - pathfinding system will compute path
ambulatory.target = Some(Vec3::new(
target.x as f32,
target.y as f32,
transform.translation.z,
));
ambulatory.current_path = None;
ambulatory.path_index = 0;
// 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(
target.x as f32,
target.y as f32,
transform.translation.z,
));
ambulatory.current_path = None;
ambulatory.path_index = 0;
}
// Otherwise: keep existing path, let movement system continue along it
}
/// Pick a random standable tile within wander radius of origin.
/// Uses reservoir sampling to avoid heap allocation.
fn pick_wander_target(
origin: &IVec3,
radius: i32,
tilemap: &TileMap,
rng: &mut WyRand,
) -> 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 dy in -radius..=radius {
let candidate = IVec3::new(
@@ -66,15 +75,13 @@ fn pick_wander_target(
origin.z,
);
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;
}
let idx = rng.random_range(0..candidates.len());
Some(candidates[idx])
chosen
}