アクツクMVで迷路を自動生成してみた

今回は、アクツクMVで迷路の自動生成を試みました。
（ツイッターで動画：https://twitter.com/havin_nothin/status/1625674204107636736 ）

とりあえず、こうすればできますという手順を説明します。

１．プロジェクトはトップビュー、画面サイズは６８０ｘ３６０、タイルサイズは２４ｘ２４で作成しています。
　プロジェクト設定から、オブジェクトグループに「迷路」を追加しています。

２．タイルを登録。タイル設定から、ギミックタイルをチェック。
（画像はトップビューのチュートリアルプロジェクトのimgファルダにある０４１.pngを使用）

３．迷路の「壁」にしたいタイルをクリックします。（画面では左上の赤い四角）
迷路グループがタイルに重なったら、別のタイルに変化して「道」になる設定をします。（画像の右側）
壁判定が必要なら基本設定で設定します。
![01tile.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3103492/85020ed3-45fa-a685-a4cd-99d23544ad29.png)

４．シーンを作成し、タイルを設定。画面全体を「壁」で埋める。
![02scene.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3103492/e97aed40-878f-5ac4-c94b-191307cb30b5.png)

５．アニメーションを作る。
「透明」モーションを作成します。アニメーションと言っても、画像は何でもよいですが、ないとダメなのです。スケール０％にして表示されないようにする。
画面では、タイルと同じ画像をつかっています。（２３ｘ１８に分割）
（画面の「四角」モーションはデバッグ用に表示するためのものです）
![03anim.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3103492/1f104d77-e128-6420-8452-6bb831fc790d.png)

６．MazeMakerオブジェクトを設定する。
作成したアニメーションで迷路作成オブジェクトを作る。
名前は「MazeMaker」にしてください。別の名前は不可。
以下変数を登録します。これも別の名前は不可です。

TileSize　初期値：タイルサイズを入力。（ここでは２４）
MazeNow　迷路作成の現在値のための変数：初期値：０
MazeSize　迷路の大きさ：初期値：０
MazeSizeX　迷路の横幅　初期値：横幅を入力。（ここでは１５）
MazeSizeY　迷路の縦幅　初期値：縦幅を入力。（ここでは１３）
OffsetX　初期値：迷路の位置のX座標（ここでは５）
OffsetY　初期値：迷路の位置のX座標（ここでは１）
RoadType　道の種類を示す（後述）初期値：-2
変数001～　迷路の縦幅（MazeSizeY）分の変数を用意する。名前はかならず、変数001、変数002と連番。（画面下の「＋」を押したら自動で名づけられる、そのままを使う）


MazeSizeX、MazeSizeYが迷路の大きさになります。縦横どちらも奇数にしてください。横幅は４９以下です。５０以上は仕様上できません。縦幅には制限はありません（理論上）。
（画面では変数001～変数025とありますが、たくさんあってもいいので多めに作ってます。
![04variable.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3103492/9609b6ed-f776-db80-0d1a-04caf0bd76cb.png)

７．アクションプログラム
アクションプログラムは、以下の画面のようになります。
とりあえず４つのアクションを作って、画面のように→でつないでください。

「開始」アクションに、モーション「透明」に設定。それ以外のアクションは「遷移前のモーションを引き継ぐ」にチェックしてください。
「何もしない」ボックスはなにもしていません。

↓は最後の「移動→終了」以外は、すべて「無条件で切り替え」です。
「移動→終了」の↓の設定は画面の通りです。「判定優先度」を別の矢印より大きくしてください（画面では２）。
変数MazaNowはカウンタです。カウンタが変数MazeSizeより大きくなったら終了ということです。
「終了」アクションはなにもしていません。

![06DigEnd.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3103492/cb01bf66-7195-0dce-8dd2-50a54cf06d66.png)

８．スクリプト
スクリプトは２つあります。
「迷路データ作成」と「迷路を掘る」です。
まず、「開始」アクションに「迷路データ作成」スクリプトを設定します。
![04MazeData.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3103492/84c93b59-dcb6-67c0-104f-98198b2d8d47.png)

スクリプトはこちら
```迷路データ作成
(function () {
    //迷路データ作成
 
   const mazeMaker = Agtk.objectInstances.get(instanceId);
    let mazeMaxX = mazeMaker.variables.get(mazeMaker.variables.getIdByName("MazeSizeX")).getValue();
    let mazeMaxY = mazeMaker.variables.get(mazeMaker.variables.getIdByName("MazeSizeY")).getValue();
    mazeMaker.variables.get(mazeMaker.variables.getIdByName("MazeSize")).setValue(mazeMaxX * mazeMaxY); let mztxt = '';
    let dd = 0;
    let mps = [];

    makemaze(); //迷路作成0か1のテキストがmztxtにはいる。０が通路

    //Agtk.log(mztxt);
    for (i = 1; i <= mazeMaxY; i++) {
        let mzd = parseInt(mztxt.substr((i - 1) * mazeMaxX, mazeMaxX), 2);
        let mdname = "変数" + ("000" + i.toString()).slice(-3);
        Agtk.log(mdname);
        Agtk.log(mzd);
        Agtk.log(mzd.toString(2));
        mazeMaker.variables.get(mazeMaker.variables.getIdByName(mdname)).setValue(mzd);
    }

    mazeMaker.variables.get(mazeMaker.variables.getIdByName("MazeSize")).setValue(mazeMaxX * mazeMaxY);

    function makemaze() {
        mztxt = '';
        for (i = 0; i < mazeMaxX * mazeMaxY; i++) {
            mztxt += '1';
        }
        mps = [];
        mps.push([1, 1]);
        mzw(1, 1);
        dd = 1;
        do {
            mpi = Math.floor(Math.random() * mps.length);
            if (mz(mps[mpi][0] - 2, mps[mpi][1]) + mz(mps[mpi][0] + 2, mps[mpi][1]) + mz(mps[mpi][0], mps[mpi][1] - 2) + mz(mps[mpi][0], mps[mpi][1] + 2) == 0) { mps.splice(mpi, 1); } else {
                dig(mps[mpi][0], mps[mpi][1]);
            }
        } while (dd < Math.floor(mazeMaxX / 2) * Math.floor(mazeMaxY / 2));

    }

    function dig(xd, yd) {
        vt = Math.floor(Math.random() * 4);
        vy = Math.floor((vt - 1) / 2);
        vx = (vt * 2 - 3) - vy * 3;
        if (mz(xd + vx * 2, yd + vy * 2) == 1) {
            mzw(xd + vx * 2, yd + vy * 2);
            mzw(xd + vx, yd + vy);
            xd += vx * 2; yd += vy * 2;
            mps.push([xd, yd]);
            dig(xd, yd);
            dd++;
        }
    }

    function mz(ax, ay) {
        if (ax < 0 || ax >= mazeMaxX || ay < 0 || ay > mazeMaxY) { return 0; }
        else { return parseInt(mztxt.substr(ax + ay * mazeMaxX, 1)); }
    }

    function mzw(ax, ay) {
        mztxt = mztxt.substr(0, ax + ay * mazeMaxX) + '0' + mztxt.substr(ax + ay * mazeMaxX + 1);
    }
})()
```

「移動」アクションにも同様に、「迷路を掘る」スクリプトを設定します。
![05Dig.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3103492/ce2eff42-f308-ec4f-f35f-257f88278a56.png)
```迷路を掘るスクリプト
(function () {
    //迷路を掘って、道タイプをRoadTypeに入れるスクリプト
    //MazeMaker以外のオブジェクトで使用するときは、変数MazeNowとRoadTypeの設定が必要
    const dropper = Agtk.objectInstances.get(instanceId);
    const mazeMaker = Agtk.objectInstances.get(Agtk.objectInstances.getIdByName(-1, "MazeMaker"));

    const tileSize = mazeMaker.variables.get(mazeMaker.variables.getIdByName("TileSize")).getValue();
    const mazeMaxX = mazeMaker.variables.get(mazeMaker.variables.getIdByName("MazeSizeX")).getValue();
    const mazeMaxY = mazeMaker.variables.get(mazeMaker.variables.getIdByName("MazeSizeY")).getValue();
    const offsetX = mazeMaker.variables.get(mazeMaker.variables.getIdByName("OffsetX")).getValue();
    const offsetY = mazeMaker.variables.get(mazeMaker.variables.getIdByName("OffsetY")).getValue();

    const mazeNow = dropper.variables.get(dropper.variables.getIdByName("MazeNow")).getValue();

    //迷路情報を取得、復元
    let mazeData = '';
    for (i = 1; i <= mazeMaxY; i++) {
        let mdname = "変数" + ("000" + i.toString()).slice(-3);
        let mztxt0 = mazeMaker.variables.get(mazeMaker.variables.getIdByName(mdname)).getValue().toString(2);
        mazeData += mztxt0;
    }
    const mazeBits = mazeData + "1".repeat(mazeMaxX);


    //道タイプ判定（上左右下の２進数：0が道、１が壁）
    const bitCenter = mazeBits.substr(mazeNow, 1);
    const bitUp = mazeBits.substr(mazeNow - mazeMaxX, 1);
    const bitLeft = mazeBits.substr(mazeNow - 1, 1);
    const bitRight = mazeBits.substr(mazeNow + 1, 1);
    const bitDown = mazeBits.substr(mazeNow + mazeMaxX, 1);
    let loadType = -1;

    //現在地が道ならそこに移動して道を掘る
    if (bitCenter == "0") {
        //Agtk.log(bitUp + bitLeft + bitRight + bitDown);
        loadType = parseInt(bitUp + bitLeft + bitRight + bitDown, 2);

        tileX = mazeNow % mazeMaxX + offsetX;
        tileY = Math.floor(mazeNow / mazeMaxX) + offsetY;
        digx = tileX * tileSize + tileSize * 0.5;
        digy = tileY * tileSize + tileSize * 0.5;
        dropper.variables.get(dropper.variables.getIdByName("X座標位置")).setValue(digx);
        dropper.variables.get(dropper.variables.getIdByName("Y座標位置")).setValue(digy);
    }

    //変数を書き出す
    dropper.variables.get(dropper.variables.getIdByName("MazeNow")).setValue(mazeNow + 1);
    dropper.variables.get(dropper.variables.getIdByName("RoadType")).setValue(loadType);

    //Agtk.log(mazeBits);
    //Agtk.log(bit);
})()
```

９．MazeMakerオブジェクトをシーンに配置して、実行すると出来上がりです。
MazeMakerオブジェクトは画面外に配置してください。
![07Maze.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3103492/8226c8ae-9152-4504-198b-87cd1509226a.png)

「迷路を掘る」スクリプトは、変数MazeNowに迷路上の現在値をいれて実行すると、変数RoadTypeに、周囲がどんな形かを入れます。
形とRoadTypeの対応（上下左右は通路の空き方向）

１か所だけ（行き止まり）
上　　7
左　　11
右　　13
下　　14
２か所（直線か曲がり角）
上左　3
上右　5
上下　6
左右　9
左下　10
右下　12
３か所（三叉路）
上左右　1
上左下　2
上右下　4
左右下　8
４か所（十字路）
上下左右　0

MazeNowの位置が道ではない　-1

MazeNowは左上から横に数えて、何番目かをしめす。開始は１から。↓こんな感じ。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3103492/18d1ef62-2f48-b09d-c6b7-eaa8eb0c3b3f.png)

（「終了」アクションでモーションを引き継がないで「設定しない」にすると、迷路を掘らなくなるのでいいかも）

長くなったので、どのように実現したかは
次回　https://qiita.com/havin_nothin/items/fa2bf322b366e8ed8473
にします。

では。

迷路作成にあたって、以下のサイトを参考にさせていただきました（っていうか、改変して使用させていただきました。ありがとうございます。）
https://little-strange.hatenablog.com/entry/2021/05/02/224300