Unity - 簡易亂數迷宮產生3(Generate maze from a seed)

  這次把之前產生迷宮的方法做個小小的修改,而這個小小的修改就可以讓迷宮的使用簡單的Seed來產生,這樣子做法的好處跟之前的比起來,之前的亂數是無法自己控制的,每次產生的迷宮都不一樣;改為Seed之後,雖然亂數的計算還是交給電腦,但是在亂數產生上可以有自己的控制,因此我同樣可以亂數產生迷宮,但如果需要也可以再度產生出某個相同的迷宮,只要把Seed記錄下來。

  同樣使用Kruskal's algorithm來做為迷宮產生的判斷。





  這次不用做太多的修改,只需要改幾個部分而已,也就是Component中使用到Random.Range()的部分,這個亂數目前是無法自己控制的,所以做個小小的調整改為使用Seed即可。

  先建立一個類別來裝需要的東西,使用Static也可以,就可以直接呼叫使用,不過如果還有其他地方也會用到的話,就要小心先後呼叫的順序不同會造成同個Seed卻不同結果,所以這邊還是用需要建立instance的方法。

    public class MyRandom
    {
        private System.Random random;

        public MyRandom(int seed)
        {
            random = new System.Random(seed); //使用System的Random來丟入Seed
        }

        //回傳整數
        public int Range(int min, int max) //使用跟Random.Range()相同的方法名稱,之後把原本的Random取代
        {
            return random.Next(min, max);
        }

        //回傳浮點數
        public float Range(float min, float max)
        {
            double range = (double)max - (double)min;
            double sample = random.NextDouble();
            double scaled = (sample * range) + min;
            return (float)scaled;
        }
    }



  之後跟原本的Component差不多,不過多加Seed參數可以在編輯器輸入數值,以及把原本的Random.Range()改為MyRandom.Range()而已,這樣就可以亂數產生迷宮的同時,使用Seed來複製某一個想要重來的迷宮。

參考Component。
public class KruskalMaze : MonoBehaviour
{
    private class Cell
    {
        //使用一個陣列來設定四個牆的屬性,0還沒檢查,1是有牆面,2則是打通的通道
        public int[] wall = new int[4]; //陣列四個依序為N, E, S, W
        public Cell[] nextCell = new Cell[4]; //紀錄通道通往的下一個格子
        public int x, y;
    }

    public int width = 10; //地圖的寬
    public int height = 10; //地圖的高
    private Cell[][] cellArr; //地圖陣列

    public int seed; //輸入Seed數值
    private MyRandom myRandom; //使用自己的類別

    void Start ()
    {
        myRandom = new MyRandom(seed); //初始化
        StartCoroutine(Maze (width, height));
    }

    //這邊用一個簡單的Draw Gizmos在Unity編輯室窗上顯示地圖形成的過程
    void OnDrawGizmos()
    {
        float size = 1.5f;
        Gizmos.color = Color.yellow;
        if(cellArr != null)
        {
            for(int i = 0; i < height; ++i)
            {
                for(int j = 0; j < width; ++j)
                {
                    Gizmos.DrawCube(new Vector3(j*size, i*size, 0), new Vector3(0.5f, 0.5f, 0.5f));
                    if(cellArr[i][j].wall[0] == 2) Gizmos.DrawCube(new Vector3(j*size, i*size + 0.5f, 0), new Vector3(0.5f, 0.5f, 0.5f));
                    if(cellArr[i][j].wall[1] == 2) Gizmos.DrawCube(new Vector3(j*size + 0.5f, i*size, 0), new Vector3(0.5f, 0.5f, 0.5f));
                    if(cellArr[i][j].wall[2] == 2) Gizmos.DrawCube(new Vector3(j*size, i*size - 0.5f, 0), new Vector3(0.5f, 0.5f, 0.5f));
                    if(cellArr[i][j].wall[3] == 2) Gizmos.DrawCube(new Vector3(j*size - 0.5f, i*size, 0), new Vector3(0.5f, 0.5f, 0.5f));
                }
            }
        }
    }

    //建立地圖
    IEnumerator Maze(int width, int height)
    {
        //初始化地圖陣列跟所有點的列表
        List<Cell> cellList = new List<Cell>();
        cellArr = new Cell[height][];
        for(int i = 0; i < height; ++i)
        {
            cellArr[i] = new Cell[width];
            for(int j = 0; j < width; ++j)
            {
                cellArr[i][j] = new Cell();

                //順便設定邊界的牆
                if(j == 0) cellArr[i][j].wall[3] = 1;
                if(j == width-1) cellArr[i][j].wall[1] = 1;
                if(i == 0) cellArr[i][j].wall[2] = 1;
                if(i == height-1) cellArr[i][j].wall[0] = 1;
                cellArr[i][j].y = i;
                cellArr[i][j].x = j;

                //加入列表
                cellList.Add(cellArr[i][j]);
            }
        }

        //只要列表的格子還沒檢查完就一直檢查
        while(cellList.Count > 0)
        {
            //從列表亂數選一個格子
            Cell cell = cellList[myRandom.Range(0, cellList.Count)]; //改為myRandom

            //先判斷這個格子四個方向是否都檢查完,如果檢查完就移除列表並且重新再選一個格子
            if(IsCellFinished(cell))
            {
                cellList.Remove(cell);
                continue; //Re-Pick cell
            }

            //亂數選一個方向並確定該方向是還沒檢查過的
            int dir = myRandom.Range(0, 4); //改為myRandom
            while(true)
            {
                if(cell.wall[dir] == 0)
                    break;

                dir = myRandom.Range(0, 4); //改為myRandom
            }

            //取得該方向的下一個格子資料
            int dirY = (dir == 0) ? 1 : (dir == 2) ? -1 : 0;
            int dirX = (dir == 1) ? 1 : (dir == 3) ? -1 : 0;
            int nextY = cell.y + dirY;
            int nextX = cell.x + dirX;
            Cell nextCell = cellArr[nextY][nextX];

            //從這個格子開始檢查是否連回最初的格子
            if(IsClosedCircuit(cell, nextCell, new List<Cell>()))
            {
                //起始格跟下一個方向的格子連線會形成封閉迴圈,把這個方向設定為牆壁
                cell.wall[dir] = 1;
                nextCell.wall[(dir+2)%4] = 1;

                //順便檢查這兩個格子是否都檢查完
                if(IsCellFinished(cell)) cellList.Remove(cell);
                if(IsCellFinished(nextCell)) cellList.Remove(nextCell);
                continue; //從頭步驟在開始
            }

            //都檢查完,把起始格跟下一個方向的格子連線形成通道
            cell.wall[dir] = 2;
            cell.nextCell[dir] = nextCell;
            nextCell.wall[(dir+2)%4] = 2;
            nextCell.nextCell[(dir+2)%4] = cell;

            //順便檢查這兩個格子是否都檢查完
            if(IsCellFinished(cell)) cellList.Remove(cell);
            if(IsCellFinished(nextCell)) cellList.Remove(nextCell);

            yield return null;
        }
    }

    //檢查封閉迴圈
    private bool IsClosedCircuit(Cell checkCell, Cell start, List<Cell> visitedList)
    {
        visitedList.Add(start);
        for(int i = 0; i < 4; ++i)
        {
            if(start.wall[i] == 2)
            {
                if(visitedList.Contains(start.nextCell[i]))
                    continue;

                if(start.nextCell[i] == checkCell)
                    return true;

                if(IsClosedCircuit(checkCell, start.nextCell[i], visitedList))
                    return true;
            }
        }
        return false;
    }

    //格子是否全部方向都檢查完
    private bool IsCellFinished(Cell cell)
    {
        bool isFinished = true;
        for(int i = 0; i < 4; ++i)
        {
            if(cell.wall[i] == 0) isFinished = false;
        }
        return isFinished;
    }
}



No comments:

Post a Comment