// 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 ghosts = new List(); 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); } } }