Skip to content

Tutorial: pathfinding + rbush (A* + spatial acceleration)

A quick story: your units navigate around obstacles. Plain A* worked until obstacle counts spiked, then it lagged. You indexed obstacles with an R-tree: now "nearby blockers" query in milliseconds; combine with grid pathfinding and dynamic updates—stutters gone.


1. Install

bash
npm install pathfinding rbush

2. Minimal A* on a grid

ts
import PF from "pathfinding";

const matrix = [
  [0, 0, 0, 0, 0],
  [0, 1, 1, 1, 0],
  [0, 0, 0, 0, 0],
  [0, 1, 0, 1, 0],
  [0, 0, 0, 0, 0],
];

const grid = new PF.Grid(matrix[0].length, matrix.length, matrix);
const finder = new PF.AStarFinder({ allowDiagonal: false });
const path = finder.findPath(0, 0, 4, 4, grid.clone());

3. rbush for fast nearby-obstacle queries

ts
import RBush from "rbush";

type Box = { minX:number; minY:number; maxX:number; maxY:number; id:string };
const tree = new RBush<Box>();

tree.insert({ minX:4, minY:4, maxX:8, maxY:6, id:"wall-1" });
const hits = tree.search({ minX:0, minY:0, maxX:5, maxY:5 });

4. Project obstacles to a grid (build the walkable matrix)

ts
function buildGridFromTree(tree: RBush<Box>, cols:number, rows:number, cell:number){
  const mat:number[][] = Array.from({length:rows},()=>Array(cols).fill(0));
  for (const b of tree.all()){
    const minCX = Math.max(0, Math.floor(b.minX / cell));
    const minCY = Math.max(0, Math.floor(b.minY / cell));
    const maxCX = Math.min(cols-1, Math.floor(b.maxX / cell));
    const maxCY = Math.min(rows-1, Math.floor(b.maxY / cell));
    for (let cy=minCY; cy<=maxCY; cy++) for (let cx=minCX; cx<=maxCX; cx++) mat[cy][cx] = 1;
  }
  return mat;
}

5. Pathfinding from world coords (snap to nearest reachable cell)

ts
import PF from "pathfinding";

function worldToCell(x:number, y:number, cell:number){ return { cx:Math.floor(x/cell), cy:Math.floor(y/cell) }; }

function findPathWorld(tree: RBush<Box>, start:{x:number;y:number}, goal:{x:number;y:number}, cols:number, rows:number, cell:number){
  const mat = buildGridFromTree(tree, cols, rows, cell);
  const grid = new PF.Grid(cols, rows, mat);
  const finder = new PF.AStarFinder({ allowDiagonal:false, heuristic:PF.Heuristic.manhattan });
  const s = worldToCell(start.x, start.y, cell);
  const g = worldToCell(goal.x, goal.y, cell);
  const clamp = (v:number, lo:number, hi:number)=>Math.max(lo, Math.min(hi, v));
  s.cx = clamp(s.cx, 0, cols-1); s.cy = clamp(s.cy, 0, rows-1);
  g.cx = clamp(g.cx, 0, cols-1); g.cy = clamp(g.cy, 0, rows-1);
  if (!grid.isWalkableAt(s.cx, s.cy)) grid.setWalkableAt(s.cx, s.cy, true);
  if (!grid.isWalkableAt(g.cx, g.cy)) grid.setWalkableAt(g.cx, g.cy, true);
  const path = finder.findPath(s.cx, s.cy, g.cx, g.cy, grid);
  return path.map(([cx,cy])=>({ x:(cx+0.5)*cell, y:(cy+0.5)*cell }));
}

6. Dynamics and tips

  • Dynamic updates: update rbush entries on obstacle move; rebuild grid locally if needed.
  • Avoid crowding: index unit footprints as boxes too; include them during planning.
  • Smooth the path: post-process with corner-cutting/funnel.
  • Balance cost vs. resolution: start coarse, then refine locally.

神岛实验室