日々是好日~every day is a good day~

日常の中の非日常の備忘録

【Unity】迷路作成のアルゴリズム

前回 Unityで作成したVR迷路ゲームをアップしました
制作段階をアップしたいところですが オブジェクトもスクリプトもそこそこの量になったので 重要な部分だけ記録しておきたいと思います
一つは迷路の作成部分
迷路を作るアルゴリズムはいくつかあります 棒倒し法 穴掘り法 壁埋め法 壁伸ばし法
丁寧にまとめられている方がいるので 私の拙い説明より見てもらった方がいいと思います
zenn.dev
理屈はなんとなーく分かるのですが それをコードにするだけの理解力がなかったので
こちらに公開されている壁伸ばし法のサンプルコードをコピペさせていただきました
algoful.com
ありがとうございます
まるっとコピペした後 仕様に合わせて変数とか戻り値を少しだけ変更しました

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MazeCreator : MonoBehaviour
{
    public int width;   //迷路横幅
    public int height;  //迷路縦幅
    public GameObject wallObj;   //Wallプレファブ
    public GameObject startObj;  //Startマークプレファブ
    public GameObject goalObj;   //Goalマークプレファブ
    public GameObject goalPoint; //ゴール位置
    public GameObject pointObj;  //移動ポイント
    int[,] maze;        //迷路配列
    GameObject movePoint;        //MovePointオブジェクト
    // 通路・壁情報
    static int Path = 0;
    static int Wall = 1;
    // 乱数生成用
    private System.Random Random;
    // 現在拡張中の壁情報を保持
    private Stack<Cell> CurrentWallCells;
    // 壁の拡張を行う開始セルの情報
    private List<Cell> StartCells;

    private enum Direction
    {
        Up = 0,
        Right = 1,
        Down = 2,
        Left = 3
    }
    void Awake()
    {
        // 5未満のサイズや偶数では生成できない
        if (width < 5 || height < 5) throw new ArgumentOutOfRangeException();
        if (width % 2 == 0) width++;
        if (height % 2 == 0) height++;
        // 迷路情報を初期化
        maze = new int[width, height];
        StartCells = new List<Cell>();
        CurrentWallCells = new Stack<Cell>();
        this.Random = new System.Random();

        CreateMaze();

        movePoint = GameObject.Find("MovePoint");
        //迷路の表示
        for (int i=0; i<width; i++)
        {
            for (int j=0; j<height; j++)
            {
                if (maze[i, j] == Wall)
                {
                    GameObject obj = Instantiate(wallObj);
                    obj.transform.position = new Vector3(i, 0, j);
                    
                }
                else if (maze[i, j] == Path)
                {
                    GameObject objP = Instantiate(pointObj);
                    objP.transform.SetParent(movePoint.transform);
                    objP.transform.position = new Vector3(i, GameManager.zombieY, j);
                }
                    
            }
        }
        GameObject start = Instantiate(startObj);
        start.transform.position = new Vector3(-0.5f, 2f, 1f);  //スタートポイント
        goalPoint = Instantiate(goalObj);
        goalPoint.transform.position = new Vector3(width , 2f, height - 2f);  //ゴールポイント
    }

    //迷路作成
    private void CreateMaze()
    {
        // 各マスの初期設定を行う
        for (int y = 0; y < height; y++)
        {
            for (int x = 0; x < width; x++)
            {
                // 外周のみ壁にしておき、開始候補として保持
                if (x == 0 || y == 0 || x == width - 1 || y == height - 1)
                {
                    maze[x, y] = Wall;
                }
                else
                { 
                    maze[x, y] = Path;
                    // 外周ではない偶数座標を壁伸ばし開始点にしておく
                    if (x % 2 == 0 && y % 2 == 0)
                    {
                        // 開始候補座標
                        StartCells.Add(new Cell(x, y));
                    }
                }
            }
        }

        // 壁が拡張できなくなるまでループ
        while (StartCells.Count > 0)
        {
            // ランダムに開始セルを取得し、開始候補から削除
            var index = Random.Next(StartCells.Count);
            var cell = StartCells[index];
            StartCells.RemoveAt(index);
            var x = cell.X;
            var y = cell.Y;

            // すでに壁の場合は何もしない
            if (maze[x, y] == Path)
            {
                // 拡張中の壁情報を初期化
                CurrentWallCells.Clear();
                ExtendWall(x, y);
            }
        }
        //Debug.Log("Maze retuen");
        maze[0, 1] = Path;
        maze[width - 1, height - 2] = Path;
    }


    // 指定座標から壁を生成拡張する
    private void ExtendWall(int x, int y)
    {
        // 伸ばすことができる方向(1マス先が通路で2マス先まで範囲内)
        // 2マス先が壁で自分自身の場合、伸ばせない
        var directions = new List<Direction>();
        if (maze[x, y - 1] == Path && !IsCurrentWall(x, y - 2))
            directions.Add(Direction.Up);
        if (maze[x + 1, y] == Path && !IsCurrentWall(x + 2, y))
            directions.Add(Direction.Right);
        if (maze[x, y + 1] == Path && !IsCurrentWall(x, y + 2))
            directions.Add(Direction.Down);
        if (maze[x - 1, y] == Path && !IsCurrentWall(x - 2, y))
            directions.Add(Direction.Left);

        // ランダムに伸ばす(2マス)
        if (directions.Count > 0)
        {
            // 壁を作成(この地点から壁を伸ばす)
            SetWall(x, y);

            // 伸ばす先が通路の場合は拡張を続ける
            var isPath = false;
            var dirIndex = Random.Next(directions.Count);
            switch (directions[dirIndex])
            {
                case Direction.Up:
                    isPath = (maze[x, y - 2] == Path);
                    SetWall(x, --y);
                    SetWall(x, --y);
                    break;
                case Direction.Right:
                    isPath = (maze[x + 2, y] == Path);
                    SetWall(++x, y);
                    SetWall(++x, y);
                    break;
                case Direction.Down:
                    isPath = (maze[x, y + 2] == Path);
                    SetWall(x, ++y);
                    SetWall(x, ++y);
                    break;
                case Direction.Left:
                    isPath = (maze[x - 2, y] == Path);
                    SetWall(--x, y);
                    SetWall(--x, y);
                    break;
            }
            if (isPath)
            {
                // 既存の壁に接続できていない場合は拡張続行
                ExtendWall(x, y);
            }
        }
        else
        {
            // すべて現在拡張中の壁にぶつかる場合、バックして再開
            var beforeCell = CurrentWallCells.Pop();
            ExtendWall(beforeCell.X, beforeCell.Y);
        }
    }

    // 壁を拡張する
    private void SetWall(int x, int y)
    {
        maze[x, y] = Wall;
        if (x % 2 == 0 && y % 2 == 0)
        {
            CurrentWallCells.Push(new Cell(x, y));
        }
    }

    // 拡張中の座標かどうか判定
    private bool IsCurrentWall(int x, int y)
    {
        return CurrentWallCells.Contains(new Cell(x, y));
    }

    // セル情報
    private struct Cell
    {
        public int X { get; set; }
        public int Y { get; set; }
        public Cell(int x, int y)
        {
            this.X = x;
            this.Y = y;
        }
    }
}

これで毎回違う迷路を作成することができるようになりました
迷路の縦幅 横幅も簡単に変更できるのでテスト用では9×7の迷路 ビルド用では19×17の迷路にしました
アルゴリズムは美しい!』です
もう一つ ゾンビの移動にもアルゴリズムを用いたのですが これは次回に