Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Pathfinding & Spatial Queries

Decision: Pathfinding and spatial queries are abstracted behind traits — like NetworkModel. A multi-layer hybrid pathfinder is the first implementation (RA1 game module). The engine core has no hardcoded assumption about grids vs. continuous space.

OpenRA uses hierarchical A* which struggles with large unit groups and lacks local avoidance. A multi-layer approach (hierarchical sectors + JPS/flowfield tiles + ORCA-lite avoidance) handles both small-group and mass unit movement. But pathfinding is a game-module concern, not an engine-core assumption.

Pathfinder Trait

#![allow(unused)]
fn main() {
/// Game modules implement this to provide pathfinding.
/// Grid-based games use multi-layer hybrid (JPS + flowfield tiles + avoidance).
/// Continuous-space games would use navmesh.
/// The engine core calls this trait — never a specific algorithm.
pub trait Pathfinder: Send + Sync {
    /// Request a path from origin to destination.
    /// Returns a local handle (`PathId`) used only inside the running sim instance.
    /// `PathId` is not part of network protocol or replay/save serialization.
    fn request_path(&mut self, origin: WorldPos, dest: WorldPos, locomotor: LocomotorType) -> PathId;

    /// Poll for completed path. Returns waypoints in WorldPos.
    fn get_path(&self, id: PathId) -> Option<&[WorldPos]>;

    /// Can a unit with this locomotor pass through this position?
    fn is_passable(&self, pos: WorldPos, locomotor: LocomotorType) -> bool;

    /// Invalidate cached paths (e.g., building placed, bridge destroyed).
    fn invalidate_area(&mut self, center: WorldPos, radius: SimCoord);

    /// Query the path distance between two points without computing full waypoints.
    /// Returns `None` if no path exists. Used by AI for target selection, threat assessment,
    /// and build placement scoring.
    fn path_distance(&self, from: WorldPos, to: WorldPos, locomotor: LocomotorType) -> Option<SimCoord>;

    /// Batch distance queries — amortizes overhead when AI needs distances to many targets.
    /// Writes results into caller-provided scratch (`out`) in the same order as `targets`.
    /// `None` entries mean no path. Implementations must clear/reuse `out` (no hidden heap scratch
    /// returned to the caller), preserving the zero-allocation hot-path discipline.
    /// Design informed by SC2's batch `RequestQueryPathing` (see `research/blizzard-github-analysis.md` § Part 4).
    fn batch_distances_into(
        &self,
        from: WorldPos,
        targets: &[WorldPos],
        locomotor: LocomotorType,
        out: &mut Vec<Option<SimCoord>>,
    );

    /// Convenience wrapper for non-hot paths (tools/debug/tests).
    /// Hot gameplay loops should prefer `batch_distances_into`.
    fn batch_distances(
        &self,
        from: WorldPos,
        targets: &[WorldPos],
        locomotor: LocomotorType,
    ) -> Vec<Option<SimCoord>> {
        let mut out = Vec::with_capacity(targets.len());
        self.batch_distances_into(from, targets, locomotor, &mut out);
        out
    }
}
}

SpatialIndex Trait

#![allow(unused)]
fn main() {
/// Game modules implement this for spatial queries (range checks, collision, targeting).
/// Grid-based games use a spatial hash grid. Continuous-space games could use BVH or R-tree.
/// The engine core queries this trait — never a specific data structure.
pub trait SpatialIndex: Send + Sync {
    /// Find all entities within range of a position.
    /// Writes results into caller-provided scratch (`out`) with deterministic ordering.
    /// Contract: for identical sim state + filter, the output order must be identical on all clients.
    /// Default recommendation is ascending `EntityId`, unless a stricter subsystem-specific contract exists.
    fn query_range_into(
        &self,
        center: WorldPos,
        range: SimCoord,
        filter: EntityFilter,
        out: &mut Vec<EntityId>,
    );

    /// Update entity position in the index.
    fn update_position(&mut self, entity: EntityId, old: WorldPos, new: WorldPos);

    /// Remove entity from the index.
    fn remove(&mut self, entity: EntityId);
}
}

Determinism, Snapshot, and Cache Rules (Pathfinding/Spatial)

The Pathfinder and SpatialIndex traits are algorithm seams, but they still operate under the simulation’s deterministic/snapshottable rules:

  • Authoritative state lives in ECS/components, not only inside opaque pathfinder internals.
  • Path IDs are local handles, not stable serialized identifiers.
  • Derived caches (flowfield caches, sector caches, spatial buckets, temporary query results) may be omitted from snapshots and rebuilt on load/restore/reconnect.
  • Pending path requests must be either:
    • represented in authoritative sim state, or
    • safely reconstructible deterministically on restore.
  • Internal parallelism is allowed only if the visible outputs (paths, distances, query results) are deterministic and independent of worker scheduling/order.
  • Validation/debug tooling may recompute caches from authoritative state (see 03-NETCODE.md cache validation) to detect missed invalidation bugs.

Why This Matters

This is the same philosophy as WorldPos.z — costs near-zero now, prevents rewrites later:

AbstractionCosts NowSaves Later
WorldPos.zOne extra i32 per positionRA2/TS elevation works without restructuring coordinates
NetworkModelOne trait + LocalNetwork implMultiplayer netcode slots in without touching sim
InputSourceOne trait + mouse/keyboard implTouch/gamepad slot in without touching game loop
PathfinderOne trait + multi-layer hybrid impl firstNavmesh pathfinding slots in; RA1 ships 3 impls (D045)
SpatialIndexOne trait + spatial hash implBVH/R-tree slots in without touching combat/targeting
FogProviderOne trait + radius fog implElevation fog, fog-authoritative server slot in
DamageResolverOne trait + standard pipeline implShield-first/sub-object damage models slot in
AiStrategyOne trait + personality-driven AI implNeural/planning/custom AI slots in without forking ic-ai
RankingProviderOne trait + Glicko-2 implCommunity servers choose their own rating algorithm
OrderValidatorOne trait + standard validation implEngine enforces validation; modules can’t skip it silently

The RA1 game module registers three Pathfinder implementations — RemastersPathfinder, OpenRaPathfinder, and IcPathfinder (D045) — plus GridSpatialHash. The active pathfinder is selected via experience profiles (D045). A deferred/optional continuous-space game module would register NavmeshPathfinder and BvhSpatialIndex. The sim core calls the trait — it never knows which one is running. The same principle applies to fog, damage, AI, ranking, and validation — see D041 in decisions/09d-gameplay.md for the full trait definitions and rationale.