迷路を自動生成してみる!エディタ拡張付き![Unity+ミニゲーム]

シンプルな迷路ゲームを作ることが出来ましたが、毎回迷路を作るのは大変です。ということで、今回は迷路の自動生成を行ってみたいと思います。エディタ拡張機能も使いながら、編集を楽に行いましょう。

目次

迷路を自動生成する

迷路を自動でつくるわけですが、まずはどんなアルゴリズムで作られるのかを軽く解説します。

自動生成の生成方針とプログラム

今回の迷路自動生成は「プリム法・Primのアルゴリズム」というものを利用します。迷路全体は格子状の四角形を全体とし、開始地点から(上から見て)上下左右にランダムで訪れていない方向に向かって穴を空けながら進み、行けるところがなくなったら一つ前に戻って進める場所が無いかを探します。1マスごとの単位をセルとし、セルのデータは上下左右4方向に壁があるかどうかの情報を持っています。

MazeCellクラス

グリッドの単位になるセルを定義します。データ自体はMazeCellModelで、Unityのコンポーネントとして扱うのがMazeCellクラスになります。

using System.Collections.Generic;
using UnityEngine;

public class MazeCellModel
{
    public enum Wall { Top, Bottom, Left, Right }
    public bool visited = false;
    private Dictionary<Wall, bool> walls = new Dictionary<Wall, bool> {
        { Wall.Top, true },
        { Wall.Bottom, true },
        { Wall.Left, true },
        { Wall.Right, true }
    };

    public void RemoveWall(Wall wall)
    {
        walls[wall] = false;
    }

    public bool HasWall(Wall wall)
    {
        return walls[wall];
    }
}

public class MazeCell : MonoBehaviour
{
    [SerializeField] private GameObject[] wallAarray = new GameObject[] { };

    public void Setup(MazeCellModel mazeCellModel)
    {
        wallAarray[(int)MazeCellModel.Wall.Top].SetActive(mazeCellModel.HasWall(MazeCellModel.Wall.Top));

        for (int i = 0; i < (int)MazeCellModel.Wall.Right + 1; i++)
        {
            wallAarray[i].SetActive(mazeCellModel.HasWall((MazeCellModel.Wall)i));
        }
    }
}

MazeGeneratorクラス

続いてMazeCellModelを利用した経路作成の処理。アルゴリズム部分と、Unityで使える部分をそのまま残しています。

using System.Collections.Generic;
using UnityEngine;

public class MazeGenerator : MonoBehaviour
{
    public int width, height;
    private System.Random random = new System.Random();
    private MazeCellModel[,] maze;

    public GameObject mazeCellPrefab;
    [SerializeField] private Transform root;
    private float cellScale = 5f;

    public void ClearMaze()
    {
        List<GameObject> tempList = new List<GameObject>();
        foreach (Transform child in root)
        {
            tempList.Add(child.gameObject);
        }
        for (int i = 0; i < tempList.Count; i++)
        {
            DestroyImmediate(tempList[i]);
        }
    }

    public void GenerateMaze()
    {
        ClearMaze();

        maze = new MazeCellModel[width, height];
        for (int x = 0; x < width; x++)
        {
            for (int y = 0; y < height; y++)
            {
                maze[x, y] = new MazeCellModel();
            }
        }
        GenerateMaze(0, 0);

        for (int x = 0; x < width; x++)
        {
            for (int y = 0; y < height; y++)
            {
                float posX = x * cellScale;
                float posZ = y * cellScale;

                MazeCell cell = Instantiate(
                    mazeCellPrefab,
                    new Vector3(posX, 0f, posZ),
                    Quaternion.identity,
                    root).GetComponent<MazeCell>();

                cell.transform.localScale *= cellScale;
                cell.name = $"{x}-{y}";
                cell.Setup(maze[x, y]);
            }
        }
    }

    private void GenerateMaze(int x, int y)
    {
        MazeCellModel currentCell = maze[x, y];
        currentCell.visited = true;

        foreach (var direction in ShuffleDirections())
        {
            int newX = x + direction.Item1;
            int newY = y + direction.Item2;
            if (newX >= 0 && newY >= 0 && newX < width && newY < height)
            {
                MazeCellModel neighbourCell = maze[newX, newY];
                if (!neighbourCell.visited)
                {
                    neighbourCell.visited = true;
                    currentCell.RemoveWall(direction.Item3);
                    neighbourCell.RemoveWall(direction.Item4);
                    GenerateMaze(newX, newY);
                }
            }
        }
    }

    private List<(int, int, MazeCellModel.Wall, MazeCellModel.Wall)> ShuffleDirections()
    {
        List<(int, int, MazeCellModel.Wall, MazeCellModel.Wall)> directions = new List<(int, int, MazeCellModel.Wall, MazeCellModel.Wall)> {
            (0, 1, MazeCellModel.Wall.Top, MazeCellModel.Wall.Bottom),
            (0, -1, MazeCellModel.Wall.Bottom, MazeCellModel.Wall.Top),
            (-1, 0, MazeCellModel.Wall.Left, MazeCellModel.Wall.Right),
            (1, 0, MazeCellModel.Wall.Right, MazeCellModel.Wall.Left)
        };
        for (int i = 0; i < directions.Count; i++)
        {
            var temp = directions[i];
            int randomIndex = random.Next(i, directions.Count);
            directions[i] = directions[randomIndex];
            directions[randomIndex] = temp;
        }
        return directions;
    }
}

迷路のセル作成

セルは空のGameObjectを中心に1×1の正方形の大きさになるように、上下左右に壁を用意します。上下左右への大きさはMageGeneratorのcellScaleを使って調整します。

ベースになる空のGameObjectにはMazeCellコンポーネントをアタッチし、上下左右に相当する壁をWallArrayにセットします。この時セットする順番が重要になります。下図を参考にしてください。

作成できたらプレファブ化して、ヒエラルキーから削除しておきましょう。

MazeGeneratorをセット

インスペクターへのセットなど行います。空のGameObjectを用意しても構いませんが、前回の続きであればMazeManagerのあるGameObjectに同居させましょう。インスペクターにはMazeCellPrefabに先程作成したセルのプレファブ、またrootにはこれから自動生成する迷路の根本になるゲームオブジェクトをセットします。これは空のGameObjectを作成し、座標をリセットしておきましょう。(position=(0,0,0) rotation=(0,0,0) scale=(1,1,1))

迷路作成

作成済みの迷路を削除するか、非表示しておきます。MazeGeneratorスクリプトを少しだけ変更して、迷路が作られる様子を確認してみましょう。ちなみにここでうまく行かなくても次の項目に移行してもOKです。

MazeGeneratorスクリプトにスタートメソッドを追加して、一時的に再生時に生成されるように変更してみましょう。

public class MazeGenerator : MonoBehaviour
{
    // メンバ変数とか

    private void Start()
    {
        GenerateMaze();
    }
}

動かしてみると、再生時に迷路が自動生成されます。もし位置がおかしく感じる場合、キャラクターの位置が0,0,0でない可能性があります。

エディタ機能で拡張

さて、自動で生成出来ましたが、このままでは思ったような迷路じゃなかったり、ゴールの配置なんかに困ると思います。ということで、ここからはエディタ拡張機能を使ってゲームを再生しなくても迷路を作ってゴールを配置したりしてみたいと思います。エディタ拡張の基本に関しては下記記事を参考にしてください。

拡張用スクリプト作成

今回は迷路を作るボタンと、作っている迷路を削除するボタンの2つを作りたいと思います。

完成予想図

エディターが完成すると、MazeGeneratorコンポーネントに2つのボタンが表示されます。これはUnityを実行しなくても迷路を消したり作ったりすることができるものです。楽しいですよ。

Editor/MazeGeneratorEditorスクリプト

エディタ拡張用スクリプトなので、かならずEditorフォルダを作成し、その中に作ってください。

using UnityEngine;
using UnityEditor;

[CustomEditor(typeof(MazeGenerator))]
public class MazeGeneratorEditor : Editor
{
    public override void OnInspectorGUI()
    {
        DrawDefaultInspector();

        MazeGenerator mazeGenerator = (MazeGenerator)target;

        if (GUILayout.Button("作る"))
        {
            mazeGenerator.GenerateMaze();
        }
        if (GUILayout.Button("消す"))
        {
            mazeGenerator.ClearMaze();
        }
    }
}

ボタンを押して作る

あとはシーンビューを見ながらボタンを押して、お好みの迷路状態になったら好きな場所にゴールを置いて遊んでください。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

目次