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