Game AI and Pathfinding skill for Claude Code
Agents that route well, decide clearly, never stall. Claude matches the pathfinding method to the world and agent count, from grid A* and flow fields to engine navmeshes, then smooths and steers paths so movement looks natural. It picks the simplest decision model that fits, adds hysteresis and stuck detection, time-slices path requests, and checks custom A* against Dijkstra with a bundled script.
- name
game-ai-pathfinding - size187 lines · ≈2,400 tokens
- files2
- licenseMIT
- updated
What Claude sees before loading it
This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.
description: Builds game AI movement and decision making with A* on grids, navmeshes in Unity, Unreal, and Godot, flow fields for crowds, path smoothing, steering and avoidance, plus finite state machines, behavior trees, utility AI, and perception. Use when enemies get stuck or walk into walls, paths look jagged or cut corners, many units lag the frame, NPC behavior flickers between states, or when choosing between FSM, behavior tree, utility AI, and GOAP for a new character. Try asking
- Enemies keep getting stuck on corners of the navmesh in our Unity level
- Write grid A* with terrain costs for my tactics game and prove it finds optimal paths
- Our guard AI flickers between patrol and chase, redesign it as a behavior tree
Use it when
- Agents get stuck, cut corners or follow jagged paths
- Hundreds of units cause pathfinding frame spikes
- Choosing FSM, behavior tree, utility AI or GOAP for NPCs
- Porting or writing A* for a grid or custom engine
Skip it when
- Generating the levels the agents walk through — use procedural-generation
- Building LLM-driven agents that call tools — use tool-use-agents
How to check the result
Fixture maps return Dijkstra-optimal costs, a soak test with max agents shows no agent stuck over two seconds, and AI time stays inside its per-frame budget.
The common mistake it prevents
Baking the navmesh with a smaller agent radius than the character collider, so agents scrape walls and jam on every corner.
Done well means: the pathfinding method matches the world and the number of agents, paths are optimal enough and look natural after smoothing and steering, decision logic is the simplest model that expresses the design, AI costs stay inside a per-frame budget, and every agent’s current path, state, and scores can be seen in a debug view.
Ground rules
- Use the engine’s navigation system before writing your own: Unity AI Navigation
(
NavMeshSurface,NavMeshAgent), Unreal Recast navmesh withAIController, GodotNavigationRegionandNavigationAgent2D/3D. Custom A* is for grid games, custom engines, or server-side simulation. - Separate the layers: decision (what to do) -> pathfinding (route) -> steering (how to move along it) -> locomotion/animation. Bugs are easier to locate when layers do not reach into each other.
- Build debug drawing first: path polyline, navmesh, current state or BT node, perception cones, utility scores above heads.
- Budget AI time explicitly (e.g. 1-2 ms per frame total) and time-slice expensive work.
Workflow
- [ ] 1. Describe the world (grid/tiles, 2D free, 3D navmesh) and agent count
- [ ] 2. Pick pathfinding and decision models from the tables below
- [ ] 3. Enable debug drawing for paths, nav data, states
- [ ] 4. Implement; route all path requests through one queue with a per-frame budget
- [ ] 5. Add stuck detection and repath rules
- [ ] 6. Stress test with max agents; profile; tune budgets and tick rates
Pathfinding choice
| World / need | Use |
|---|---|
| Tile or grid game, few hundred agents | A* on the grid (Manhattan for 4-way, octile for 8-way) |
| Large uniform-cost grid, many queries | Jump Point Search, or HPA* (hierarchical clusters) |
| 3D levels, arbitrary geometry | Navmesh (Recast/Detour or the engine’s) + funnel smoothing |
| Hundreds or thousands of units to a shared goal (RTS, tower defense) | Flow field: one Dijkstra from the goal, every unit reads the gradient |
| Open space with dynamic obstacles | Navmesh with carving obstacles, or local avoidance on a coarse path |
| Any-angle paths on grids | Theta* or line-of-sight smoothing after A* |
A* essentials
- Heuristic must never overestimate (admissible) for optimal paths: Manhattan for 4-way,
octile for 8-way (
(dx + dy) + (sqrt(2) - 2) * min(dx, dy)), Euclidean for any-angle. Multiply by the cheapest tile cost if terrain costs vary. - Manhattan with diagonal moves overestimates: faster, but paths are not guaranteed optimal.
Weighted A* (
f = g + w*h, w = 1.2-2) is a deliberate version of the same trade-off. - Binary heap open list with lazy deletion (skip already-closed nodes on pop); a closed set or per-node generation counter instead of clearing arrays per search.
- Tie-break toward larger g (or by cross product with the start-goal line) to stop A* expanding every equal-cost node in open areas.
- Disallow diagonal moves that cut wall corners.
- Cost of entering a tile = tile cost x step length; keep this convention consistent with the heuristic.
Test a hand-written or ported A* against Dijkstra on sample maps:
python3 scripts/astar_grid.py map.txt # 4-way, Manhattan
python3 scripts/astar_grid.py map.txt --diagonal # 8-way, octile, no corner cutting
python3 scripts/astar_grid.py map.txt --diagonal --weight 1.5
Legend: S start, G goal, # wall, . cost 1, 1-9 terrain cost, ~ cost 5. The
script prints the path overlay, cost, steps, and nodes expanded, then runs Dijkstra and
exits 1 if A* returned a costlier path than allowed (inadmissible heuristic or bug), or if
no path exists. Use the same maps as fixtures for the game’s own pathfinding tests.
Path smoothing and following
- Grid paths: remove intermediate waypoints while line of sight (raycast or Bresenham with agent radius) holds; or use Theta*.
- Navmesh paths: the funnel (string-pulling) algorithm; engines already return smoothed corners.
- Follow with a lookahead point (carrot) 1-2 agent lengths ahead instead of steering to the exact next waypoint; switch waypoints when within an acceptance radius, not exact equality.
- Arrive behavior: slow down within a braking radius so agents do not overshoot and orbit the goal.
Steering and avoidance
| Behavior | Use |
|---|---|
| Seek / Arrive | Move to a point; Arrive decelerates |
| Separation | Keep spacing in groups (sum of repulsion from neighbors within radius) |
| Obstacle avoidance | Feelers (raycasts) for static, velocity obstacles for dynamic |
| Crowds | RVO/ORCA (built into Unity NavMeshAgent avoidance, Unreal Detour crowd/RVO, Godot NavigationAgent avoidance) |
| Flocking | Separation + alignment + cohesion with a spatial hash for neighbor queries |
Godot 4 avoidance pattern:
@onready var agent: NavigationAgent3D = $NavigationAgent3D
func _ready() -> void:
agent.velocity_computed.connect(func(v: Vector3) -> void:
velocity = v
move_and_slide())
func _physics_process(_delta: float) -> void:
if agent.is_navigation_finished():
return
var next := agent.get_next_path_position()
agent.velocity = global_position.direction_to(next) * speed # avoidance adjusts it
Unity: agent.SetDestination(target); check !agent.pathPending && agent.remainingDistance <= agent.stoppingDistance
for arrival; NavMeshObstacle with Carve for moving blockers; bake with NavMeshSurface
(AI Navigation package). Unreal: NavMeshBoundsVolume in the level, MoveToLocation/MoveToActor
on an AIController, Behavior Tree or StateTree for decisions, EQS for position queries.
Decision-making choice
| Character complexity | Model | Notes |
|---|---|---|
| 3-6 states, clear transitions (patrol, chase, attack) | Finite state machine | Enter/exit hooks; transitions in one place |
| Many states with shared sub-behaviors | Hierarchical FSM or StateTree | Parent states handle common transitions (e.g. “dead”) |
| Reactive, designer-authored, interruptible behaviors | Behavior tree + blackboard | Industry default for action games |
| Many competing options weighed continuously (sims, survival, squad tactics) | Utility AI | Smooth, emergent choices; needs tuning tools |
| Multi-step plans toward goals (gather, craft, build) | GOAP / HTN planning | Expensive; plan rarely, cache plans |
Behavior tree rules
- Selector = try children until one succeeds; Sequence = run children until one fails;
Runningstatus persists across ticks. - Use conditional aborts/decorators (observer aborts) so higher-priority branches interrupt lower ones when conditions change, instead of re-evaluating everything every frame.
- Tick at 5-10 Hz for most NPCs; movement and animation interpolate in between.
- Keep leaf tasks small and reusable (MoveTo, Wait, PlayMontage, SetBlackboardValue).
Utility AI rules
- Each action scores 0..1 from considerations (distance, health, ammo, threat) passed through response curves (linear, quadratic, logistic), multiplied together.
- Compensate for consideration count so actions with more factors are not penalized.
- Add momentum (bonus for the current action) or a minimum commitment time to stop dithering between two near-equal actions.
- Log the top three scores per decision; tuning without visibility is guesswork.
Perception
- Sight: distance check, then angle check against the view cone, then a line-of-sight raycast (cheapest first). Stagger checks across frames.
- Hearing: gameplay emits noise events with a radius; listeners query them, no per-frame polling.
- Memory: last known position with a timestamp in the blackboard; search behavior when the target is lost.
Performance
- Path requests go through a queue: N searches or X ms per frame; agents wait a frame or two.
- Repath only when the goal moved beyond a threshold (e.g. 1-2 m) or on a timer (0.5-1 s), not every frame.
- AI level of detail: distant or off-screen agents tick less often and skip perception raycasts.
- Flow fields for many units sharing a goal: one O(cells) pass replaces hundreds of A* runs.
- Cache paths between common points (spawn to base) and invalidate on navmesh change.
Gotchas
- Navmesh baked with a smaller agent radius than the agent’s collider: agents scrape walls and get stuck on corners. Bake per agent size class.
- Jumps, ladders, doors, and drops need off-mesh links; otherwise the navmesh says “unreachable” and agents stand still.
- Stuck detection: if progress toward the next waypoint is under a threshold for 1-2 s, repath; after repeated failures, pick a new goal or nudge the agent.
- FSM flicker: two transitions with mirrored conditions (
dist < 10chase,dist > 10patrol) oscillate at the boundary; add hysteresis (chase under 10, give up over 14). - Dynamic carving every frame is expensive; carve only when an obstacle stops moving.
- Pathfinding on the main thread with large graphs causes spikes; time-slice or move it to a job.
Verification
- Unit tests on fixture maps: costs match Dijkstra (or the weight bound), no corner cutting, unreachable goals return “no path” quickly.
- Debug view during play: paths follow corridors smoothly, no agents stuck for more than 2 s in a 10-minute soak with max agents.
- Profiler: AI and navigation total under the agreed per-frame budget at peak agent count, with no single-frame spikes from path requests.
- Decision logs show no state oscillation and expected priorities in scripted scenarios.
Bundled files
Loaded only when a step in the skill needs them. Review them here before installing.
scripts/astar_grid.py
#!/usr/bin/env python3
"""A* on an ASCII grid, with an optimality check against Dijkstra (stdlib only).
Grid legend: S start G goal # wall . cost 1 1-9 terrain cost ~ cost 5 (water)
Reads a file argument or stdin. Prints the path overlay (*), cost, length, and nodes
expanded, then runs Dijkstra to confirm A* found an optimal path (catches inadmissible
heuristics and bugs in ported implementations).
python3 astar_grid.py map.txt
python3 astar_grid.py map.txt --diagonal # 8-way, octile heuristic, no corner cutting
python3 astar_grid.py map.txt --heuristic euclid --weight 1.5 # weighted A* (may be suboptimal)
printf 'S..#\\n.#.#\\n...G\\n' | python3 astar_grid.py -
Exit status: 0 = path found and optimal (or within weight bound), 1 = no path or
optimality check failed, 2 = bad input.
"""
from __future__ import annotations
import argparse
import heapq
import math
import sys
COST = {".": 1.0, "S": 1.0, "G": 1.0, "~": 5.0, **{str(d): float(d) for d in range(1, 10)}}
def parse(text: str):
rows = [r.rstrip("\n") for r in text.splitlines() if r.strip()]
if not rows:
raise ValueError("empty grid")
w = max(len(r) for r in rows)
grid = [list(r.ljust(w, "#")) for r in rows]
start = goal = None
for y, row in enumerate(grid):
for x, c in enumerate(row):
if c == "S":
start = (x, y)
elif c == "G":
goal = (x, y)
elif c != "#" and c not in COST:
raise ValueError(f"unknown tile '{c}' at ({x},{y})")
if start is None or goal is None:
raise ValueError("grid needs exactly one S and one G")
return grid, start, goal
def neighbors(grid, x, y, diagonal):
h, w = len(grid), len(grid[0])
steps = [(1, 0), (-1, 0), (0, 1), (0, -1)]
if diagonal:
steps += [(1, 1), (1, -1), (-1, 1), (-1, -1)]
for dx, dy in steps:
nx, ny = x + dx, y + dy
if not (0 <= nx < w and 0 <= ny < h) or grid[ny][nx] == "#":
continue
if dx and dy and (grid[y][nx] == "#" or grid[ny][x] == "#"):
continue # no cutting corners past walls
step = math.sqrt(2) if dx and dy else 1.0
# cost of entering a tile = tile cost x step length
yield (nx, ny), step * COST[grid[ny][nx]]
def heuristic(kind, a, b, min_cost):
dx, dy = abs(a[0] - b[0]), abs(a[1] - b[1])
if kind == "manhattan":
d = dx + dy
elif kind == "octile":
d = (dx + dy) + (math.sqrt(2) - 2) * min(dx, dy)
elif kind == "euclid":
d = math.hypot(dx, dy)
else:
d = 0.0
return d * min_cost # scale by cheapest tile so the estimate never overestimates
def search(grid, start, goal, diagonal, hkind, weight):
min_cost = min(COST[c] for row in grid for c in row if c != "#")
g = {start: 0.0}
parent = {start: None}
tie = 0
open_heap = [(0.0, 0.0, tie, start)]
closed = set()
expanded = 0
while open_heap:
_, _, _, cur = heapq.heappop(open_heap)
if cur in closed:
continue # stale entry (lazy deletion instead of decrease-key)
closed.add(cur)
expanded += 1
if cur == goal:
path = []
while cur is not None:
path.append(cur)
cur = parent[cur]
return path[::-1], g[goal], expanded
for nxt, c in neighbors(grid, *cur, diagonal):
ng = g[cur] + c
if nxt not in g or ng < g[nxt] - 1e-12:
g[nxt] = ng
parent[nxt] = cur
tie += 1
h = weight * heuristic(hkind, nxt, goal, min_cost)
# tie-break on larger g (prefer nodes deeper along the path)
heapq.heappush(open_heap, (ng + h, -ng, tie, nxt))
return None, math.inf, expanded
def main() -> int:
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
p.add_argument("map", help="grid file, or - for stdin")
p.add_argument("--diagonal", action="store_true")
p.add_argument("--heuristic", choices=["auto", "manhattan", "octile", "euclid", "zero"], default="auto")
p.add_argument("--weight", type=float, default=1.0, help="weighted A*: >1 expands fewer nodes, cost <= weight x optimal")
p.add_argument("--quiet", action="store_true", help="do not print the overlay")
a = p.parse_args()
try:
text = sys.stdin.read() if a.map == "-" else open(a.map, encoding="utf-8").read()
grid, start, goal = parse(text)
except (OSError, ValueError) as e:
print(f"error: {e}", file=sys.stderr)
return 2
hkind = a.heuristic if a.heuristic != "auto" else ("octile" if a.diagonal else "manhattan")
if hkind == "manhattan" and a.diagonal:
print("warn: manhattan overestimates with diagonal moves (inadmissible); expect suboptimal paths", file=sys.stderr)
path, cost, expanded = search(grid, start, goal, a.diagonal, hkind, a.weight)
if path is None:
print(f"NO PATH from {start} to {goal} ({expanded} nodes expanded)")
return 1
if not a.quiet:
out = [row[:] for row in grid]
for x, y in path[1:-1]:
out[y][x] = "*"
print("\n".join("".join(r) for r in out))
print(f"A* ({hkind}, weight {a.weight}, {'8' if a.diagonal else '4'}-way): cost {cost:.3f}, "
f"{len(path) - 1} steps, {expanded} nodes expanded")
_, best, dexp = search(grid, start, goal, a.diagonal, "zero", 1.0)
print(f"Dijkstra reference: cost {best:.3f}, {dexp} nodes expanded")
bound = best * max(a.weight, 1.0) + 1e-9
if cost > bound:
print(f"FAIL: A* cost {cost:.3f} exceeds allowed {bound:.3f}; heuristic is inadmissible or search is buggy")
return 1
if cost > best + 1e-9:
print(f"OK (weighted): {cost / best:.3f}x optimal, within the {a.weight}x bound")
else:
print("OK: optimal")
return 0
if __name__ == "__main__":
sys.exit(main())
About this skill
Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.