Source file unity-csharp/arcade-classics/pac-man-lite/MazeGrid.cs from the GDnD code examples. Download raw file
// GDnD wiki example
// Demonstrates: a tile maze with pellets, cell<->world conversion, and BFS distance for ghost pathfinding
// Related pages: [[overview-arcade-classics-as-learning-projects]], [[unity-tilemap]], [[pathfinding-algorithms]], [[csharp-collections]]
using System.Collections.Generic;
using UnityEngine;
// The maze is the shared world for the player and the ghosts. It owns the wall
// layout, the pellets, the conversion between grid cells and world positions, and
// a BFS distance query the ghosts use to find their way. The layout is generated
// as a "pillar" maze (border + isolated single-cell pillars) so it is always
// fully connected — see the README for swapping in a hand-authored tilemap.
public class MazeGrid : MonoBehaviour
{
public enum Pellet { None, Normal, Power }
[SerializeField] private int width = 15; // use odd numbers
[SerializeField] private int height = 11; // use odd numbers
[SerializeField] private float cellSize = 0.6f;
[Header("Tile prefabs")]
[SerializeField] private Transform wallPrefab;
[SerializeField] private Transform pelletPrefab;
[SerializeField] private Transform powerPelletPrefab;
public static readonly Vector2Int[] Directions =
{ Vector2Int.up, Vector2Int.down, Vector2Int.left, Vector2Int.right };
public float CellSize => cellSize;
public int Width => width;
public int Height => height;
public Vector2Int PlayerStart { get; private set; }
public List<Vector2Int> GhostStarts { get; } = new List<Vector2Int>();
public int PelletsRemaining { get; private set; }
private bool[,] walls;
private Pellet[,] pellets;
private readonly Dictionary<Vector2Int, Transform> pelletVisuals = new Dictionary<Vector2Int, Transform>();
public void Build()
{
// Clear any visuals from a previous level.
for (int i = transform.childCount - 1; i >= 0; i--)
{
Destroy(transform.GetChild(i).gameObject);
}
walls = new bool[width, height];
pellets = new Pellet[width, height];
pelletVisuals.Clear();
GhostStarts.Clear();
PelletsRemaining = 0;
for (int x = 0; x < width; x++)
{
for (int y = 0; y < height; y++)
{
bool border = x == 0 || y == 0 || x == width - 1 || y == height - 1;
bool pillar = x % 2 == 0 && y % 2 == 0; // isolated → never disconnects the maze
walls[x, y] = border || pillar;
}
}
PlayerStart = new Vector2Int(1, 1);
GhostStarts.Add(NearestOpen(new Vector2Int(width / 2, height / 2)));
GhostStarts.Add(NearestOpen(new Vector2Int(width / 2, height / 2 - 1)));
var powerCells = new HashSet<Vector2Int>
{
new Vector2Int(1, height - 2),
new Vector2Int(width - 2, 1),
new Vector2Int(width - 2, height - 2),
};
for (int x = 0; x < width; x++)
{
for (int y = 0; y < height; y++)
{
if (walls[x, y])
{
continue;
}
Vector2Int cell = new Vector2Int(x, y);
if (cell == PlayerStart || GhostStarts.Contains(cell))
{
continue;
}
pellets[x, y] = powerCells.Contains(cell) ? Pellet.Power : Pellet.Normal;
PelletsRemaining++;
}
}
BuildVisuals();
}
public bool IsWalkable(Vector2Int cell)
{
return InBounds(cell) && !walls[cell.x, cell.y];
}
public Vector3 CellToWorld(Vector2Int cell)
{
float originX = -((width - 1) * cellSize) / 2f;
float originY = -((height - 1) * cellSize) / 2f;
return transform.position + new Vector3(cell.x * cellSize + originX, cell.y * cellSize + originY, 0f);
}
public Pellet EatPellet(Vector2Int cell)
{
if (!InBounds(cell))
{
return Pellet.None;
}
Pellet pellet = pellets[cell.x, cell.y];
if (pellet != Pellet.None)
{
pellets[cell.x, cell.y] = Pellet.None;
PelletsRemaining--;
if (pelletVisuals.TryGetValue(cell, out Transform visual))
{
if (visual != null)
{
Destroy(visual.gameObject);
}
pelletVisuals.Remove(cell);
}
}
return pellet;
}
// Breadth-first shortest-path length between two walkable cells. This is the
// "simple pathfinding" the ghosts rely on; for larger maps see A* on
// [[pathfinding-algorithms]].
public int Distance(Vector2Int from, Vector2Int to)
{
if (from == to)
{
return 0;
}
if (!IsWalkable(from) || !IsWalkable(to))
{
return int.MaxValue;
}
var visited = new HashSet<Vector2Int> { from };
var queue = new Queue<(Vector2Int cell, int dist)>();
queue.Enqueue((from, 0));
while (queue.Count > 0)
{
(Vector2Int cell, int dist) = queue.Dequeue();
foreach (Vector2Int dir in Directions)
{
Vector2Int next = cell + dir;
if (!IsWalkable(next) || visited.Contains(next))
{
continue;
}
if (next == to)
{
return dist + 1;
}
visited.Add(next);
queue.Enqueue((next, dist + 1));
}
}
return int.MaxValue;
}
private bool InBounds(Vector2Int cell)
{
return cell.x >= 0 && cell.y >= 0 && cell.x < width && cell.y < height;
}
private Vector2Int NearestOpen(Vector2Int cell)
{
if (IsWalkable(cell))
{
return cell;
}
foreach (Vector2Int dir in Directions)
{
if (IsWalkable(cell + dir))
{
return cell + dir;
}
}
return PlayerStart;
}
private void BuildVisuals()
{
for (int x = 0; x < width; x++)
{
for (int y = 0; y < height; y++)
{
Vector2Int cell = new Vector2Int(x, y);
if (walls[x, y])
{
if (wallPrefab != null)
{
Place(wallPrefab, cell, cellSize);
}
continue;
}
if (pellets[x, y] == Pellet.Normal && pelletPrefab != null)
{
pelletVisuals[cell] = Place(pelletPrefab, cell, cellSize * 0.2f);
}
else if (pellets[x, y] == Pellet.Power && powerPelletPrefab != null)
{
pelletVisuals[cell] = Place(powerPelletPrefab, cell, cellSize * 0.45f);
}
}
}
}
private Transform Place(Transform prefab, Vector2Int cell, float scale)
{
Transform tile = Instantiate(prefab, CellToWorld(cell), Quaternion.identity, transform);
tile.localScale = Vector3.one * scale;
return tile;
}
}