← Back to Blog
TUTORIALS

How to Build A* Pathfinding for HTML5 Games

Build fast A* pathfinding for HTML5 games with practical grids, heuristics, priority queues, movement costs, smoothing, and performance testing.

How to Build A* Pathfinding for HTML5 Games

Excerpt: A* pathfinding gives HTML5 games fast, controllable routes across grids and navigation graphs. This practical guide covers heuristics, priority queues, movement costs, path smoothing, debugging, and performance choices for browser games.

Pathfinding looks simple until agents must move around walls, choose among uneven terrain costs, and react without freezing the main loop. A* remains a strong default because it combines Dijkstra's reliable cost search with a heuristic that points the search toward the destination.

How A* chooses a route

Each searchable cell or node stores three values:

  • g: the known movement cost from the start to this node.
  • h: an estimated cost from this node to the goal.
  • f: the combined score, calculated as g + h.

The algorithm repeatedly expands the open node with the smallest f score. When it reaches the goal, parent references reconstruct the route. The heuristic improves speed, but it must not overestimate the remaining cost if you need a guaranteed shortest path.

Represent the world

Grid maps

A grid is ideal for tile games, tactics, maze games, and worlds that already use discrete cells. Store walkability and base movement cost in compact arrays. Convert world coordinates to cell coordinates only at the pathfinding boundary, then convert the final route back to world positions.

Navigation graphs

For platformers, roads, rooms, or sparse open worlds, explicit graph nodes can be cheaper than searching every tile. Connect only meaningful waypoints and store the travel cost on each edge. A navigation mesh is another option for continuous movement, but A* still commonly searches the polygon graph.

When many moving entities need nearby-neighbor queries as well as paths, combine navigation with a spatial hash grid instead of scanning every entity on every frame.

Use the right heuristic

Movement model Heuristic Formula
Four-direction grid Manhattan abs(dx) + abs(dy)
Eight-direction grid Octile max(dx,dy) + (sqrt(2)-1)*min(dx,dy)
Free-angle graph Euclidean sqrt(dx*dx + dy*dy)

Multiply the heuristic by the minimum legal terrain cost. If roads cost 1 and mud costs 3, the minimum is still 1. Increasing the heuristic weight can produce faster, less optimal routes; expose that as a deliberate quality setting rather than a hidden magic number.

A compact JavaScript structure

function findPath(start, goal, grid) {
  const open = new MinHeap((a, b) => a.f - b.f);
  const cameFrom = new Map();
  const gScore = new Map([[key(start), 0]]);
  open.push({ ...start, f: heuristic(start, goal) });

  while (open.size) {
    const current = open.pop();
    if (sameCell(current, goal)) {
      return rebuildPath(cameFrom, current);
    }

    for (const next of neighbors(current, grid)) {
      const tentative = gScore.get(key(current)) + moveCost(current, next, grid);
      if (tentative < (gScore.get(key(next)) ?? Infinity)) {
        cameFrom.set(key(next), current);
        gScore.set(key(next), tentative);
        open.push({ ...next, f: tentative + heuristic(next, goal) });
      }
    }
  }
  return [];
}

This outline omits production details such as stale heap entries, typed-array storage, search IDs, and cancellation. Keep the core algorithm small, then add optimizations only after measuring real maps.

Build an efficient open set

A binary min-heap keeps insertion and removal near O(log n). Avoid sorting a JavaScript array after every insertion. A simple implementation may push a node again when its score improves and ignore stale entries when popped. This is often easier and fast enough; a more complex indexed heap can update priorities in place.

For fixed grids, numeric cell IDs and typed arrays usually outperform string keys and per-node objects. Keep arrays for g score, parent ID, visit state, and search generation. A generation counter avoids clearing the entire map between searches.

Handle diagonal movement correctly

If diagonal movement is permitted, decide whether an agent may cut through a blocked corner. In most games, a diagonal neighbor should be rejected when either adjacent orthogonal cell is blocked. Charge approximately 1.414 times the straight cost for a diagonal step, otherwise diagonal routes become artificially cheap.

Movement costs create better game behavior

Walkable does not need to mean equal. Roads can be cheap, tall grass moderately expensive, shallow water costly, and hazards nearly forbidden. Different agent classes can supply different cost functions: a flying unit ignores ground terrain, while a cautious NPC adds cost near danger.

Path requests fit naturally into the decision layer described in HTML5 game AI state machines. The state machine decides when a path is needed; the pathfinder decides where to go.

Smooth the route without breaking it

Grid paths often look like stair steps. First remove collinear points. Then test line of sight between non-adjacent waypoints and skip intermediate nodes when the segment stays inside walkable space. Use the same collision radius as the moving agent, not a point-sized ray, or smoothed paths may clip corners.

Keep the unsmoothed path available for debugging. Rendering expanded nodes, final nodes, and collision checks in different colors makes most navigation errors obvious.

Keep pathfinding out of the frame-time danger zone

A long search can produce a visible hitch. Limit expansions per update and resume the search later, or process requests in a Web Worker when data transfer is manageable. Prioritize player-visible requests and cancel work when the target changes or an agent disappears.

Run simulation on a stable step as described in the fixed-timestep game loop guide, but budget pathfinding separately. Add frame-time telemetry so you can see whether navigation spikes align with missed frames.

  • Cache paths only when map state and agent rules match.
  • Share flow fields when many agents move to the same destination.
  • Use hierarchical regions for very large maps.
  • Repath only when the route becomes invalid or the goal moves far enough.
  • Pool temporary search records where allocation pressure is measurable; see object pooling for HTML5 games.

Test cases that catch common bugs

  1. Start equals goal: return an empty or one-node path consistently.
  2. No route exists: terminate cleanly and return a documented failure.
  3. Narrow diagonal corner: confirm the configured corner rule.
  4. Cheaper long road versus short mud route: verify movement costs win.
  5. Dynamic obstacle: invalidate or repair the affected route.
  6. Large unreachable map: confirm the expansion budget prevents a hitch.
  7. Identical-cost alternatives: use stable tie-breaking to prevent jitter between runs.

Shipping checklist

  • Select the world representation and heuristic as a pair.
  • Use a min-heap and compact numeric node IDs.
  • Define diagonal and corner-cutting rules explicitly.
  • Keep terrain costs non-negative and compatible with the heuristic.
  • Visualize open, closed, and final path nodes during development.
  • Measure expansions, search duration, queue depth, and path length.
  • Budget or move expensive searches so rendering remains responsive.

Final takeaway

A* is dependable because its behavior is understandable and testable. Start with correct grid or graph data, an admissible heuristic, and a binary heap. Then improve player-visible movement with terrain costs and smoothing. When profiling shows pressure, add typed arrays, search budgets, workers, caching, or hierarchy one step at a time.