fix
This commit is contained in:
@@ -9,7 +9,7 @@ use crate::world::VisibleGameEntity;
|
||||
use bevy::prelude::*;
|
||||
use bevy_rand::prelude::*;
|
||||
use rand::RngExt;
|
||||
use smallvec::smallvec;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
#[derive(Bundle)]
|
||||
pub struct Dorf {
|
||||
@@ -53,11 +53,11 @@ impl Dorf {
|
||||
haul_slot: HaulSlot::default(),
|
||||
carry_visual: CarryVisualState::new(normal_sprite, carry_sprite),
|
||||
task_queue: TaskQueue {
|
||||
tasks: smallvec![Task::Idle {
|
||||
tasks: VecDeque::from([Task::Idle {
|
||||
target: origin,
|
||||
wander_radius: 10,
|
||||
origin,
|
||||
}],
|
||||
}]),
|
||||
},
|
||||
task_state: TaskState::Pending,
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user