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