phaser 3: 迷路ゲームを作る① timerによるイベントループ

迷路ゲームの作成に挑戦してみます。
f:id:Phaser_3:20181203162500p:plain

Pure JavaScript perfect tile maze generation – with a bit of magic thanks to Phaser – Emanuele Feronato

のチュートリアルに従い、迷路を自動生成してみます。
上記のサンプルはphaser2のjavascriptのものなので、phaser3のtypescript用コードに書き直します。

まずは迷路の各パラメータの設定をします。

public graphics: Phaser.GameObjects.Graphics;

//迷路の幅、高さ
public mazeWidth: number = 0;
public mazeHeight: number = 0;
public maze: number[][] = [];

//迷路の道幅
public tileSize: number = 10;
~~
create() {

    //背景を白に
    this.cameras.main.setBackgroundColor("#ffffff");

    //グラフィックスオブジェクトを追加
    this.graphics = this.add.graphics();

    //移動データ用配列
    var moves = [];

    //キャンバスの大きさから迷路の配列サイズを設定
    this.mazeWidth = this.sys.canvas.width / this.tileSize + 1
    this.mazeHeight = this.sys.canvas.height / this.tileSize + 1

    for (var i = 0; i < this.mazeHeight; i++) {
        this.maze[i] = [];
        for (var j = 0; j < this.mazeWidth; j++) {
            this.maze[i][j] = 1;
        }
    }

    //迷路の初期位置を設定
    var posX = 1;
    var posY = 1;

}

timerでループイベントを設定

サンプル元は逐次実行で迷路を作り上げていく形なので、timerでループイベントを設定します。
time.addeventでタイマーを作り、timer.collbackでコールバックを追加できます。

create(){
~~
    //timerを設定、10ミリ秒ごとにcallbackをループする
    let timer = this.time.addEvent({
        delay: 10,
        loop: true
    });

    //タイマーのコールバック
    timer.callback = () => {

上記のdelay:10,loop:trueの設定であれば10秒ごとにcollbackを呼びます。
callbackの中に迷路描画処理を書きます。

    //タイマーのコールバック
    timer.callback = () => {
        //moveに次の進行可能箇所を設定する
        var possibleDirections = "";
        if (posX + 2 > 0 && posX + 2 < this.mazeHeight - 1 && this.maze[posX + 2][posY] == 1) {
            possibleDirections += "S";
        }
        if (posX - 2 > 0 && posX - 2 < this.mazeHeight - 1 && this.maze[posX - 2][posY] == 1) {
            possibleDirections += "N";
        }
        if (posY - 2 > 0 && posY - 2 < this.mazeWidth - 1 && this.maze[posX][posY - 2] == 1) {
            possibleDirections += "W";
        }
        if (posY + 2 > 0 && posY + 2 < this.mazeWidth - 1 && this.maze[posX][posY + 2] == 1) {
            possibleDirections += "E";
        }
        if (possibleDirections) {
            var move = Phaser.Math.Between(0, possibleDirections.length - 1);
            switch (possibleDirections[move]) {
                case "N":
                    this.maze[posX - 2][posY] = 0;
                    this.maze[posX - 1][posY] = 0;
                    posX -= 2;
                    break;
                case "S":
                    this.maze[posX + 2][posY] = 0;
                    this.maze[posX + 1][posY] = 0;
                    posX += 2;
                    break;
                case "W":
                    this.maze[posX][posY - 2] = 0;
                    this.maze[posX][posY - 1] = 0;
                    posY -= 2;
                    break;
                case "E":
                    this.maze[posX][posY + 2] = 0;
                    this.maze[posX][posY + 1] = 0;
                    posY += 2;
                    break;
            }
            moves.push(posY + posX * this.mazeWidth);
        }
        else {
            var back = moves.pop();
            posX = Math.floor(back / this.mazeWidth);
            posY = back % this.mazeWidth;
        }
        //迷路を描画
        this.drawMaze(posX, posY);
    }

fillRectで矩形描画

上記でループ実行部分を移植できました。
次はコールバックの各ループ最後に呼ばれている描画処理を移植します。
this.graphicsの中にfillRectで矩形を描画します。
fillRectの直前に呼ばれたfillStyleの形式に沿って矩形が描画されます。
道の色には黒を、現在位置の色には緑を指定しました。

public drawMaze(posX, posY) {
    //道を表示
    this.graphics.fillStyle(0x000000, 1);

    for (let i = 0; i < this.mazeHeight; i++) {
        for (let j = 0; j < this.mazeWidth; j++) {
            if (this.maze[i][j] !== 1) {
                this.graphics.fillRect(j * this.tileSize, i * this.tileSize, this.tileSize, this.tileSize);
            }
        }
    }

    //現在位置を表示
    this.graphics.fillStyle(0x00ff00, 1);
    this.graphics.fillRect(posY * this.tileSize, posX * this.tileSize, this.tileSize, this.tileSize);
            
}

以上で迷路のプロシージャル生成アルゴリズムができました。
サンプルでは生成経過を見せるために逐次実行する形式になっているのかと思いますが、while等で迷路の生成を待てば一気に迷路を作り上げることができます。
その場合800*600の画面で数秒程度で迷路が出来上がります。
f:id:Phaser_3:20181203162340g:plainf:id:Phaser_3:20181203162500p:plain

今回のソースはこちらになります。

/// <reference path="../app.ts" />
namespace MyGame {

    export class MyScene1 extends Phaser.Scene {
        
        public graphics: Phaser.GameObjects.Graphics;

        //迷路の幅、高さ
        public mazeWidth: number = 0;
        public mazeHeight: number = 0;
        public maze: number[][] = [];

        //迷路の道幅
        public tileSize: number = 10;

        constructor() {
            super({ key: 'MyScene1', active: false });
        }

        create() {

            //背景を白に
            this.cameras.main.setBackgroundColor("#ffffff");

            //グラフィックスオブジェクトを追加
            this.graphics = this.add.graphics();

            //移動データ用配列
            var moves = [];

            //キャンバスの大きさから迷路の配列サイズを設定
            this.mazeWidth = this.sys.canvas.width / this.tileSize + 1
            this.mazeHeight = this.sys.canvas.height / this.tileSize + 1

            for (var i = 0; i < this.mazeHeight; i++) {
                this.maze[i] = [];
                for (var j = 0; j < this.mazeWidth; j++) {
                    this.maze[i][j] = 1;
                }
            }

            //迷路の初期位置を設定
            var posX = 1;
            var posY = 1;

            //初期位置には0(壁ではない)を設定
            this.maze[posX][posY] = 0;
            moves.push(posY + posY * this.mazeWidth);

            //timerを設定、10ミリ秒ごとにcallbackをループする
            let timer = this.time.addEvent({
                delay: 10,
                loop: true
            });

            //タイマーのコールバック
            timer.callback = () => {
                //moveに次の進行可能箇所を設定する
                var possibleDirections = "";
                if (posX + 2 > 0 && posX + 2 < this.mazeHeight - 1 && this.maze[posX + 2][posY] == 1) {
                    possibleDirections += "S";
                }
                if (posX - 2 > 0 && posX - 2 < this.mazeHeight - 1 && this.maze[posX - 2][posY] == 1) {
                    possibleDirections += "N";
                }
                if (posY - 2 > 0 && posY - 2 < this.mazeWidth - 1 && this.maze[posX][posY - 2] == 1) {
                    possibleDirections += "W";
                }
                if (posY + 2 > 0 && posY + 2 < this.mazeWidth - 1 && this.maze[posX][posY + 2] == 1) {
                    possibleDirections += "E";
                }
                if (possibleDirections) {
                    var move = Phaser.Math.Between(0, possibleDirections.length - 1);
                    switch (possibleDirections[move]) {
                        case "N":
                            this.maze[posX - 2][posY] = 0;
                            this.maze[posX - 1][posY] = 0;
                            posX -= 2;
                            break;
                        case "S":
                            this.maze[posX + 2][posY] = 0;
                            this.maze[posX + 1][posY] = 0;
                            posX += 2;
                            break;
                        case "W":
                            this.maze[posX][posY - 2] = 0;
                            this.maze[posX][posY - 1] = 0;
                            posY -= 2;
                            break;
                        case "E":
                            this.maze[posX][posY + 2] = 0;
                            this.maze[posX][posY + 1] = 0;
                            posY += 2;
                            break;
                    }
                    moves.push(posY + posX * this.mazeWidth);
                }
                else {
                    var back = moves.pop();
                    posX = Math.floor(back / this.mazeWidth);
                    posY = back % this.mazeWidth;
                }
                //迷路を描画
                this.drawMaze(posX, posY);
            }

        }

        public drawMaze(posX, posY) {
            //道を表示
            this.graphics.fillStyle(0x000000, 1);

            for (let i = 0; i < this.mazeHeight; i++) {
                for (let j = 0; j < this.mazeWidth; j++) {
                    if (this.maze[i][j] !== 1) {
                        this.graphics.fillRect(j * this.tileSize, i * this.tileSize, this.tileSize, this.tileSize);
                    }
                }
            }

            //現在位置を表示
            this.graphics.fillStyle(0x00ff00, 1);
            this.graphics.fillRect(posY * this.tileSize, posX * this.tileSize, this.tileSize, this.tileSize);
            
        }
    }
}