Run fixed schedules single-threaded; cache reach; cap wander searches
- author
- Marijn Besseling <njirambem@gmail.com> · 2026-10-25 12:59 UTC
- commit
- 4acafd45bec176a9aff2ab9e064551786d979f8f
- parent
- bd659d1db1
- tree
- browse at this commit
6 files changed +242 -83
| src/animals.rs | +4 -4 | |
| src/family.rs | +11 -6 | |
| src/jobs/mod.rs | +58 -18 | |
| src/pathfinding.rs | +119 -54 | |
| src/sim.rs | +47 -1 | |
| src/world.rs | +3 -0 |
src/animals.rs +4 -4
| @@ -6,7 +6,7 @@ use bevy::prelude::*; | ||
| 6 | 6 | use crate::{ |
| 7 | 7 | colonist::{Colonist, MoveSpeed, Path}, |
| 8 | 8 | jobs::{CurrentJob, Task}, |
| 9 | - pathfinding::find_path, | |
| 9 | + pathfinding::{WANDER_SEARCH, find_nearby_path}, | |
| 10 | 10 | rng::SimRng, |
| 11 | 11 | sim::{SimPos, SimSystems, SimTick, TICKS_PER_HOUR, sim_pos}, |
| 12 | 12 | threats::{Health, Predator}, |
| @@ -391,7 +391,7 @@ pub fn wander( | ||
| 391 | 391 | rng.range_i32(-PEN_RADIUS..PEN_RADIUS + 1), |
| 392 | 392 | ); |
| 393 | 393 | if map.in_bounds(goal) |
| 394 | - && let Some(route) = find_path(&map, tile.0, goal.as_uvec2()) | |
| 394 | + && let Some(route) = find_nearby_path(&map, tile.0, goal.as_uvec2(), WANDER_SEARCH) | |
| 395 | 395 | { |
| 396 | 396 | path.0 = route.into(); |
| 397 | 397 | idle.0 |
| @@ -407,7 +407,7 @@ pub fn wander( | ||
| 407 | 407 | .is_none_or(|end| flee_goal(*end, threats).is_some()); |
| 408 | 408 | if heading_into_danger && let Some(goal) = flee_goal(tile.0, threats) { |
| 409 | 409 | let goal = goal.clamp(IVec2::ZERO, map.size.as_ivec2() - 1).as_uvec2(); |
| 410 | - if let Some(route) = find_path(&map, tile.0, goal) { | |
| 410 | + if let Some(route) = find_nearby_path(&map, tile.0, goal, WANDER_SEARCH) { | |
| 411 | 411 | path.0 = route.into(); |
| 412 | 412 | continue; |
| 413 | 413 | } |
| @@ -421,7 +421,7 @@ pub fn wander( | ||
| 421 | 421 | rng.range_i32(-WANDER_RADIUS..WANDER_RADIUS + 1), |
| 422 | 422 | ); |
| 423 | 423 | if map.in_bounds(goal) |
| 424 | - && let Some(route) = find_path(&map, tile.0, goal.as_uvec2()) | |
| 424 | + && let Some(route) = find_nearby_path(&map, tile.0, goal.as_uvec2(), WANDER_SEARCH) | |
| 425 | 425 | { |
| 426 | 426 | path.0 = route.into(); |
| 427 | 427 | let secs = rng.range_f32(IDLE_SECS); |
src/family.rs +11 -6
| @@ -438,6 +438,7 @@ fn plan_homes( | ||
| 438 | 438 | items: Query<&TilePos, With<Item>>, |
| 439 | 439 | families: Query<(Option<&Family>, Has<Partner>, Has<Child>), With<Colonist>>, |
| 440 | 440 | mut colony_reach: ResMut<ColonyReach>, |
| 441 | + mut flood: Local<Option<((u64, UVec2, UVec2), Reach)>>, | |
| 441 | 442 | ) { |
| 442 | 443 | if !tick.0.is_multiple_of(HOME_PLAN_INTERVAL_TICKS) { |
| 443 | 444 | return; |
| @@ -495,12 +496,16 @@ fn plan_homes( | ||
| 495 | 496 | let home = homes.vacant.swap_remove(index); |
| 496 | 497 | homes.by_family.insert(family, home); |
| 497 | 498 | } |
| 498 | - let reachable = Reach::flood(&map, colony_home); | |
| 499 | - colony_reach.0 = map | |
| 500 | - .move_cost(colony_home.as_ivec2()) | |
| 501 | - .is_some() | |
| 502 | - .then(|| reachable.clone()); | |
| 503 | 499 | let anchored = map.move_cost(colony_home.as_ivec2()).is_some(); |
| 500 | + let key = (map.shape_version(), colony_home, map.size); | |
| 501 | + let stale = flood.as_ref().is_none_or(|(cached, _)| *cached != key) | |
| 502 | + || (anchored && colony_reach.0.is_none()); | |
| 503 | + if stale { | |
| 504 | + let fresh = Reach::flood(&map, colony_home); | |
| 505 | + colony_reach.0 = anchored.then(|| fresh.clone()); | |
| 506 | + *flood = Some((key, fresh)); | |
| 507 | + } | |
| 508 | + let reachable = &flood.as_ref().expect("flood computed above").1; | |
| 504 | 509 | blueprints |
| 505 | 510 | .0 |
| 506 | 511 | .retain(|tile, _| !anchored || reachable.near(*tile)); |
| @@ -514,7 +519,7 @@ fn plan_homes( | ||
| 514 | 519 | .chain(doors.iter().copied()) |
| 515 | 520 | .collect(); |
| 516 | 521 | for home in homes.all() { |
| 517 | - if let Some((tile, kind)) = door_repair(&map, &entries, &reachable, home, colony_home) { | |
| 522 | + if let Some((tile, kind)) = door_repair(&map, &entries, reachable, home, colony_home) { | |
| 518 | 523 | blueprints.0.insert(tile, kind); |
| 519 | 524 | continue; |
| 520 | 525 | } |
src/jobs/mod.rs +58 -18
| @@ -27,7 +27,8 @@ use crate::{ | ||
| 27 | 27 | mood::{Ate, Sick}, |
| 28 | 28 | needs::{EXHAUSTED_REST, Rest, SleepContext, done_sleeping, wants_sleep}, |
| 29 | 29 | pathfinding::{ |
| 30 | - ColonyReach, can_step, find_path, find_path_adjacent, is_adjacent, nearest_of, nearest_tile, | |
| 30 | + ColonyReach, WANDER_SEARCH, can_step, find_nearby_path, find_path, find_path_adjacent, | |
| 31 | + is_adjacent, nearest_of, nearest_tile, | |
| 31 | 32 | }, |
| 32 | 33 | rng::SimRng, |
| 33 | 34 | sim::{SimSystems, SimTick, hour_of_day}, |
| @@ -519,13 +520,27 @@ fn reserve_task(reservations: &mut Reservations, task: Task, by: Entity) { | ||
| 519 | 520 | } |
| 520 | 521 | |
| 521 | 522 | const GATHER_RETRY_TICKS: u64 = 300; |
| 523 | +const HAUL_RETRY_TICKS: u64 = 120; | |
| 522 | 524 | |
| 523 | -#[derive(Default)] | |
| 524 | -pub struct GatherMemo { | |
| 525 | +#[derive(Default, Clone, Copy)] | |
| 526 | +pub struct Memo { | |
| 525 | 527 | mask: u32, |
| 526 | 528 | until: u64, |
| 527 | 529 | } |
| 528 | 530 | |
| 531 | +impl Memo { | |
| 532 | + fn skips(self, mask: u32, tick: u64) -> bool { | |
| 533 | + self.mask == mask && tick < self.until | |
| 534 | + } | |
| 535 | +} | |
| 536 | + | |
| 537 | +#[derive(Default)] | |
| 538 | +pub struct GatherMemo { | |
| 539 | + gather: Memo, | |
| 540 | + forage: Memo, | |
| 541 | + haul: Memo, | |
| 542 | +} | |
| 543 | + | |
| 529 | 544 | pub fn find_work( |
| 530 | 545 | map: &WorldMap, |
| 531 | 546 | reservations: &Reservations, |
| @@ -1213,6 +1228,9 @@ fn choose_jobs( | ||
| 1213 | 1228 | mut gather_memo: Local<GatherMemo>, |
| 1214 | 1229 | ) { |
| 1215 | 1230 | let (homes, designations, reach) = plans; |
| 1231 | + if zones.is_changed() { | |
| 1232 | + gather_memo.haul = Memo::default(); | |
| 1233 | + } | |
| 1216 | 1234 | let wolves: Vec<(Entity, UVec2)> = animals |
| 1217 | 1235 | .iter() |
| 1218 | 1236 | .filter(|(_, _, animal, _)| animal.kind.is_predator()) |
| @@ -1462,8 +1480,17 @@ fn choose_jobs( | ||
| 1462 | 1480 | .iter() |
| 1463 | 1481 | .filter(|&&k| wanted(k)) |
| 1464 | 1482 | .fold(0u32, |mask, k| mask | 1 << k.index()); |
| 1465 | - let skip_gather = gather_memo.mask == gather_mask && tick.0 < gather_memo.until; | |
| 1483 | + let food_mask = gather_mask | |
| 1484 | + & ItemKind::ALL | |
| 1485 | + .iter() | |
| 1486 | + .filter(|k| k.is_food()) | |
| 1487 | + .fold(0u32, |mask, k| mask | 1 << k.index()); | |
| 1488 | + let skip_gather = gather_memo.gather.skips(gather_mask, tick.0); | |
| 1489 | + let skip_forage = gather_memo.forage.skips(food_mask, tick.0); | |
| 1490 | + let skip_haul = gather_memo.haul.skips(0, tick.0); | |
| 1466 | 1491 | let gather_failed = std::cell::Cell::new(false); |
| 1492 | + let forage_failed = std::cell::Cell::new(false); | |
| 1493 | + let haul_failed = std::cell::Cell::new(false); | |
| 1467 | 1494 | let candidates: [(WorkType, Option<Skill>, &dyn Fn() -> Option<Task>); 16] = [ |
| 1468 | 1495 | (WorkType::Haul, None, &|| { |
| 1469 | 1496 | find_expansion(&map, &zones, &reservations, &snapshot, &blueprints) |
| @@ -1479,14 +1506,14 @@ fn choose_jobs( | ||
| 1479 | 1506 | find_butchering(&map, &reservations, &snapshot, tile.0) |
| 1480 | 1507 | }), |
| 1481 | 1508 | (WorkType::Food, None, &|| { |
| 1482 | - ItemKind::ALL | |
| 1483 | - .iter() | |
| 1484 | - .any(|&k| k.is_food() && wanted(k)) | |
| 1485 | - .then(|| { | |
| 1486 | - let food = |_, k: ItemKind| k.is_food() && wanted(k); | |
| 1487 | - find_work_within(&map, &reservations, tile.0, FORAGE_SEARCH_TILES, food) | |
| 1488 | - }) | |
| 1489 | - .flatten() | |
| 1509 | + if food_mask == 0 || skip_forage { | |
| 1510 | + return None; | |
| 1511 | + } | |
| 1512 | + let food = |_, k: ItemKind| k.is_food() && wanted(k); | |
| 1513 | + let work = | |
| 1514 | + find_work_within(&map, &reservations, tile.0, FORAGE_SEARCH_TILES, food); | |
| 1515 | + forage_failed.set(work.is_none()); | |
| 1516 | + work | |
| 1490 | 1517 | }), |
| 1491 | 1518 | (WorkType::Gather, None, &designated), |
| 1492 | 1519 | (WorkType::Food, None, &|| { |
| @@ -1525,7 +1552,12 @@ fn choose_jobs( | ||
| 1525 | 1552 | .flatten() |
| 1526 | 1553 | }), |
| 1527 | 1554 | (WorkType::Haul, None, &|| { |
| 1528 | - find_haul(&map, &zones, &reservations, &reach, &snapshot, tile.0) | |
| 1555 | + if skip_haul { | |
| 1556 | + return None; | |
| 1557 | + } | |
| 1558 | + let haul = find_haul(&map, &zones, &reservations, &reach, &snapshot, tile.0); | |
| 1559 | + haul_failed.set(haul.is_none()); | |
| 1560 | + haul | |
| 1529 | 1561 | }), |
| 1530 | 1562 | ]; |
| 1531 | 1563 | let poor = |skill: &Option<Skill>| skill.is_some_and(|s| skills.level(s) < POOR_LEVEL); |
| @@ -1537,11 +1569,18 @@ fn choose_jobs( | ||
| 1537 | 1569 | .filter(|(_, skill, _)| !poor(skill)) |
| 1538 | 1570 | .chain(allowed.filter(|(_, skill, _)| poor(skill))) |
| 1539 | 1571 | .find_map(|(.., find)| find()); |
| 1572 | + let retry = |mask, ticks| Memo { | |
| 1573 | + mask, | |
| 1574 | + until: tick.0 + ticks, | |
| 1575 | + }; | |
| 1540 | 1576 | if gather_failed.get() { |
| 1541 | - *gather_memo = GatherMemo { | |
| 1542 | - mask: gather_mask, | |
| 1543 | - until: tick.0 + GATHER_RETRY_TICKS, | |
| 1544 | - }; | |
| 1577 | + gather_memo.gather = retry(gather_mask, GATHER_RETRY_TICKS); | |
| 1578 | + } | |
| 1579 | + if forage_failed.get() { | |
| 1580 | + gather_memo.forage = retry(food_mask, GATHER_RETRY_TICKS); | |
| 1581 | + } | |
| 1582 | + if haul_failed.get() { | |
| 1583 | + gather_memo.haul = retry(0, HAUL_RETRY_TICKS); | |
| 1545 | 1584 | } |
| 1546 | 1585 | task |
| 1547 | 1586 | }; |
| @@ -1579,7 +1618,8 @@ fn choose_jobs( | ||
| 1579 | 1618 | rng.range_i32(-WANDER_RADIUS..WANDER_RADIUS + 1), |
| 1580 | 1619 | ); |
| 1581 | 1620 | if map.in_bounds(goal) |
| 1582 | - && let Some(wander_path) = find_path(&map, tile.0, goal.as_uvec2()) | |
| 1621 | + && let Some(wander_path) = | |
| 1622 | + find_nearby_path(&map, tile.0, goal.as_uvec2(), WANDER_SEARCH) | |
| 1583 | 1623 | { |
| 1584 | 1624 | path.0 = wander_path.into(); |
| 1585 | 1625 | idle.restart(rng.range_f32(IDLE_SECS)); |
src/pathfinding.rs +119 -54
| @@ -160,12 +160,41 @@ pub fn nearest_of( | ||
| 160 | 160 | nearest_tile(map, start, max_tiles, |p| targets.contains(&p)) |
| 161 | 161 | } |
| 162 | 162 | |
| 163 | +fn octile(a: IVec2, b: IVec2) -> u32 { | |
| 164 | + let d = (a - b).abs(); | |
| 165 | + let (min, max) = (d.x.min(d.y) as u32, d.x.max(d.y) as u32); | |
| 166 | + (DIAGONAL * min + STRAIGHT * (max - min)) * MIN_TERRAIN_COST | |
| 167 | +} | |
| 168 | + | |
| 169 | +#[derive(Default)] | |
| 170 | +struct Scratch { | |
| 171 | + generation: u32, | |
| 172 | + stamp: Vec<u32>, | |
| 173 | + cost: Vec<u32>, | |
| 174 | + from: Vec<IVec2>, | |
| 175 | + open: BinaryHeap<Reverse<(u32, i32, i32)>>, | |
| 176 | +} | |
| 177 | + | |
| 178 | +thread_local! { | |
| 179 | + static SCRATCH: std::cell::RefCell<Scratch> = std::cell::RefCell::new(Scratch::default()); | |
| 180 | +} | |
| 181 | + | |
| 163 | 182 | fn astar( |
| 164 | 183 | map: &WorldMap, |
| 165 | 184 | start: UVec2, |
| 166 | 185 | is_goal: impl Fn(IVec2) -> bool, |
| 167 | 186 | heuristic: impl Fn(IVec2) -> u32, |
| 168 | 187 | ) -> Option<Vec<UVec2>> { |
| 188 | + astar_limited(map, start, is_goal, heuristic, usize::MAX) | |
| 189 | +} | |
| 190 | + | |
| 191 | +fn astar_limited( | |
| 192 | + map: &WorldMap, | |
| 193 | + start: UVec2, | |
| 194 | + is_goal: impl Fn(IVec2) -> bool, | |
| 195 | + heuristic: impl Fn(IVec2) -> u32, | |
| 196 | + max_expanded: usize, | |
| 197 | +) -> Option<Vec<UVec2>> { | |
| 169 | 198 | let start = start.as_ivec2(); |
| 170 | 199 | if !map.in_bounds(start) { |
| 171 | 200 | return None; |
| @@ -173,67 +202,89 @@ fn astar( | ||
| 173 | 202 | if is_goal(start) { |
| 174 | 203 | return Some(vec![]); |
| 175 | 204 | } |
| 176 | - | |
| 177 | - let width = map.size.x as usize; | |
| 178 | - let index = |p: IVec2| p.y as usize * width + p.x as usize; | |
| 179 | - let tiles = map.size.element_product() as usize; | |
| 180 | - let mut cost_so_far = vec![u32::MAX; tiles]; | |
| 181 | - let mut came_from = vec![None::<IVec2>; tiles]; | |
| 182 | - let mut open = BinaryHeap::new(); | |
| 183 | - | |
| 184 | - cost_so_far[index(start)] = 0; | |
| 185 | - open.push(Reverse((heuristic(start), start.x, start.y))); | |
| 186 | - | |
| 187 | - while let Some(Reverse((_, x, y))) = open.pop() { | |
| 188 | - let current = IVec2::new(x, y); | |
| 189 | - if is_goal(current) { | |
| 190 | - return Some(reconstruct(&came_from, index, start, current)); | |
| 205 | + SCRATCH.with_borrow_mut(|scratch| { | |
| 206 | + let tiles = map.size.element_product() as usize; | |
| 207 | + if scratch.stamp.len() != tiles { | |
| 208 | + *scratch = Scratch { | |
| 209 | + stamp: vec![0; tiles], | |
| 210 | + cost: vec![0; tiles], | |
| 211 | + from: vec![IVec2::ZERO; tiles], | |
| 212 | + ..default() | |
| 213 | + }; | |
| 191 | 214 | } |
| 192 | - let current_cost = cost_so_far[index(current)]; | |
| 193 | - for step in NEIGHBORS { | |
| 194 | - let next = current + step; | |
| 195 | - if !can_step(map, current, step) { | |
| 196 | - continue; | |
| 215 | + scratch.generation = scratch.generation.wrapping_add(1); | |
| 216 | + if scratch.generation == 0 { | |
| 217 | + scratch.stamp.fill(0); | |
| 218 | + scratch.generation = 1; | |
| 219 | + } | |
| 220 | + let Scratch { | |
| 221 | + generation, | |
| 222 | + stamp, | |
| 223 | + cost, | |
| 224 | + from, | |
| 225 | + open, | |
| 226 | + } = scratch; | |
| 227 | + let generation = *generation; | |
| 228 | + let width = map.size.x as usize; | |
| 229 | + let index = |p: IVec2| p.y as usize * width + p.x as usize; | |
| 230 | + open.clear(); | |
| 231 | + stamp[index(start)] = generation; | |
| 232 | + cost[index(start)] = 0; | |
| 233 | + from[index(start)] = start; | |
| 234 | + open.push(Reverse((heuristic(start), start.x, start.y))); | |
| 235 | + let mut expanded = 0; | |
| 236 | + while let Some(Reverse((_, x, y))) = open.pop() { | |
| 237 | + let current = IVec2::new(x, y); | |
| 238 | + if is_goal(current) { | |
| 239 | + let mut path = vec![current.as_uvec2()]; | |
| 240 | + let mut at = current; | |
| 241 | + while from[index(at)] != start { | |
| 242 | + at = from[index(at)]; | |
| 243 | + path.push(at.as_uvec2()); | |
| 244 | + } | |
| 245 | + path.reverse(); | |
| 246 | + return Some(path); | |
| 197 | 247 | } |
| 198 | - let Some(terrain_cost) = map.step_cost(current, next) else { | |
| 199 | - continue; | |
| 200 | - }; | |
| 201 | - let diagonal = step.x != 0 && step.y != 0; | |
| 202 | - let base = if diagonal { DIAGONAL } else { STRAIGHT }; | |
| 203 | - let new_cost = current_cost + base * terrain_cost; | |
| 204 | - if new_cost < cost_so_far[index(next)] { | |
| 205 | - cost_so_far[index(next)] = new_cost; | |
| 206 | - came_from[index(next)] = Some(current); | |
| 207 | - open.push(Reverse((new_cost + heuristic(next), next.x, next.y))); | |
| 248 | + expanded += 1; | |
| 249 | + if expanded > max_expanded { | |
| 250 | + return None; | |
| 251 | + } | |
| 252 | + let current_cost = cost[index(current)]; | |
| 253 | + for step in NEIGHBORS { | |
| 254 | + let next = current + step; | |
| 255 | + if !can_step(map, current, step) { | |
| 256 | + continue; | |
| 257 | + } | |
| 258 | + let Some(terrain_cost) = map.step_cost(current, next) else { | |
| 259 | + continue; | |
| 260 | + }; | |
| 261 | + let diagonal = step.x != 0 && step.y != 0; | |
| 262 | + let base = if diagonal { DIAGONAL } else { STRAIGHT }; | |
| 263 | + let new_cost = current_cost + base * terrain_cost; | |
| 264 | + let i = index(next); | |
| 265 | + if stamp[i] != generation || new_cost < cost[i] { | |
| 266 | + stamp[i] = generation; | |
| 267 | + cost[i] = new_cost; | |
| 268 | + from[i] = current; | |
| 269 | + open.push(Reverse((new_cost + heuristic(next), next.x, next.y))); | |
| 270 | + } | |
| 208 | 271 | } |
| 209 | 272 | } |
| 210 | - } | |
| 211 | - None | |
| 273 | + None | |
| 274 | + }) | |
| 212 | 275 | } |
| 213 | 276 | |
| 214 | -fn octile(a: IVec2, b: IVec2) -> u32 { | |
| 215 | - let d = (a - b).abs(); | |
| 216 | - let (min, max) = (d.x.min(d.y) as u32, d.x.max(d.y) as u32); | |
| 217 | - (DIAGONAL * min + STRAIGHT * (max - min)) * MIN_TERRAIN_COST | |
| 218 | -} | |
| 277 | +pub const WANDER_SEARCH: usize = 800; | |
| 219 | 278 | |
| 220 | -fn reconstruct( | |
| 221 | - came_from: &[Option<IVec2>], | |
| 222 | - index: impl Fn(IVec2) -> usize, | |
| 223 | - start: IVec2, | |
| 224 | - goal: IVec2, | |
| 225 | -) -> Vec<UVec2> { | |
| 226 | - let mut path = vec![goal.as_uvec2()]; | |
| 227 | - let mut current = goal; | |
| 228 | - while let Some(prev) = came_from[index(current)] { | |
| 229 | - if prev == start { | |
| 230 | - break; | |
| 231 | - } | |
| 232 | - path.push(prev.as_uvec2()); | |
| 233 | - current = prev; | |
| 234 | - } | |
| 235 | - path.reverse(); | |
| 236 | - path | |
| 279 | +pub fn find_nearby_path( | |
| 280 | + map: &WorldMap, | |
| 281 | + start: UVec2, | |
| 282 | + goal: UVec2, | |
| 283 | + max_expanded: usize, | |
| 284 | +) -> Option<Vec<UVec2>> { | |
| 285 | + map.move_cost(goal.as_ivec2())?; | |
| 286 | + let goal = goal.as_ivec2(); | |
| 287 | + astar_limited(map, start, |p| p == goal, |p| octile(p, goal), max_expanded) | |
| 237 | 288 | } |
| 238 | 289 | |
| 239 | 290 | #[cfg(test)] |
| @@ -241,6 +292,20 @@ mod tests { | ||
| 241 | 292 | use super::*; |
| 242 | 293 | use crate::world::Terrain; |
| 243 | 294 | |
| 295 | + // Wandering picks random nearby goals; one walled off by a river or cliff made A* search the | |
| 296 | + // whole connected region before failing. Short trips cap the search instead. | |
| 297 | + #[test] | |
| 298 | + fn nearby_paths_give_up_early_on_unreachable_goals() { | |
| 299 | + let mut map = WorldMap::filled(UVec2::splat(100), Terrain::Grass); | |
| 300 | + for y in 0..100 { | |
| 301 | + map.set(UVec2::new(50, y), Terrain::DeepWater); | |
| 302 | + } | |
| 303 | + let start = UVec2::new(45, 50); | |
| 304 | + assert!(find_nearby_path(&map, start, UVec2::new(40, 52), 200).is_some()); | |
| 305 | + assert_eq!(find_nearby_path(&map, start, UVec2::new(55, 50), 200), None); | |
| 306 | + assert_eq!(find_path(&map, start, UVec2::new(55, 50)), None); | |
| 307 | + } | |
| 308 | + | |
| 244 | 309 | // Target searches must use the same no-corner-cutting rule as A*. A stack on a tile touching |
| 245 | 310 | // the colony only diagonally between two rocks was "found" by the search, A* then explored |
| 246 | 311 | // the whole map before failing, and the job was picked again at once: ~10 ms, many times a |
src/sim.rs +47 -1
| @@ -1,6 +1,11 @@ | ||
| 1 | 1 | use std::time::Duration; |
| 2 | 2 | |
| 3 | -use bevy::{app::RunFixedMainLoopSystems, platform::time::Instant, prelude::*}; | |
| 3 | +use bevy::{ | |
| 4 | + app::RunFixedMainLoopSystems, | |
| 5 | + ecs::schedule::{ScheduleLabel, SingleThreadedExecutor}, | |
| 6 | + platform::time::Instant, | |
| 7 | + prelude::*, | |
| 8 | +}; | |
| 4 | 9 | |
| 5 | 10 | use crate::actions::Action; |
| 6 | 11 | |
| @@ -13,6 +18,17 @@ pub struct SimPlugin; | ||
| 13 | 18 | |
| 14 | 19 | impl Plugin for SimPlugin { |
| 15 | 20 | fn build(&self, app: &mut App) { |
| 21 | + for label in [ | |
| 22 | + FixedFirst.intern(), | |
| 23 | + FixedPreUpdate.intern(), | |
| 24 | + FixedUpdate.intern(), | |
| 25 | + FixedPostUpdate.intern(), | |
| 26 | + FixedLast.intern(), | |
| 27 | + ] { | |
| 28 | + app.edit_schedule(label, |schedule| { | |
| 29 | + schedule.set_executor(SingleThreadedExecutor::new()); | |
| 30 | + }); | |
| 31 | + } | |
| 16 | 32 | app.insert_resource(Time::<Fixed>::from_hz(TICKS_PER_SECOND)) |
| 17 | 33 | .init_resource::<SimTick>() |
| 18 | 34 | .init_resource::<SimSpeed>() |
| @@ -31,9 +47,39 @@ impl Plugin for SimPlugin { | ||
| 31 | 47 | .in_set(RunFixedMainLoopSystems::AfterFixedMainLoop), |
| 32 | 48 | ), |
| 33 | 49 | ); |
| 50 | + #[cfg(not(target_arch = "wasm32"))] | |
| 51 | + if std::env::var_os("SIM_BENCH").is_some() { | |
| 52 | + app.add_systems(Startup, start_bench) | |
| 53 | + .add_systems(Update, report_bench); | |
| 54 | + } | |
| 55 | + } | |
| 56 | +} | |
| 57 | + | |
| 58 | +#[cfg(not(target_arch = "wasm32"))] | |
| 59 | +fn start_bench(mut actions: MessageWriter<Action>) { | |
| 60 | + actions.write(Action::Load); | |
| 61 | + actions.write(Action::SetSpeed(SimSpeed::Max)); | |
| 62 | +} | |
| 63 | + | |
| 64 | +#[cfg(not(target_arch = "wasm32"))] | |
| 65 | +fn report_bench(tick: Res<SimTick>, real: Res<Time<Real>>, mut last: Local<Option<(f32, u64)>>) { | |
| 66 | + let now = real.elapsed_secs(); | |
| 67 | + if now < BENCH_WARMUP_SECS { | |
| 68 | + return; | |
| 69 | + } | |
| 70 | + let (since, from) = *last.get_or_insert((now, tick.0)); | |
| 71 | + if now - since >= BENCH_REPORT_SECS { | |
| 72 | + let days = (tick.0 - from) as f32 / (HOURS_PER_DAY * TICKS_PER_HOUR) as f32; | |
| 73 | + info!("bench: {days:.2} game days per {BENCH_REPORT_SECS} s"); | |
| 74 | + *last = Some((now, tick.0)); | |
| 34 | 75 | } |
| 35 | 76 | } |
| 36 | 77 | |
| 78 | +#[cfg(not(target_arch = "wasm32"))] | |
| 79 | +const BENCH_REPORT_SECS: f32 = 10.0; | |
| 80 | +#[cfg(not(target_arch = "wasm32"))] | |
| 81 | +const BENCH_WARMUP_SECS: f32 = 5.0; | |
| 82 | + | |
| 37 | 83 | #[derive(SystemSet, Debug, Clone, Copy, PartialEq, Eq, Hash)] |
| 38 | 84 | pub enum SimSystems { |
| 39 | 85 | Decide, |
src/world.rs +3 -0
| @@ -197,6 +197,9 @@ impl WorldMap { | ||
| 197 | 197 | |
| 198 | 198 | pub fn set(&mut self, pos: UVec2, terrain: Terrain) { |
| 199 | 199 | let i = self.index(pos); |
| 200 | + if self.tiles[i].move_cost().is_some() != terrain.move_cost().is_some() { | |
| 201 | + self.shape_version += 1; | |
| 202 | + } | |
| 200 | 203 | self.tiles[i] = terrain; |
| 201 | 204 | } |
| 202 | 205 | |