想要在Unity快速產生迷宮,提升製作關卡的效率,可以自己寫一個簡單的迷宮產生器腳本,產生迷宮物件後,再自行進行修改。
[Step1:新增Script腳本]
新增一個空的物件,命名為"MazeGenerator",並新增Script"MazeGenerator":

[Step2:編輯Script腳本]
編輯MazeGenerator類別,將ChatGPT建議的程式碼覆蓋上去。主要在函數前面標註[ContextMenu("Generate Maze")],便能夠在Unity編輯執行該函數。
using UnityEngine;
public class MazeGenerator : MonoBehaviour
{
public int width = 10; // 迷宮寬度(以格為單位)
public int height = 10; // 迷宮高度(以格為單位)
public float cellSize = 2f; // 每格大小 (2x2)
public GameObject wallPrefab;
private bool[,] maze; // true = 空地, false = 牆
// 清除舊的物件,並且做程式初始化
private void ClearObject()
{
// 先清空舊的
for (int i = transform.childCount - 1; i >= 0; i--)
{
DestroyImmediate(transform.GetChild(i).gameObject);
}
// 建立迷宮資料(這裡用簡單演算法,可以換成 DFS/Prim/Kruskal)
maze = new bool[width, height];
// 簡單做法:全部先設為牆
for (int x = 0; x < width; x++)
{
for (int y = 0; y < height; y++)
{
maze[x, y] = false;
}
}
}
// 產生新的物件
private void GenerateObject()
{
// 產生 GameObject
int mid_width = width >> 1;
int mid_height = height >> 1;
for (int x = 0; x < width; x++)
{
for (int y = 0; y < height; y++)
{
Vector3 pos = new Vector3((x - mid_width) * cellSize + 1, 0, (y - mid_height) * cellSize + 1);
if (!maze[x, y])
{
// 牆
if (wallPrefab != null)
Instantiate(wallPrefab, pos, Quaternion.identity, transform);
}
}
}
}
[ContextMenu("Generate Maze")]
public void GenerateMaze()
{
// 清除舊的物件,並且做程式初始化
ClearObject();
// 用 DFS carve 出路徑
Carve(Random.Range(0, 2), Random.Range(0, 2));
// 產生新的物件
GenerateObject();
}
// 用 DFS carve 出路徑
void Carve(int x, int y)
{
maze[x, y] = true; // 打通
// 隨機方向
var dirs = new Vector2Int[]
{
Vector2Int.up, Vector2Int.down,
Vector2Int.left, Vector2Int.right
};
dirs = Shuffle(dirs);
foreach (var dir in dirs)
{
int nx = x + dir.x * 2;
int ny = y + dir.y * 2;
if (nx >= 0 && ny >= 0 && nx <= width - 1 && ny <= height - 1 && !maze[nx, ny])
{
maze[x + dir.x, y + dir.y] = true; // 中間打通
Carve(nx, ny);
}
}
}
Vector2Int[] Shuffle(Vector2Int[] arr)
{
for (int i = 0; i < arr.Length; i++)
{
int rand = Random.Range(i, arr.Length);
var tmp = arr[i];
arr[i] = arr[rand];
arr[rand] = tmp;
}
return arr;
}
}
這段程式的大意是從起點開始,利用DFS隨機走四個方向其中一個,每次走兩格然後中間打通,直到無路可走。地圖maze一開始都設成牆壁,走到某格再設成空格,兩格之間的牆壁也設成空格。
[Step3:測試程式]
a.設定好參數後,點選MazeGenerator腳本的右上"…",選擇執行GenerateMaze函數:


b. 測試執行效果,每次點選GenerateMaze函數,都會重新產生迷宮物件,這些物件在Hierarchy視窗會顯示出來,所以可以進行編輯的動作。


[Step4:修改Script腳本]
可以看到該程式產生的迷宮比較單調,而且有兩邊都是牆壁,所以後來我自己又改了一下程式。
public int MAX_EXCEPTION_COUNT = 3; // 最多例外的數量
private int exceptionCount = 0; // 目前例外的數量
[ContextMenu("Generate Maze 3")]
public void GenerateMaze3()
{
ClearObject();
// 用 DFS carve 出路徑
Carve3(Random.Range(0, 2), Random.Range(0, 2));
GenerateObject();
}
/// 回傳該點是否為合法位置
private bool isValidPosition(int x, int y)
{
return x >= 0 && x < width && y >= 0 && y < height;
}
/// 該點是否為空格? true是空格,false是牆壁
/// <returns>true是空格,false表示牆壁或超出邊界</returns>
private bool isEmptyGrid(int x, int y)
{
return isValidPosition(x, y) && maze[x, y];
}
/// 該點是否為牆壁? true是空格,false是牆壁
/// <returns>true是牆壁,false表示空格或超出邊界</returns>
private bool isWallGrid(int x, int y)
{
return isValidPosition(x, y) && !maze[x, y];
}
/// <summary>
/// 取得鄰居有幾個牆壁的點
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <returns></returns>
private int GetNeighborWallCount(int x, int y)
{
int count = 0;
if (isWallGrid(x, y - 1)) ++count;
if (isWallGrid(x - 1, y)) ++count;
if (isWallGrid(x, y + 1)) ++count;
if (isWallGrid(x + 1, y)) ++count;
return count;
}
/// <summary>
/// 這邊不允許出現2*2空格
/// 牆:false 空格:true
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <returns></returns>
bool IsValid(int x, int y)
{
// 左上角
if (isEmptyGrid(x - 1, y - 1) && isEmptyGrid(x, y - 1) && isEmptyGrid(x - 1, y)) return false;
// 右上角
if (isEmptyGrid(x, y - 1) && isEmptyGrid(x + 1, y - 1) && isEmptyGrid(x + 1, y)) return false;
// 左下角
if (isEmptyGrid(x - 1, y) && isEmptyGrid(x - 1, y + 1) && isEmptyGrid(x, y + 1)) return false;
// 右上角
if (isEmptyGrid(x + 1, y) && isEmptyGrid(x, y + 1) && isEmptyGrid(x + 1, y + 1)) return false;
return true;
}
/// <summary>
/// 檢查(x,y)四周的牆壁是否被孤立
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <returns></returns>
bool CheckWallIsValid(int x, int y)
{
if (isWallGrid(x, y - 1) && 0 == GetNeighborWallCount(x, y - 1)) return false;
if (isWallGrid(x - 1, y) && 0 == GetNeighborWallCount(x - 1, y)) return false;
if (isWallGrid(x, y + 1) && 0 == GetNeighborWallCount(x, y + 1)) return false;
if (isWallGrid(x + 1, y) && 0 == GetNeighborWallCount(x + 1, y)) return false;
return true;
}
void Carve3(int x, int y)
{
maze[x, y] = true; // 打通
if(!CheckWallIsValid(x, y)) // 旁邊有牆壁被孤立
{
if (exceptionCount >= MAX_EXCEPTION_COUNT) // 而且超出例外
{
maze[x, y] = false;
return;
}
++exceptionCount;
}
// 隨機方向
var dirs = new Vector2Int[]
{
Vector2Int.up, Vector2Int.down,
Vector2Int.left, Vector2Int.right
};
dirs = Shuffle(dirs);
foreach (var dir in dirs)
{
int nx = x + dir.x;
int ny = y + dir.y;
if (nx >= 0 && ny >= 0 && nx <= width - 1 && ny <= height - 1 && !maze[nx, ny] && IsValid(nx, ny))
{
Carve3(nx, ny);
}
}
}
這版的DFS走訪就不再是走兩格而是走隔壁,而且不允許有2*2空格的存在。此外根據新增變數exceptionCount來判斷有幾個牆壁被孤立(旁邊四個方向都是空格),到達MAX_EXCEPTION_COUNT界限就不允許牆壁被孤立,這是為了避免地圖太破碎,不過看來效果不大,所以通常我會再修改一下地圖。
[Step5:測試修改的結果]
測試新函數的結果如下:


舊版的GenerateMaze函數也會保留,所以可以根據需要選用適合的函數。
【結語】
這兩個版本目前只能夠產生牆壁和空格產生的迷宮,不過我們的迷宮還有許多元素,例如金幣、門、鑰匙、陷阱和機關等等,目前看來也無法隨便用程式產生這些元素。不過利用程式幫助自己製作關卡,也算是提升一些工作效率了吧。也許以後除了迷宮產生器,也可以寫各式各樣的Script幫助自己能夠更快速開發遊戲或工具。