"Prune a waypoint graph by connected component, not by isolated nodes"
▸ SYMPTOM
- Some agents in a crowd never move, or a fraction of a spawn wave ends up stuck in one region of the map.
- Every per-node check the graph builder runs passes: each node has edges, the edges resolve, and no reference dangles.
- The stuck count is small and quiet, so the bug survives on every build since the graph first shipped.
▸ CAUSE
Pruning nodes with zero edges removes the obviously broken case. It says nothing about a cluster of nodes that connect to each other but not to the rest of the graph.
Picture a sealed arena corner wired as a clean little sub-graph. Every node in it has edges, and every edge resolves. The sub-graph simply never joins the main graph.
One build measured this directly. Sealed arena corners stranded 27 of 524 nodes as three internally-connected pockets. About 5% of a max-size crowd spawned into corners it could never leave.
The per-node validity checks all passed for every node in every pocket, because "has edges" and "edges resolve" are local properties. Reachability is a global property, so a local check cannot see it. Graph kept is not graph reachable.
▸ FIX
Test reachability with a flood fill, then filter candidates by the result.
- Pick a known-good root node, such as the main spawn or hub node.
- Flood-fill outward along edges from that root and mark every node you reach.
- Treat any unmarked node as unreachable, whatever its local edge count.
- Filter destination selection, portal placement, and spawn points to marked nodes only.
Run the flood fill once when the graph builds and cache the connected-component id per node. Any system built on top of the graph then reads that id instead of re-deriving reachability.
▸ WHY IT WORKS
A flood fill from a root computes the connected component that contains the root, which is exactly the set of nodes an agent starting at the root can reach. A per-node check like "has edges" is local, so it accepts a node inside a sealed pocket. The flood fill is global, so it rejects that node because no path from the root ever reaches it. Filtering on the component id turns an invisible failure into a build-time fact you can count.
- Published