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