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]]
 
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;
 
    [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;
        Debug.Log($"Pac-Man ready. Lives {Lives}, pellets {maze.PelletsRemaining}.");
    }
 
    private void Update()
    {
        if (player == null)
        {
            return;
        }
 
        TickPhases();
        CheckCatches();
 
        if (maze.PelletsRemaining <= 0)
        {
            Debug.Log($"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;
            }
            else
            {
                OnPlayerCaught();
                return;
            }
        }
    }
 
    public void OnPelletEaten()
    {
        Score += 10;
    }
 
    public void OnPowerPelletEaten()
    {
        Score += 50;
        frightened = true;
        frightenedTimer = frightenedTime;
        SetGhostStates(Ghost.GhostState.Frightened);
    }
 
    private void OnPlayerCaught()
    {
        Lives--;
        Debug.Log($"Caught! Lives remaining: {Lives}");
 
        if (Lives <= 0)
        {
            Debug.Log($"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);
        }
    }
}