AppleyAppley Docs
Docs/API Reference

Pathfinding

Implement A* and Dijkstra pathfinding for grid and graph-based worlds.

27 min read Advanced Updated 2026-07-01 v2.0 ~110 min to complete

Introduction

A* is the industry-standard pathfinding algorithm used in everything from RTS games to navigation apps. This guide explains how A* works, how to optimize it with heuristics and binary heaps, and how to implement it in Appley for grid-based and graph-based worlds.

Advanced Guide 13 of 25

This is part of the Advanced Documentation collection. Category: AI & Systems. Estimated completion: 110 minutes. Difficulty: Advanced/Professional.

Screenshot

Pathfinding
Pathfinding

How Professional Games Do It

1

U

n

2

A

*

3

C

h

4

F

o

5

U

s

6

A

*

7

H

a

8

D

i

9

O

p

10

F

o

11

C

a

12

I

f

FluxScript Implementation

Here is how to build this system in Appley using FluxScript (Appley Scripting):

Pathfinding — FluxScript
// A* pathfinding with binary heap priority queue
const GRID_W = 100
const GRID_H = 100
grid = null  // 0=walkable, 1+=cost multiplier, -1=wall

on start {
  grid = new Int8Array(GRID_W * GRID_H)
  // Populate grid from terrain
  for x = 0 to GRID_W {
    for y = 0 to GRID_H {
      terrainType = terrain.getTile(x, y)
      if terrainType == "water" grid[x + y * GRID_W] = -1
      else if terrainType == "mud" grid[x + y * GRID_W] = 4
      else if terrainType == "grass" grid[x + y * GRID_W] = 2
      else grid[x + y * GRID_W] = 1  // road/walkable
    }
  }
}

func findPath(startX, startY, endX, endY) {
  // A* with binary heap
  open = new PriorityQueue((a, b) -> a.f < b.f)
  closed = new Set()
  gScore = {}
  cameFrom = {}

  startKey = startX + "," + startY
  gScore[startKey] = 0
  open.enqueue({
    x: startX, y: startY, f: heuristic(startX, startY, endX, endY)
  })

  while open.size() > 0 {
    current = open.dequeue()
    currentKey = current.x + "," + current.y

    // Goal reached
    if current.x == endX && current.y == endY {
      return reconstructPath(cameFrom, currentKey)
    }

    closed.add(currentKey)

    // Check 8 neighbors
    neighbors = [
      [1,0], [-1,0], [0,1], [0,-1],  // 4-connected
      [1,1], [1,-1], [-1,1], [-1,-1]  // diagonals
    ]

    for n = 0 to neighbors.length {
      nx = current.x + neighbors[n][0]
      ny = current.y + neighbors[n][1]

      // Bounds check
      if nx < 0 || nx >= GRID_W continue
      if ny < 0 || ny >= GRID_H continue

      nKey = nx + "," + ny
      if closed.has(nKey) continue

      // Terrain cost
      tileCost = grid[nx + ny * GRID_W]
      if tileCost < 0 continue  // Wall

      // Diagonal movement costs more
      moveCost = (neighbors[n][0] != 0 && neighbors[n][1] != 0) ? 1.414 : 1.0
      tentativeG = gScore[currentKey] + moveCost * tileCost

      // Check if this is a better path
      if gScore[nKey] == null || tentativeG < gScore[nKey] {
        cameFrom[nKey] = currentKey
        gScore[nKey] = tentativeG
        f = tentativeG + heuristic(nx, ny, endX, endY)
        open.enqueue({ x: nx, y: ny, f: f })
      }
    }
  }

  return null  // No path found
}

// Manhattan heuristic (never overestimates on 4-connected grid)
func heuristic(x1, y1, x2, y2) {
  return math.abs(x1 - x2) + math.abs(y1 - y2)
}

// Reconstruct path from cameFrom map
func reconstructPath(cameFrom, endKey) {
  path = [endKey]
  current = endKey
  while cameFrom[current] != null {
    current = cameFrom[current]
    path.unshift(current)
  }
  // Convert keys to positions
  result = []
  for key in path {
    parts = string.split(key, ",")
    result.push(vec3(math.parse(parts[0]), 0, math.parse(parts[1])))
  }
  return result
}

// Path caching for shared destinations
pathCache = {}

func findPathCached(start, end) {
  cacheKey = start.x + "," + start.z + "->" + end.x + "," + end.z
  if pathCache[cacheKey] != null {
    return pathCache[cacheKey]
  }
  path = findPath(start.x, start.z, end.x, end.z)
  pathCache[cacheKey] = path
  return path
}

// Invalidate cache when terrain changes
on event("terrainChanged", data) {
  pathCache = {}  // Clear all cached paths
}

Node Graph Equivalent

The same system implemented as a Node Graph in Appley Node Scripting:

Pathfinding — Node Graph
[A* Algorithm]
   --> [Init: open=PriorityQueue, closed=Set, gScore={}] --> [Start: g=0, f=heuristic]
   --> [While: open not empty]
        --> [Dequeue: lowest-f node]
        --> [Branch: goal reached?]
             +-- True --> [Reconstruct path] --> [Return]
        --> [Add: to closed set]
        --> [For Each: 8 neighbors]
             --> [Bounds + wall check]
             --> [Calculate: tentative g = currentG + moveCost * tileCost]
             --> [Branch: better than known g?]
                  +-- True --> [Update: cameFrom, gScore, f]
                            --> [Enqueue: into open]
   --> [Return: null (no path)]

[Function: heuristic] --> [Manhattan: |x1-x2| + |y1-y2|]

[Path Caching]
   --> [Check: cache key exists] --> [Return cached]
   --> [Else: compute + cache]
   --> [On terrainChanged: clear cache]

Professional Optimization Tips

How AAA studios optimize these systems — applicable to your Appley project:

  • Use a binary heap for the open set. A simple array gives O(n) extract-min. A binary heap gives O(log n). For a 100x100 grid, this is the difference between 10ms and 500ms per pathfinding call.
  • Use Manhattan heuristic for 4-connected grids, Euclidean for 8-connected. The heuristic must never overestimate real cost (admissible). Overestimating produces fast but non-optimal paths.
  • Cache paths for shared destinations. If 10 enemies path to the same target, compute once and share. Cache invalidation on terrain changes is cheaper than recomputing 10 paths.
  • Use Jump Point Search for uniform grids. JPS is 10-20x faster than A* on grids with uniform movement costs. It jumps in straight lines, skipping symmetric path equivalents.

Common Mistakes

Using a sorted array instead of a heap

Array-based priority queues are O(n) for extract-min. For large grids, this makes A* 50x slower. Always use a binary heap or Fibonacci heap.

Heuristic overestimates real cost

If the heuristic is higher than the actual cost, A* finds a path but not the optimal one. Use admissible heuristics (Manhattan for grids, straight-line for graphs).