Procedural Generation Of Mazes With Unity

Create a procedurally generated maze from scratch with Unity! By Joseph Hocking.

Leave a rating/review
Save for later
Share

Note: This tutorial was written using Unity 2017.1.0 and is aimed at advanced users. It assumes you are already comfortable programming games in Unity. Feel free to start with our basic Unity tutorials before doing this one.

As a Unity developer, you’re probably quite skilled at constructing levels manually. But have you ever wanted to generate levels on-the-fly? Procedural generation of meshes for floors and walls, as opposed to simply laying out preconstructed models, offers a lot of flexibility and interesting replay value.

In this tutorial, you’ll learn how to:

  • Procedurally generate levels by making a maze-running game.
  • Generate maze data.
  • Use maze data to build a mesh.

Getting Started

Most algorithms you can find (such as the ones here and here) produce “perfect”, dense mazes; that is, ones with only one correct path and no loops. They’re much like the ones you would find in a newspaper’s puzzle section.

Image courtesy of Wikipedia

Perfect maze

However, most games play better with mazes that are both imperfect, with looping paths, and sparse, made from open spaces instead of tight twisty corridors. This is especially true of the rogue-like genre, where procedural levels aren’t so much “mazes” as they are dungeons.

Image courtesy of Bob Nystrom

Roguelike dungeon

In this tutorial, you’re going to implement one of the simplest maze algorithms around, described here. The reason for this choice is simply to get mazes into your game with the least amount of effort. This simple approach works well for the classic games at the above link, so you’ll use that same algorithm to create the mazes in a game called Speedy Treasure Thief.

In this game, every level is a new maze that includes a treasure chest somewhere in the level. However, you don’t have much time to find it and get out before the guards come back! Each level has a time limit, and you can keep playing until you get caught. Your score is based on how much treasure you snag.

Screenshot of finished game

To start off, create a new empty project in Unity.

Now download the starter package, unzip it and import **proc-mazes-starter.unitypackage** into your new project. The starter package includes the following:

  1. A Graphics folder, which contains all the graphics required for the game.
  2. The Scene scene, which is the initial scene for this tutorial and contains the player and the UI.
  3. A folder called Scripts, which contains two helper scripts. You’ll write the rest during this tutorial.

That’s enough to get you started. You’ll go into each of these areas in detail later.

Establishing the Code Architecture

Start by adding an empty object to the scene. Select GameObject ▸ Create Empty, name it Controller and position it at (X:0, Y:0, Z:0). This object is simply an attachment point for the scripts that control the game.

In the project’s Scripts directory, create a C# script named GameController, then create another script and name it MazeConstructor. The first script will manage the overall game, while the second will specifically handle the maze generation.

Replace everything in GameController with the following:

using System;
using UnityEngine;

[RequireComponent(typeof(MazeConstructor))]               // 1

public class GameController : MonoBehaviour
{
    private MazeConstructor generator;

    void Start()
    {
        generator = GetComponent<MazeConstructor>();      // 2
    }
}

Here’s a quick summary of what you have just created:

  1. The RequireComponent attribute ensures that a MazeConstructor component will also be added when you add this script to a GameObject.
  2. A private variable that stores a reference returned by the GetComponent().

Add this script into the scene: drag the GameController script from the Project window and drop it on Controller GameObject in the Hierarchy window.

Notice that MazeConstructor was also added to Controller; that happened automatically because of the RequireComponent attribute.

Now in MazeConstructor, replace everything with the following:

using UnityEngine;

public class MazeConstructor : MonoBehaviour
{
    //1
    public bool showDebug;
    
    [SerializeField] private Material mazeMat1;
    [SerializeField] private Material mazeMat2;
    [SerializeField] private Material startMat;
    [SerializeField] private Material treasureMat;

    //2
    public int[,] data
    {
        get; private set;
    }

    //3
    void Awake()
    {
        // default to walls surrounding a single empty cell
        data = new int[,]
        {
            {1, 1, 1},
            {1, 0, 1},
            {1, 1, 1}
        };
    }
    
    public void GenerateNewMaze(int sizeRows, int sizeCols)
    {
        // stub to fill in
    }
}

Here’s what’s going on above:

As for the data type, the maze boils down to a grid of cells. Thus the maze data is simply a two-dimensional array that’s either 0 or 1 (to represent open or blocked) for every space. It’s that simple!

  1. These fields are all available to you in the Inspector. showDebug will toggle debug displays, while the various Material references are materials for generated models. Incidentally, the SerializeField attribute displays a field in the Inspector, even though the variable is private as far as code access is concerned.
  2. Next is the data property. The access declarations (i.e. declaring the property as public but then assigning private set) makes it read-only outside this class. Thus, maze data can’t be modified from outside.
  3. The last bit of interesting code is in Awake(). This initializes data with a 3 by 3 array of ones surrounding zero. 1 means “wall” while 0 means “empty”, so this default grid is simply a walled-in room.

That’s a fair amount of groundwork for the code, but nothing is visible yet!

To display the maze data and verify how it looks, add the following method to MazeConstructor:

void OnGUI()
{
    //1
    if (!showDebug)
    {
        return;
    }

    //2
    int[,] maze = data;
    int rMax = maze.GetUpperBound(0);
    int cMax = maze.GetUpperBound(1);

    string msg = "";

    //3
    for (int i = rMax; i >= 0; i--)
    {
        for (int j = 0; j <= cMax; j++)
        {
            if (maze[i, j] == 0)
            {
                msg += "....";
            }
            else
            {
                msg += "==";
            }
        }
        msg += "\n";
    }

    //4
    GUI.Label(new Rect(20, 20, 500, 500), msg);
}

Taking each commented section in turn:

  1. This code checks if debug displays are enabled.
  2. Initialize several local variables: a local copy of the stored maze, the maximum row and column, and a string to build up.
  3. Two nested loops iterate over the rows and columns of the two-dimensional array. For each row/column of the array, the code checks the stored value and appends either "...." or "==" depending on if the value is zero. The code also appends a newline after iterating through all the columns in a row, so that each row is a new line.
  4. Finally, GUI.Label() prints out the built-up string. This project uses the newer GUI system for displays seen by the player, but the older system is simpler for creating quick debug displays.

Remember to turn on Show Debug on the MazeConstructor component. Hit Play, and the stored maze data (which is just the default maze for now) will be displayed:

Default procedural mazes

Displaying the stored data is a good start! However, the code isn't actually generating a maze yet. The next section explains how to handle that task.