Source file unity-csharp/arcade-classics/pac-man-lite/Ghost.cs from the GDnD code examples. Download raw file
// GDnD wiki example
// Demonstrates: a finite state machine (Scatter / Chase / Frightened) choosing a target, with BFS-guided movement
// Related pages: [[overview-arcade-classics-as-learning-projects]], [[ai-state-machine-pattern]], [[pathfinding-algorithms]]
using UnityEngine;
// The ghost is the route's first real AI. Its three states each pick a different
// target, and movement is the same every time: of the walkable neighbours (no
// reversing), step to the one whose BFS distance to the target is best. This is
// essentially how the original Pac-Man ghosts decide, expressed as an explicit
// [[ai-state-machine-pattern]] over [[pathfinding-algorithms]].
public class Ghost : GridMover
{
public enum GhostState { Scatter, Chase, Frightened }
public GhostState State { get; private set; } = GhostState.Scatter;
private PacPlayer player;
private Vector2Int scatterCorner;
private Vector2Int homeCell;
private bool forceReverse;
public void Setup(PacPlayer target, Vector2Int corner, Vector2Int home)
{
player = target;
scatterCorner = corner;
homeCell = home;
}
public void SetState(GhostState next)
{
// Ghosts reverse direction the moment their state changes — a readable tell.
if (next != State)
{
forceReverse = true;
}
State = next;
}
public void ReturnHome()
{
SnapTo(homeCell);
State = GhostState.Scatter;
}
protected override Vector2Int ChooseDirection(Vector2Int cell, Vector2Int dir)
{
Vector2Int reverse = -dir;
Vector2Int best = Vector2Int.zero;
float bestScore = State == GhostState.Frightened ? float.MinValue : float.MaxValue;
foreach (Vector2Int d in MazeGrid.Directions)
{
if (!maze.IsWalkable(cell + d))
{
continue;
}
// No reversing unless a state change just forced it.
if (d == reverse && !forceReverse)
{
continue;
}
float score = ScoreNeighbour(cell + d);
bool better = State == GhostState.Frightened ? score > bestScore : score < bestScore;
if (better)
{
bestScore = score;
best = d;
}
}
// Dead end: the only way out is back.
if (best == Vector2Int.zero && maze.IsWalkable(cell + reverse))
{
best = reverse;
}
forceReverse = false;
return best;
}
// Chase heads for the player, Scatter for a home corner, Frightened flees the player.
private float ScoreNeighbour(Vector2Int neighbour)
{
Vector2Int playerCell = player.CurrentCell;
return State switch
{
GhostState.Scatter => maze.Distance(neighbour, scatterCorner),
GhostState.Frightened => maze.Distance(neighbour, playerCell), // maximised
_ => maze.Distance(neighbour, playerCell), // Chase, minimised
};
}
}