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
        };
    }
}