Source file unity-csharp/arcade-classics/pac-man-lite/PacManGame.cs from the GDnD code examples. Download raw file

// GDnD wiki example
// Demonstrates: driving the ghost FSM phases, score/lives, power-pellet frightened mode, and win/lose
// Related pages: [[overview-arcade-classics-as-learning-projects]], [[unity-gamemanager-pattern]], [[ai-state-machine-pattern]], [[ui-design]]
 
using System.Collections.Generic;
using UnityEngine;
 
// Builds the level, spawns the player and ghosts, and runs the global rhythm that
// the ghosts obey: alternating Scatter and Chase phases, with a Frightened burst
// whenever the player eats a power pellet. Also owns score, lives, and the
// catch / level-clear outcomes.
public class PacManGame : MonoBehaviour
{
    [SerializeField] private MazeGrid maze;
    [SerializeField] private PacPlayer playerPrefab;
    [SerializeField] private Ghost ghostPrefab;
    [SerializeField] private int startingLives = 3;
    [Tooltip("Optional on-screen HUD. If empty, messages print to the Console.")]
    [SerializeField] private ArcadeHud hud;
 
    [Header("Phase timing (seconds)")]
    [SerializeField] private float scatterTime = 7f;
    [SerializeField] private float chaseTime = 20f;
    [SerializeField] private float frightenedTime = 6f;
 
    public int Score { get; private set; }
    public int Lives { get; private set; }
 
    private PacPlayer player;
    private readonly List<Ghost> ghosts = new List<Ghost>();
    private float phaseTimer;
    private float frightenedTimer;
    private bool chasePhase;
    private bool frightened;
 
    private void Start()
    {
        NewGame();
    }
 
    private void NewGame()
    {
        maze.Build();
 
        if (player == null)
        {
            player = Instantiate(playerPrefab);
            player.Bind(this);
        }
 
        player.Init(maze, maze.PlayerStart);
 
        while (ghosts.Count < maze.GhostStarts.Count)
        {
            ghosts.Add(Instantiate(ghostPrefab));
        }
 
        Vector2Int[] corners =
        {
            new Vector2Int(1, maze.Height - 2),
            new Vector2Int(maze.Width - 2, 1),
            new Vector2Int(maze.Width - 2, maze.Height - 2),
            new Vector2Int(1, 1),
        };
 
        for (int i = 0; i < maze.GhostStarts.Count; i++)
        {
            Ghost ghost = ghosts[i];
            ghost.Init(maze, maze.GhostStarts[i]);
            ghost.Setup(player, corners[i % corners.Length], maze.GhostStarts[i]);
            ghost.SetState(Ghost.GhostState.Scatter);
        }
 
        Score = 0;
        Lives = startingLives;
        phaseTimer = scatterTime;
        chasePhase = false;
        frightened = false;
        RefreshHud();
    }
 
    private void Update()
    {
        if (player == null)
        {
            return;
        }
 
        TickPhases();
        CheckCatches();
 
        if (maze.PelletsRemaining <= 0)
        {
            ShowMessage($"Level cleared! Score {Score}");
            NewGame();
        }
    }
 
    private void TickPhases()
    {
        if (frightened)
        {
            frightenedTimer -= Time.deltaTime;
            if (frightenedTimer <= 0f)
            {
                frightened = false;
                SetGhostStates(chasePhase ? Ghost.GhostState.Chase : Ghost.GhostState.Scatter);
            }
 
            return; // hold the scatter/chase clock while ghosts are frightened
        }
 
        phaseTimer -= Time.deltaTime;
        if (phaseTimer <= 0f)
        {
            chasePhase = !chasePhase;
            phaseTimer = chasePhase ? chaseTime : scatterTime;
            SetGhostStates(chasePhase ? Ghost.GhostState.Chase : Ghost.GhostState.Scatter);
        }
    }
 
    private void CheckCatches()
    {
        float catchDistance = maze.CellSize * 0.5f;
        foreach (Ghost ghost in ghosts)
        {
            if (Vector3.Distance(ghost.transform.position, player.transform.position) >= catchDistance)
            {
                continue;
            }
 
            if (ghost.State == Ghost.GhostState.Frightened)
            {
                ghost.ReturnHome();
                Score += 200;
                RefreshHud();
            }
            else
            {
                OnPlayerCaught();
                return;
            }
        }
    }
 
    public void OnPelletEaten()
    {
        Score += 10;
        RefreshHud();
    }
 
    public void OnPowerPelletEaten()
    {
        Score += 50;
        RefreshHud();
        frightened = true;
        frightenedTimer = frightenedTime;
        SetGhostStates(Ghost.GhostState.Frightened);
    }
 
    private void OnPlayerCaught()
    {
        Lives--;
        RefreshHud();
        ShowMessage($"Caught! Lives remaining: {Lives}");
 
        if (Lives <= 0)
        {
            ShowMessage($"Game over. Final score: {Score}");
            NewGame();
            return;
        }
 
        player.SnapTo(maze.PlayerStart);
        foreach (Ghost ghost in ghosts)
        {
            ghost.ReturnHome();
        }
    }
 
    private void SetGhostStates(Ghost.GhostState state)
    {
        foreach (Ghost ghost in ghosts)
        {
            ghost.SetState(state);
        }
    }
 
    // HUD helpers. The game owns the numbers; ArcadeHud only displays them.
    // With no HUD assigned, messages fall back to the Console instead of throwing.
    private void RefreshHud()
    {
        if (hud != null)
        {
            hud.SetStatus($"Score {Score}   Lives {Lives}");
        }
    }
 
    private void ShowMessage(string message)
    {
        if (hud != null)
        {
            hud.ShowMessage(message);
        }
        else
        {
            Debug.Log(message);
        }
    }
}