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