Godot pathfinding / 2026-07-16 / 16 min read
Verified as of Godot 4.7.1 stable
Pac-Man-Style Ghost AI in Godot: Build the Full Maze Chase
Build a Pac-Man-style maze chase in Godot with TileMapLayer, buffered grid movement, shared Dijkstra fields, ghost modes, tunnels, and reservations.

Interactive tutorial scene
Play the maze-chase starter in your browser
This is the exact small Godot scene taught below: one authored maze, buffered player turns, four target policies, chase/scatter/frightened modes, wrap tunnels, and next-cell reservations. Launch it before reading, then press F to expose the shared field.
Loads only after launch. Requires WebAssembly and WebGL 2.
HTML transcript
- The authored scene starts with one 21×17 maze, a player marker, four pursuers, pellets, four power pellets, two lives, and chase mode active.
- Arrow keys or WASD buffer a cardinal turn and apply it at the next legal committed cell.
- The four pursuers share movement code while hunter, ambusher, weaver, and drifter policies choose different target fields.
- Press F to toggle the player-centered field overlay and press R to restart the tutorial scene.
Fallback coverage
- The poster is a real Chromium capture of the exact tutorial Web export.
- The source ZIP contains the authored Godot 4.7.1 scene and all tutorial scripts.
- The visible article includes the same movement, field, policy, reservation, tunnel, collectible, and contact code.
- The source and Web receipts bind the tutorial package and browser export separately.
The direct answer
Build a Pac-Man-style maze chase in Godot around committed grid cells. Keep the maze topology in a TileMapLayer-backed board, store each actor's current cell and target cell, and interpolate only the visible Node2D position. Buffer the player's requested direction so the turn can be accepted before the next intersection.
For several pursuers chasing the same player, rebuild one player-centered Dijkstra field when the player commits to a new cell. Every direct pursuer can read that same field and choose one legal downhill neighbor. A different personality changes the target: a cell ahead of the player, a scatter corner, a target reflected around another pursuer, or a distance-dependent switch.
During ordinary movement, reject immediate reversal unless the pursuer reaches a dead end. Before the next pursuer chooses, reserve the target cell selected by the previous one. Treat a wrap tunnel as a real graph edge in both movement and field expansion.
A ghost needs a target policy, not a private pathfinder.
Build the tutorial in this order
The Web player above is the assembled tutorial result. Work through these layers in order so each step leaves the scene runnable before the next system is added.
If your painted TileMap import is the part you need to replace, use TileMap to Navigation Grid in Godot. For deeper field math, continue with Dijkstra Maps for Roguelike AI. This tutorial stays on the complete small game loop.
| Step | Add | Working result |
|---|---|---|
| 1 | Author the maze board | Walkable cells, collectibles, spawn markers, cell centers, and wrap rows are inspectable in the editor. |
| 2 | Add one grid actor | The committed cell stays authoritative while the visible position interpolates. |
| 3 | Buffer player input | A requested turn is remembered and applied at the next legal intersection. |
| 4 | Build the player-centered field | Every reachable maze cell has a downhill answer toward the player. |
| 5 | Add four target policies | Pursuers share movement code but produce direct, ambush, weave, and distance-switch behavior. |
| 6 | Add chase, scatter, and frightened modes | Target fields, reversal rules, and contact outcomes change together. |
| 7 | Add reservations, tunnels, pellets, and reset | The small maze chase is playable as one complete loop. |
Use five small layers
The downloadable scene keeps these responsibilities in four scripts: maze_board.gd, grid_actor.gd, maze_rules.gd, and maze_chase_demo.gd.
Every code block below is an exact fragment from those packaged files. The complete project and verifier are in the download.
| Layer | Owns | Does not own |
|---|---|---|
| Maze board | Walkable cells, wrap rows, collectibles, spawn markers, cell centers | Pursuer personality |
| Grid actor | Current cell, target cell, direction, interpolation | Choosing a destination |
| Field solver | Cost from every reachable cell to a target | Chase/scatter timers |
| Pursuer policy | Which target or field one pursuer reads | Rendering or contact outcome |
| Game state | Modes, score, lives, contact, reset, field invalidation | Low-level neighbor relaxation |
Buffer the player's next turn
Input actions update _buffered_direction. At each committed cell, the mover tries that request first and continues straight only when the requested turn is still blocked:
The exact keyboard, D-pad, swipe, or touch surface is separate. All of them should set the same buffered cardinal request.
Do I need AStarGrid2D for every pursuer?
AStarGrid2D is appropriate when one actor needs one point-to-point route. Several pursuers chasing the same player ask a different-shaped question: from every reachable maze cell, which neighbor leads toward this one target?
The AStarGrid2D complete reference covers the point-to-point API. This tutorial uses a shared field because it needs a next-step answer everywhere on a small maze.
| Situation | Use |
|---|---|
| One actor needs one complete route from A to B | AStarGrid2D.get_id_path() |
| Several actors share the player as target | One player-centered Dijkstra field |
| A policy targets ahead of the player | One cached field seeded at the projected target |
| Each pursuer scatters toward a different corner | Cache one field per active corner target |
| Frightened pursuers move away from the player | One rescanned flee field shared by the group |
Build one field from the target
The solver seeds the target at zero and expands outward through cardinal neighbors. A middle row can wrap because the same topology helper is used during expansion:
The full Dijkstra-map guide owns weighted fields, source bias, composed maps, flee-map theory, and INF-safe scoring. The starter keeps uniform cardinal cost so the integration remains visible.
Rebuild on committed player cells
The player field changes when the player enters a new cell. Policy fields depending on that player state are invalidated at the same boundary:
Do not rebuild because one pursuer advanced one cell. Reading the field does not invalidate it. The flee field is also absent during ordinary play; it is built only while frightened mode needs it.
Let each pursuer choose one legal neighbor
At a cell boundary, a pursuer reads its field and checks at most four neighbors. Ordinary movement rejects the immediate reverse direction. If that leaves no legal choice, the function retries once with reversal allowed:
CARDINALS has a stable order, so equal field values do not depend on Dictionary iteration or scene-tree order.
Personality comes from the target policy
The four pursuers share the same movement code. Their policy changes the field they read:
_field_for_target() normalizes a requested target to a legal cell and caches the result for the current player revision. A different personality does not require another pathfinding class.
| Role | Target policy | Result |
|---|---|---|
| Hunter | Player's committed cell | Direct pressure using the shared player field |
| Ambusher | Legal cell four steps ahead of the player | Cuts toward the player's current heading |
| Weaver | Projected player target reflected around the hunter | Approaches from another direction |
| Drifter | Player field at range; scatter corner when close | Alternates pursuit and retreat |
Make the game modes explicit
The starter alternates twelve seconds of chase with four seconds of scatter. A power pellet interrupts that schedule for six seconds of frightened mode:
A finished game may animate a captured pursuer back to a home cell through a separate returning state. This small starter teleports it to spawn so the tutorial stays on navigation and mode boundaries.
| Mode | Field | Reversal | Contact |
|---|---|---|---|
| Chase | Player or policy target | Normally rejected | Player loses a life |
| Scatter | Per-pursuer corner target | Normally rejected | Player loses a life |
| Frightened | Shared rescanned flee field | Dead-end fallback still applies | Pursuer returns to spawn and awards points |
Reserve the chosen target before the next pursuer moves
Current occupancy is not enough. Two pursuers can start in different cells and choose the same empty neighbor during the same frame.
The update builds one occupied set, processes pursuers in a stable order, and adds every accepted target before the next choice:
The claims are ephemeral. The set is rebuilt on the next update because this is real-time next-cell selection, not turn ownership. The reservation-table guide owns persistent claims, simultaneous turns, confirmation, and undo.
Treat the side tunnel as an edge
The middle row is open at both ends. neighbor() maps a horizontal step beyond one edge to the cell on the other edge:
If movement wraps but the field does not, the tunnel looks open and the AI treats it as a wall.
- Use the same topology for player legality.
- Use it for pursuer legality.
- Use it for Dijkstra expansion.
- Use it for direction reconstruction.
- Use it for interpolation across the screen edge.
Collect pellets only after the move commits
Pellet state changes at the same cell boundary that updates navigation:
Interpolation frames never collect the same pellet repeatedly. A power pellet changes score and mode in one transaction.
Resolve contact from the current mode
The starter checks same-cell contact and actors crossing in opposite directions. The active mode decides the outcome:
The outcome does not depend on which sprite callback happened first.
What to update, and when
This table is the maintenance contract. Field rebuilds should follow fact changes, not be added to arbitrary render callbacks.
| Event | Rebuild fields? | Reservation effect | State effect |
|---|---|---|---|
| Player commits a cell | Rebuild player field; clear player-dependent target cache | Rebuilt during the next pursuer update | None |
| Pursuer commits a cell | Usually no | Its committed/target cell enters the current occupied set | None |
| Power pellet is collected | Build the flee field | Next choices read frightened occupancy | Enter frightened |
| Frightened timer expires | Discard flee field; ordinary targets rebuild on demand | Next choices use ordinary occupancy | Resume chase |
| Chase/scatter timer changes | Cache new target fields on demand | Next choices use the new policy | Chase or scatter |
| Maze topology changes | Reparse topology and rebuild all fields | Clear current claims | None |
| Player is caught | Rebuild from reset cells | Clear transient claims | Lose life or end run |
Run the verified starter
The source ZIP contains the exact scene and code shown in this article. Download the Godot 4.7.1 source project, then read the machine-readable verification receipt.
The receipt reports 16/16 checks after the exact ZIP was extracted and rerun. It covers the authored maze, buffered turns, committed cells, shared fields, target policies, flee rescan, no-reverse behavior, dead-end fallback, reservations, tunnel wrapping, collectibles, contact mode, scene startup, local resource dependencies, and the package rerun.
Measurement-gap: this is a correctness receipt, not a performance dataset. It does not measure FPS, throughput, allocations, memory, or commercial-game readiness.
For the larger browser-playable result, open Dijkstra's Ghost. It has four hunters, ten mazes, weighted routes, touch controls, a live field overlay, and a separate public receipt. The starter is not a copy of that product's source.
Common failure modes
Most broken maze chases cross one of the boundaries above. Match the visible symptom to the state contract before replacing the solver.
The player reaches an intersection before the turn is accepted
Input is being tested only on the exact intersection frame. Keep the most recent cardinal request and retry it at every committed cell.
The visible actor is in one cell and the rules use another
World position has replaced grid state. Store cell and target_cell separately; update the authoritative cell only when interpolation completes.
All pursuers stack or form one merged sprite
Shared navigation does not imply shared occupancy. Include moving target cells in the occupied set before the next pursuer chooses.
A pursuer shakes between two corridor cells
The selection rule allows immediate reversal. Exclude the opposite direction during ordinary movement and retry with reversal only if no other move exists.
The ambusher aims into a wall
Normalize the projected target before seeding a field. The starter uses nearest_walkable() with the player cell as a safe fallback.
The tunnel works for the player but not the pursuers
Movement and field expansion have different topology functions. Route both through the same wrap-aware neighbor rule.
The game pauses briefly on every player cell
The field builder is probably recomputing topology inside its inner loop. Cache dimensions and wrap rows once per board revision, use packed queues, and build the flee field only when frightened mode is active.
Where PathForge goes further
This starter has one fixed one-cell movement profile, static topology, uniform cost, and a small number of target fields.
PathForge is being built for the larger capability layer: multi-size clearance, dynamic blockers, tactical movement profiles, scheduled queries, and editor-visible diagnostics. The standalone project above proves the native small-game baseline; PathForge addresses the broader grid-navigation workflow.
Frequently asked questions
How do I build Pac-Man-style ghost AI in Godot?
Keep the maze and actors on cardinal grid cells, rebuild one player-centered Dijkstra field when the player commits a move, and let direct pursuers choose a legal neighboring cell with a lower field value. Add personality by changing the target seed, not by giving every pursuer a separate random steering rule.
Should every ghost use its own AStarGrid2D path?
No when several ghosts share the player as their target. One reverse Dijkstra field gives every reachable cell a next-step answer. Use AStarGrid2D when one actor needs one complete point-to-point path, and cache another field only when a pursuer policy has a genuinely different target.
How do I make the player turn smoothly at intersections?
Buffer the latest requested cardinal direction. When a cell move commits, try the buffered direction first; if it is still blocked, continue straight when possible. Interpolate the Node2D, but keep the committed cell authoritative.
How do I stop ghosts from reversing constantly?
Exclude the direction opposite current_direction during chase and scatter. If no other legal neighbor remains, repeat the selection once with reversal allowed so the actor can leave a dead end.
How do I stop two ghosts moving into the same tile?
Build an occupied set containing every pursuer's current or active target cell. After a pursuer selects a new target, add it immediately before the next pursuer chooses.
How do wrap tunnels work with pathfinding?
Represent the two edge cells as neighbors in the same helper used by movement and Dijkstra expansion. Do not implement wrapping only as a visual teleport.
Does this recreate the original PAC-MAN ghost algorithms?
No. Pac-Man-style describes the maze-chase genre. The starter uses original code-drawn actors, an original maze, and its own hunter, ambusher, weaver, and drifter policies.