Maze Generator – Expandable Procedural Tile system for Unity

 

Description

This is my attempt at a maze generator in unity, it should be viable for any tile-based game. It takes tiles (corridors or rooms) of a fixed size and builds a customisable path from start room to end room, and then adds similar paths at random points along the chain.

The paths can be winding and diverse but will never wrap so long as there are no L-shaped tiles. The operation runs in a coroutine so it can make for quite a dramatic live-build effect as seen in the video. The script is easily expandable and allows for more than just simple floor tiles. For example, the original project included walls and doors but theoretically any numbers of additional object could be built into the enemy (enemy spawn points, upgrades, coins, etc).

Full script below (Creative Commons 4.0)

Video

 

Code

This is the main script. Unlike in the video tutorial, the Room/Corridor scripts are not necessary, only that the tiles have the Tile class attached (shown at the bottom of the page). Even that could be fixed relatively easily by simply replacing any Tile reference with GameObject.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class LevelBuilderTut : MonoBehaviour
{
    //Balance Vars
    [SerializeField] int mainPathLength = 5;
    [SerializeField] int sidePathLength = 3;
    [SerializeField] int sidePathAmount = 4;
    [SerializeField] [Range(0f, 1f)] float corridorChance = 0.8f;
    [SerializeField] [Range(0f, 1f)] float stackingCorridorChance = 0.1f;

    //Working Bools
    bool makingLevel;
    bool makingChain;

    //Room Types
    [SerializeField] Tile startRoomType;
    [SerializeField] Tile endRoomType;
    [SerializeField] Tile[] corridorTypes;
    [SerializeField] Tile[] roomTypes;

    //Build data
    [SerializeField] List<Vector2> tileCoords = new List<Vector2>();
    List<Tile> placedTiles = new List<Tile>();
    List<Transform> availableExits = new List<Transform>();
    
    private void Start()
    {
        NewLevel();
    }

    void EmptyLevel()
    {
        StopAllCoroutines();
        foreach (Transform t in transform)
        {
            Destroy(t.gameObject);
        }
        tileCoords = new List<Vector2>();
        ClearVars();
    }

    void ClearVars()
    {
        availableExits.RemoveAll(i => i == null);
        placedTiles.RemoveAll(i => i == null);
    }

    public void NewLevel()
    {
        EmptyLevel();
        makingLevel = makingChain = false;
        if (!makingLevel)
        {
            StartCoroutine(CrouteMakeLevel());
        }
    }

    IEnumerator CrouteMakeLevel()
    {
        makingLevel = true;

        //Make first room
        Tile startTile = Instantiate(startRoomType, Vector3.zero, Quaternion.identity, transform);
        AddTile(startTile);

        //Make first chain and wait
        StartCoroutine(CrouteMakeChain(mainPathLength, startTile));
        while (makingChain) { yield return null; }

        //Make Last Room by replacing the last tile in chain with end room
        Tile lastTileMade = placedTiles[placedTiles.Count - 1];
        Tile endTile = Instantiate(endRoomType, lastTileMade.transform.position, lastTileMade.transform.rotation, transform);
        Destroy(lastTileMade.gameObject);
        //Remove destroyed tile from list (update arrays by returning null) then add new end tile to lists
        yield return null;
        tileCoords.RemoveAt(tileCoords.Count - 1);
        ClearVars();
        AddTile(endTile);

        //Make side Paths
        int sidePaths = sidePathAmount;
        while(sidePaths > 0)
        {
            //Use same function but start at a random tile
            StartCoroutine(CrouteMakeChain(sidePathLength, placedTiles[Random.Range(0, placedTiles.Count - 1)]));
            while (makingChain) { yield return null; }
            sidePaths--;
        }

        makingLevel = false;
    }

    /*
    int breakCount = 0;
    bool WhileBreak(string _error)
    {
        breakCount++;
        if(breakCount > 100)
        {
            Debug.LogError(_error);
            return true;
        }
        return false;
    }
    */

    IEnumerator CrouteMakeChain(int _roomCount, Tile _startTile)
    {
        makingChain = true;
        while (makingChain)
        {
            //if(WhileBreak("While makingChain")) { yield break; }
            float probMult = 0f; //steadilly incread chance to make room not corridor
            while (_roomCount > 0)
            {
                Transform joiningPoint = null;
                bool goodEntry = false;
                //checks if there is a possible exit to the current tile
                while (!goodEntry)
                {
                    //if (WhileBreak("While !GoodEntry")) { yield break; }
                    List<Transform> availableExits = GetAvailableExits(_startTile);
                    if(availableExits.Count > 0)
                    {
                        goodEntry = true;
                        joiningPoint = availableExits[Random.Range(0, availableExits.Count)];
                    }
                    //if not, go back a tile
                    else
                    {
                        int newTileIndex = placedTiles.IndexOf(_startTile) - 1;
                        if (newTileIndex >= 0)
                        {
                            _startTile = placedTiles[newTileIndex];
                        }
                        else //or loop to last tile if at start
                        {
                            _startTile = placedTiles[placedTiles.Count - 1];
                        }
                    }
                }

                Tile newTileType;
                bool isRoom = true;
                if(Random.Range(0f,1f) < corridorChance - probMult)
                {
                    //Make Corridor
                    newTileType = corridorTypes[Random.Range(0,corridorTypes.Length)];
                    isRoom = false;
                }
                else
                {
                    //Make Room
                    newTileType = roomTypes[Random.Range(0, roomTypes.Length)];
                }

                Vector3 tilePos = Round(joiningPoint.position + joiningPoint.forward * 3);
                //Check if space is available
                if(!tileCoords.Contains(tilePos))
                {
                    //Build Tile
                    Tile newTile = Instantiate(newTileType, new Vector3(tilePos.x, 0, tilePos.y), joiningPoint.rotation, transform);

                    _startTile = newTile; //Set the next starting tile to this new one
                    AddTile(newTile);

                    //If room, lower remaining room chance and reset corridor chance
                    if (isRoom)
                    {
                        _roomCount--;
                        probMult = 0;
                    }
                    //Otherwise increase corridor chance
                    else
                    {
                        probMult += stackingCorridorChance;
                    }
                }
                //once exit has tile or was found to be blocked, remove exit from available exits
                availableExits.Remove(joiningPoint);
                yield return null;
            }
            makingChain = false;
        }       

    }


        
    void AddTile(Tile _tile)
    {
        //Add exits to available
        foreach (Transform t in _tile.transform.Find("AnchorPoints/Exits"))
        {
            availableExits.Add(t);
        }
        tileCoords.Add(Round(_tile.transform.position)); //Add its position to the occupied spaces
        placedTiles.Add(_tile); //Add tile to list of placed tiles
    }

    Vector3 Round(Vector3 v)
    {
        return new Vector3(
            Mathf.Round(v.x),
            Mathf.Round(v.z)
            );
    }

    List<Transform> GetAvailableExits(Tile _newTile)
    {
        List<Transform> ret = new List<Transform>();
        //iterate through all anchors in tile of the specified type (Exit) and add them to list
        foreach (Transform t in _newTile.transform.Find("AnchorPoints/Exits"))
        {
            if (availableExits.Contains(t))
            {
                ret.Add(t);
            }
        }
        return ret;
    }
}

This is the tile script. The code itself is not actually necessary but may help with placing the exits correctly.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Tile : MonoBehaviour
{
    void OnDrawGizmosSelected()
    {
        if (transform.Find("AnchorPoints/Entries").childCount > 0)
        {
            Gizmos.color = Color.green;
            foreach (Transform t in transform.Find("AnchorPoints/Entries"))
            {
                Gizmos.DrawLine(t.position, t.position + t.forward + t.up*.6f);
            }
        }

        if (transform.Find("AnchorPoints/Exits").childCount > 0)
        {
            Gizmos.color = Color.blue;
            foreach (Transform t in transform.Find("AnchorPoints/Exits"))
            {
                Gizmos.DrawLine(t.position, t.position + t.forward + t.up);
            }
        }

        if (transform.Find("AnchorPoints/Walls").childCount > 0)
        {
            Gizmos.color = Color.red;
            foreach (Transform t in transform.Find("AnchorPoints/Walls"))
            {
                Gizmos.DrawCube(t.position, new Vector3(.4f, .4f, .4f));
                Gizmos.DrawLine(t.position, t.position + t.forward + t.up);
            }
        }
    }
}

I also made an editor script that adds a button to remake the maze. That way you don’t have to reload the entire instance.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;

[CustomEditor(typeof(LevelBuilder))]
public class LevelBuilderEditor : Editor
{
    public override void OnInspectorGUI()
    {
        if (EditorApplication.isPlaying)
        {
            LevelBuilder lvBuild = (LevelBuilder)target;
            if (GUILayout.Button("Build New Level"))
            {
                lvBuild.NewLevel();
            }
        }

        DrawDefaultInspector();
    }
}

Maze Generator Cover Image

I hope this helps anyone trying to make a maze game or any sort of room-based game.

 

If you liked this rambling, shoddy tutorial then be sure to check out my Dialogue System Tutorial!

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.