Skip to content

PPather V1 pathfinding: SoA SpotManager, grid-based connectivity, wall and bridge falloff avoidance#761

Merged
Xian55 merged 1 commit intodevfrom
refactor/v1-ppather-soa-avoidance
Feb 8, 2026
Merged

PPather V1 pathfinding: SoA SpotManager, grid-based connectivity, wall and bridge falloff avoidance#761
Xian55 merged 1 commit intodevfrom
refactor/v1-ppather-soa-avoidance

Conversation

@Xian55
Copy link
Owner

@Xian55 Xian55 commented Feb 8, 2026

TLDR:

  • Less likely to hug walls
  • More likely to reach the destination via the navigation
  • Respects the current player movement limitation and accounts for them

Introduce Structure-of-Arrays (SoA) data layout for pathfinding graph nodes via a new SpotManager, replacing the previous object-scattered Spot storage. Add grid-aligned spot placement, wall/edge repulsion post-processing, and improved triangle scoring -- all validated by iterative benchmarking.

=== Architecture: SpotManager (SoA data layout) ===

  • NEW PPather/Graph/SpotManager.cs: Central manager storing all spot data in contiguous parallel arrays (locations, flags, path edges, chunks) indexed by a cached Spot.ManagerIndex for O(1) access.

    • Flattened edge storage in a single float[] with per-spot offset/capacity tracking, eliminating per-Spot float[] path allocations.
    • Edge spot cache (edgeSpotCache[]) avoids costly coordinate->Spot round-trips in GetPathsToSpots; cache misses resolved lazily.
    • Reusable neighborBuffer for single-threaded A* neighbor enumeration.
    • Cached triangle scores (short[]) per spot to avoid redundant closeness+gradient recomputation during a single search.
  • NEW PPather/Graph/SearchBuffer.cs: Pooled per-search state buffer (traceBack indices, G/F scores, closed/scoreSet BitArrays) allocated from a ConcurrentStack pool and returned after each search. Replaces search state fields that were previously scattered across individual Spot objects (searchID, traceBack, score, closed, scoreSet). ~180 KB per search, reused across searches via pool.

  • CHANGED PPather/Graph/Spot.cs: Stripped down to a lightweight identifier. Removed ~170 lines of path management, search state, and scoring methods. All data now lives in SpotManager arrays. Only Loc, flags, chunk, next (for spatial hash chaining), and ManagerIndex remain. Added FLAG_ON_OBJECT (0x0040) to distinguish bridge/ramp surfaces from indoor corridors.

=== Pathfinding: Grid-based connectivity ===

  • CHANGED PPather/Graph/PathGraph.cs:

    • Replaced radial spot scanning (FindAllSpots + angle-based loop) with grid-aligned 8-neighbor connectivity using SpotGridSize (= WantedStepLength).
    • SnapToGrid() ensures spots land on predictable, evenly-spaced positions.
    • AddAndConnectSpot() now connects only to 8 cardinal+diagonal grid neighbors instead of all spots within MaxStepLength radius, preventing long-distance shortcut edges.
    • CreateSpotsAroundSpot() uses grid offsets instead of radial angle sweep, producing uniform coverage with no overlap or gaps.
    • Line-of-sight checks now filter by TriangleType (Object|Model only), preventing terrain slopes from falsely blocking neighbor connections while still blocking paths through walls and tree trunks.
    • Spots standing on Object geometry (bridges, ramps) skip the LoS block check to avoid the walkable surface's own triangles blocking the ray.
    • Ceiling raycast (head->sky at CeilingCheckHeight=20) distinguishes indoor corridors (FLAG_INDOORS) from open bridges (FLAG_ON_OBJECT only).
  • All A* scoring methods (ScoreSpot_A_Star, ScoreSpot_A_Star_With_Model_ And_Gradient_Avoidance, ScoreSpot_Pather) updated to use SpotManager for distance calculations, flag checks, and search state management.

  • CreatePath() now wraps Search() in try/finally to guarantee SpotManager.CompleteSearch() returns the SearchBuffer to the pool.

  • Search start/end spots are now integrated into the grid by calling CreateSpotsAroundSpot() at path creation time.

=== Wall and edge avoidance ===

  • NEW: ApplyWallRepulsion() post-processes path waypoints (2 passes):

    • For ground-level waypoints near Model|Object walls: computes XY repulsion direction from closest wall triangle (filtering floors/ ceilings via normal.Z > 0.7 threshold), pushes waypoint away by WallAvoidanceDistance (3.0 yards) minus actual wall distance.
    • For waypoints on Object surfaces (bridges): probes 8 directions for drop-off edges, pushes toward center of walkable surface.
    • Validates all pushed positions are standable before applying.
    • Only applied when SearchStrategy is A_Star_With_Model_Avoidance.
  • CHANGED: IsCloseToObjectRange increased from MinStepLength to MaxStepLength for broader object proximity detection.

  • CHANGED Core/GoalsComponent/Navigation.cs: minAngleToStopBeforeTurn reduced from PI/2 (90 degrees) to PI/3 (60 degrees) for smoother turning behavior.

=== Triangle geometry improvements ===

  • CHANGED PPather/Triangles/ChunkedTriangleCollection.cs:

    • NEW: GradiantScoreTiered() -- single-pass tiered gradient scoring at ranges 1/2/3 instead of 3 separate GradiantScore() calls.
    • NEW: ClosestDistanceToType() -- returns continuous float distance to nearest triangle of given type (replaces boolean IsCloseToType for closeness scoring, enabling smooth cost gradients).
    • NEW: CombinedScoring() -- fused closeness+gradient scoring in one triangle query pass, eliminating duplicate GetChunkAt/GetTriangleMatrix/ GetAllCloseTo lookups. Filters wall-like triangles (|normal.Z| <= 0.7) for closeness, ignoring floors and ceilings.
    • NEW: LineOfSightExists(a, b, blockingMask) overload -- LoS check considering only triangles matching a TriangleType mask.
    • NEW: TryGetWallRepulsion() -- finds closest wall triangle within range and returns XY repulsion direction + distance for path smoothing.
    • FIX: IsCloseToType() now checks maxZ (not minZ) against toon height, fixing false negatives for triangles partially above the toon.
  • NEW PPather/Triangles/MCNKHelper.cs: MCNK-aligned coordinate conversion utilities (ADT->MCNK coord mapping, bounds calculation, AABB intersection tests for Model/WMO instances). Enables MCNK-granularity chunk loading.

  • CHANGED PPather/Triangles/MPQTriangleSupplier.cs: NEW GetMCNKTriangles() loads only a single MCNK's terrain (145 vertices) plus spatially-culled models/WMOs, ~97% memory reduction vs full ADT loading.

  • NEW PPather/Triangles/Utils.cs: Added ClosestPointOnSegment() and ClosestPointOnTriangle() for wall repulsion distance calculations.

  • CHANGED PPather/Triangles/TriangleMatrix.cs: Replaced ArrayPool
    rentals with ThreadStatic reusable buffer (t_elementBuffer) to eliminate per-query pool overhead in hot triangle lookup paths.

=== GraphChunk alignment ===

  • CHANGED PPather/Graph/GraphChunk.cs:
    • CHUNK_SIZE reduced from 256 to 34 (MCNK-aligned: 33.33 yards), reducing per-chunk spot array from 65,536 to 1,156 entries (56x smaller).
    • Constructor accepts optional SpotManager for eager spot registration.
    • Load/Save updated to use SpotManager for path storage instead of per-Spot float[] paths.

=== Visualization enhancements ===

  • CHANGED PathingAPI/Pages/Watch.razor:

    • Replaced binary Show3D toggle with 5-mode geometry cycle: Off -> SparseFromSpots -> WalkableOnly -> ExploredArea -> All
    • Each mode has distinct color indicator (cyan/yellow/orange/green).
    • Spots are filtered to chunk bounds before mesh generation.
  • NEW PPather/Search/MeshFactory.cs: Three sparse mesh creation variants:

    • CreateSparseFromSpots: Only triangles intersecting discovered spots.
    • CreateWalkableOnly: Pre-filtered walkable vertices near explored spots.
    • CreateExploredArea: All triangles within explored bounding box.
  • CHANGED PPather/Search/PPatherService.cs: Exposed TriangleWorld property and GetSpots() method for visualization access.

=== Benchmarks ===

  • NEW Benchmarks/PPather/PPather_PathGraph_Performance.cs: BenchmarkDotNet suite measuring SpotManager operations (location lookups, distance calcs, flag ops, path reconstruction, A* scoring loop, SearchBuffer alloc/reset).

=== Performance progression (30-route PathingAPI benchmark suite) ===

Before refactor: 297ms avg, 171ms median, 17.86s total
After initial SoA: 357ms avg, 142ms median, 21.45s total (regression)
Manager index opt: 199ms avg, 10ms median, 11.97s total (breakthrough)
Final stabilized: 243ms avg, 74ms median, 14.65s total
Third warm run: 201ms avg, 62ms median, 12.05s total

Median improved 64% (171ms -> 62ms). Key route improvements:

  • "Coldridge->gnome": 950ms -> 187ms (80% faster)
  • "Redridge Grave->North": 570ms -> 248ms (56% faster)

Few issues which had been addressed

Before: Too close to corridor walls often hugging them
hugs_close_to_wall

After
image


Before
hugging_wall

After
image


After
image


Before: Unable to step on ramps during the terran type transition
unable_to_step_on_the_ramp_stairs

After
image


Before: Crossing bridges dangerously close to the edge
bridges_objects_dangerously_close_to_edge

After
made_difference

…y, and wall avoidance

Introduce Structure-of-Arrays (SoA) data layout for pathfinding graph nodes
via a new SpotManager, replacing the previous object-scattered Spot storage.
Add grid-aligned spot placement, wall/edge repulsion post-processing, and
improved triangle scoring -- all validated by iterative benchmarking.

=== Architecture: SpotManager (SoA data layout) ===

- NEW PPather/Graph/SpotManager.cs: Central manager storing all spot data
  in contiguous parallel arrays (locations, flags, path edges, chunks)
  indexed by a cached Spot.ManagerIndex for O(1) access.
  - Flattened edge storage in a single float[] with per-spot offset/capacity
    tracking, eliminating per-Spot float[] path allocations.
  - Edge spot cache (edgeSpotCache[]) avoids costly coordinate->Spot
    round-trips in GetPathsToSpots; cache misses resolved lazily.
  - Reusable neighborBuffer for single-threaded A* neighbor enumeration.
  - Cached triangle scores (short[]) per spot to avoid redundant
    closeness+gradient recomputation during a single search.

- NEW PPather/Graph/SearchBuffer.cs: Pooled per-search state buffer
  (traceBack indices, G/F scores, closed/scoreSet BitArrays) allocated
  from a ConcurrentStack<SearchBuffer> pool and returned after each search.
  Replaces search state fields that were previously scattered across
  individual Spot objects (searchID, traceBack, score, closed, scoreSet).
  ~180 KB per search, reused across searches via pool.

- CHANGED PPather/Graph/Spot.cs: Stripped down to a lightweight identifier.
  Removed ~170 lines of path management, search state, and scoring methods.
  All data now lives in SpotManager arrays. Only Loc, flags, chunk, next
  (for spatial hash chaining), and ManagerIndex remain.
  Added FLAG_ON_OBJECT (0x0040) to distinguish bridge/ramp surfaces from
  indoor corridors.

=== Pathfinding: Grid-based connectivity ===

- CHANGED PPather/Graph/PathGraph.cs:
  - Replaced radial spot scanning (FindAllSpots + angle-based loop) with
    grid-aligned 8-neighbor connectivity using SpotGridSize (= WantedStepLength).
  - SnapToGrid() ensures spots land on predictable, evenly-spaced positions.
  - AddAndConnectSpot() now connects only to 8 cardinal+diagonal grid
    neighbors instead of all spots within MaxStepLength radius, preventing
    long-distance shortcut edges.
  - CreateSpotsAroundSpot() uses grid offsets instead of radial angle sweep,
    producing uniform coverage with no overlap or gaps.
  - Line-of-sight checks now filter by TriangleType (Object|Model only),
    preventing terrain slopes from falsely blocking neighbor connections
    while still blocking paths through walls and tree trunks.
  - Spots standing on Object geometry (bridges, ramps) skip the LoS block
    check to avoid the walkable surface's own triangles blocking the ray.
  - Ceiling raycast (head->sky at CeilingCheckHeight=20) distinguishes
    indoor corridors (FLAG_INDOORS) from open bridges (FLAG_ON_OBJECT only).

- All A* scoring methods (ScoreSpot_A_Star, ScoreSpot_A_Star_With_Model_
  And_Gradient_Avoidance, ScoreSpot_Pather) updated to use SpotManager
  for distance calculations, flag checks, and search state management.

- CreatePath() now wraps Search() in try/finally to guarantee
  SpotManager.CompleteSearch() returns the SearchBuffer to the pool.

- Search start/end spots are now integrated into the grid by calling
  CreateSpotsAroundSpot() at path creation time.

=== Wall and edge avoidance ===

- NEW: ApplyWallRepulsion() post-processes path waypoints (2 passes):
  - For ground-level waypoints near Model|Object walls: computes XY
    repulsion direction from closest wall triangle (filtering floors/
    ceilings via normal.Z > 0.7 threshold), pushes waypoint away by
    WallAvoidanceDistance (3.0 yards) minus actual wall distance.
  - For waypoints on Object surfaces (bridges): probes 8 directions for
    drop-off edges, pushes toward center of walkable surface.
  - Validates all pushed positions are standable before applying.
  - Only applied when SearchStrategy is A_Star_With_Model_Avoidance.

- CHANGED: IsCloseToObjectRange increased from MinStepLength to
  MaxStepLength for broader object proximity detection.

- CHANGED Core/GoalsComponent/Navigation.cs: minAngleToStopBeforeTurn
  reduced from PI/2 (90 degrees) to PI/3 (60 degrees) for smoother
  turning behavior.

=== Triangle geometry improvements ===

- CHANGED PPather/Triangles/ChunkedTriangleCollection.cs:
  - NEW: GradiantScoreTiered() -- single-pass tiered gradient scoring at
    ranges 1/2/3 instead of 3 separate GradiantScore() calls.
  - NEW: ClosestDistanceToType() -- returns continuous float distance to
    nearest triangle of given type (replaces boolean IsCloseToType for
    closeness scoring, enabling smooth cost gradients).
  - NEW: CombinedScoring() -- fused closeness+gradient scoring in one
    triangle query pass, eliminating duplicate GetChunkAt/GetTriangleMatrix/
    GetAllCloseTo lookups. Filters wall-like triangles (|normal.Z| <= 0.7)
    for closeness, ignoring floors and ceilings.
  - NEW: LineOfSightExists(a, b, blockingMask) overload -- LoS check
    considering only triangles matching a TriangleType mask.
  - NEW: TryGetWallRepulsion() -- finds closest wall triangle within range
    and returns XY repulsion direction + distance for path smoothing.
  - FIX: IsCloseToType() now checks maxZ (not minZ) against toon height,
    fixing false negatives for triangles partially above the toon.

- NEW PPather/Triangles/MCNKHelper.cs: MCNK-aligned coordinate conversion
  utilities (ADT->MCNK coord mapping, bounds calculation, AABB intersection
  tests for Model/WMO instances). Enables MCNK-granularity chunk loading.

- CHANGED PPather/Triangles/MPQTriangleSupplier.cs: NEW GetMCNKTriangles()
  loads only a single MCNK's terrain (145 vertices) plus spatially-culled
  models/WMOs, ~97% memory reduction vs full ADT loading.

- NEW PPather/Triangles/Utils.cs: Added ClosestPointOnSegment() and
  ClosestPointOnTriangle() for wall repulsion distance calculations.

- CHANGED PPather/Triangles/TriangleMatrix.cs: Replaced ArrayPool<int>
  rentals with ThreadStatic reusable buffer (t_elementBuffer) to eliminate
  per-query pool overhead in hot triangle lookup paths.

=== GraphChunk alignment ===

- CHANGED PPather/Graph/GraphChunk.cs:
  - CHUNK_SIZE reduced from 256 to 34 (MCNK-aligned: 33.33 yards),
    reducing per-chunk spot array from 65,536 to 1,156 entries (56x smaller).
  - Constructor accepts optional SpotManager for eager spot registration.
  - Load/Save updated to use SpotManager for path storage instead of
    per-Spot float[] paths.

=== Visualization enhancements ===

- CHANGED PathingAPI/Pages/Watch.razor:
  - Replaced binary Show3D toggle with 5-mode geometry cycle:
    Off -> SparseFromSpots -> WalkableOnly -> ExploredArea -> All
  - Each mode has distinct color indicator (cyan/yellow/orange/green).
  - Spots are filtered to chunk bounds before mesh generation.

- NEW PPather/Search/MeshFactory.cs: Three sparse mesh creation variants:
  - CreateSparseFromSpots: Only triangles intersecting discovered spots.
  - CreateWalkableOnly: Pre-filtered walkable vertices near explored spots.
  - CreateExploredArea: All triangles within explored bounding box.

- CHANGED PPather/Search/PPatherService.cs: Exposed TriangleWorld property
  and GetSpots() method for visualization access.

=== Benchmarks ===

- NEW Benchmarks/PPather/PPather_PathGraph_Performance.cs: BenchmarkDotNet
  suite measuring SpotManager operations (location lookups, distance calcs,
  flag ops, path reconstruction, A* scoring loop, SearchBuffer alloc/reset).

=== Performance progression (30-route PathingAPI benchmark suite) ===

  Before refactor:    297ms avg, 171ms median, 17.86s total
  After initial SoA:  357ms avg, 142ms median, 21.45s total (regression)
  Manager index opt:  199ms avg,  10ms median, 11.97s total (breakthrough)
  Final stabilized:   243ms avg,  74ms median, 14.65s total
  Third warm run:     201ms avg,  62ms median, 12.05s total

  Median improved 64% (171ms -> 62ms). Key route improvements:
  - "Coldridge->gnome": 950ms -> 187ms (80% faster)
  - "Redridge Grave->North": 570ms -> 248ms (56% faster)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@Xian55 Xian55 added bugfix This pull request fixes an issue. enhancement This pull request implements a new feature. labels Feb 8, 2026
@Xian55 Xian55 changed the title Refactor PPather pathfinding: SoA SpotManager, grid-based connectivity, and wall avoidance Refactor PPather pathfinding: SoA SpotManager, grid-based connectivity, wall and bridge falloff avoidance Feb 8, 2026
@Xian55 Xian55 merged commit a83b820 into dev Feb 8, 2026
1 check passed
@Xian55 Xian55 deleted the refactor/v1-ppather-soa-avoidance branch February 8, 2026 00:31
@Xian55 Xian55 changed the title Refactor PPather pathfinding: SoA SpotManager, grid-based connectivity, wall and bridge falloff avoidance PPather V1 pathfinding: SoA SpotManager, grid-based connectivity, wall and bridge falloff avoidance Feb 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfix This pull request fixes an issue. enhancement This pull request implements a new feature.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant