😊

Phaserでマップ内のタイルを移動させる

2022/06/16に公開

tilemapで作成したタイルレイヤーのタイルを移動させる

マップエディタ(Tiled)で作成したtilemapを利用して、マップを作成します。tilemapで作成したレイヤーのタイルを移動させます。プレイヤーがタイルを「押した」状態になるとタイルが移動します

移動可能なタイルとして「boxLayer」を作成します。「boxLayer」はプレイヤーと衝突します。プレイヤーが「boxLayer」のタイルを押した状態になると、移動後の座標が空であれば、移動可能にします。移動後の座標がgroundlayerのタイル、boxLayerのタイルが存在する場合、移動できません

マップの使用

  • タイルは64x64
  • 横は12マス、縦は12マス
  • ステージとなるのはstage => groundLayer
  • 移動可能なボックスはbox => boxLayer
  • groundLayerにはプレイヤーは衝突する
  • boxLayerのタイルはプレイヤーが押すことができる

議論のある点

悩んだのが以下の点

  • マップエディタでレイヤーを作成するときオブジェクトレイヤーなのか、タイルレイヤーなのか
  • オブジェクトレイヤーの場合、スプライトとして作成できるので、衝突、押す処理の自由度があがる。一方、座標を決めるのがタイルではないので、ステージ内に配置するのが面倒
  • マップエディタでタイルレイヤーで作成すると、マップエディタでの作業が圧倒的に楽!
  • マップエディタでタイルレイヤーで作成したうえで、タイルレイヤーの「TileMapData」を利用して、スプライトを作成することが、一般的な方法に思える
  • プレイヤーとスプライトの衝突にした場合、衝突するときの座標の計算が実は面倒になる。レイヤーの「getTileAtWorldXY」利用して、タイルの有無を確認するのが簡単だと分かった
  • ということえ、タイルレイヤーを利用してレイヤーを作成して、タイルレイヤーのままでタイルの移動処理を考えました

利用するスプライト画像

以下の画像を利用したtilemapとプレイヤーを作成します。1個のタイルのサイズは横64、縦64です

こちらの画像

createメソッド

マップとプレイヤーを作成します

mainScene.create = function() {
    this.createMap();
    this.createPlayer();
};

createMapによるマップデータ作成

createMapではtilemapのデータによるgroundLayerとboxLayerを作成します

mainScene.createMap = function() {
    // マップ表示
    // JSON形式のマップデータの読み込み Tilemapオブジェクトの作成
    this.map = this.make.tilemap({key: 'map09'});

    // タイル画像をマップデータに反映する Tilesetオブジェクトの作成
    this.groundTiles = this.map.addTilesetImage("tile", "tile");
    
    // 地面レイヤー作成 DynamicTilemapLayerオブジェクト作成
    const layerWidth = 64 * 1 * 12;
    const layerHeight = 64 * 1 * 12;

    // 画面中央のX座標
    const centerX = this.game.config.width / 2;
    // 画面中央のY座標
    const centerY = this.game.config.height / 2;
    // レイヤーの左上X座標
    const layerOriginX = (this.game.config.width - layerWidth) / 2;
    // レイヤーの左上Y座標
    const layerOriginY = (this.game.config.height - layerHeight) / 2;
    // レイヤー作成
    this.groundLayer = this.map.createLayer('stage', this.groundTiles,layerOriginX, layerOriginY);
    this.groundLayer.setDisplaySize(layerWidth, layerHeight);
    this.groundLayer.setCollisionByExclusion([-1]);

    // BOXのレイヤー作成
    this.boxLayer = this.map.createLayer('box', this.groundTiles,layerOriginX, layerOriginY);
    this.boxLayer.setDisplaySize(layerWidth, layerHeight);
    this.boxLayer.setCollisionByExclusion([-1]);
    
    // ゲームワールドの幅と高さの設定
    this.physics.world.bounds.width = this.game.config.width;
    this.physics.world.bounds.height = this.game.config.height;
    // カメラの表示サイズの設定をする。マップのサイズがカメラの表示サイズ
    this.cameras.main.setBounds(0, 0, this.physics.world.bounds.width, this.physics.world.bounds.height);
};

updateメソッド

プレイヤーの移動処理を行います。プレイヤーとgroundLayerのタイルは衝突します。プレイヤーはboxLayerのタイルを押すことができます。ただし、boxLayerのタイルの移動後の座標が空の場合のみ押すことができます

mainScene.update = function() {
    // 中略
};

左方向の移動

this.input.keyboard.checkDownはようするにonkeydownを細かく調整するためのメソッドです。第1引数はキーがプレスダウンされているかを確認するキー、第2引数は待機時間のミリ秒です。ここでは左カーソルキーがプレスダウンされているかどうかを判定します

  • tileは移動後の1個先groundLayerのタイル
  • tile2は移動後の2個先のgroundLayerのタイル
  • boxTileは移動後の1個先のboxLayerのタイル
  • boxTile2は移動後の2個先のboxLayerのタイル

boxTileと衝突している場合、tile2とboxTile2がnullであれば、boxTile2を押す
boxTileと衝突していないくて、かつtileとも衝突していない場合、プレイヤーの移動

    if (this.input.keyboard.checkDown(this.cursors.left, 100)) {
        const tile = this.groundLayer.getTileAtWorldXY(this.player.x - this.player.dx, this.player.y);
        const tile2 = this.groundLayer.getTileAtWorldXY(this.player.x - this.player.dx * 2, this.player.y);
        const boxTile = this.boxLayer.getTileAtWorldXY(this.player.x - this.player.dx, this.player.y);
        const boxtile2 = this.boxLayer.getTileAtWorldXY(this.player.x - this.player.dx * 2, this.player.y);
        if (boxTile !== null) {
            // BOXと衝突
            if( tile2 === null && boxtile2 === null) {
                // 移動後のタイルも空なので移動
                this.boxLayer.removeTileAt(boxTile.x, boxTile.y);
                this.boxLayer.putTileAt(boxTile, boxTile.x-1, boxTile.y);
                this.player.x -= this.player.dx;
                this.player.anims.play('left', true);
            }
        } else {
            // BOXとは衝突していない
            if (tile === null) {
                // タイルもないので移動可能
                this.player.x -= this.player.dx;
                this.player.anims.play('left', true);
            }
        }

タイルを押す処理

スプライトの場合、座標を指定することで移動させることができますが、タイルの場合、座標の指定でも移動させることができなかったので、タイルの削除と新しいタイルの作成という処理で疑似的にタイルを移動させる(タイルを押す)を実現しました

                this.boxLayer.removeTileAt(boxTile.x, boxTile.y);
                this.boxLayer.putTileAt(boxTile, boxTile.x-1, boxTile.y);

全体の移動判定

上下左右の移動は以下の通りです。リファクタの余地がありますが、とりあえずベタっと書きました

mainScene.update = function() {
    if (this.input.keyboard.checkDown(this.cursors.left, 100)) {
        const tile = this.groundLayer.getTileAtWorldXY(this.player.x - this.player.dx, this.player.y);
        const tile2 = this.groundLayer.getTileAtWorldXY(this.player.x - this.player.dx * 2, this.player.y);
        const boxTile = this.boxLayer.getTileAtWorldXY(this.player.x - this.player.dx, this.player.y);
        const boxtile2 = this.boxLayer.getTileAtWorldXY(this.player.x - this.player.dx * 2, this.player.y);
        if (boxTile !== null) {
            // BOXと衝突
            if( tile2 === null && boxtile2 === null) {
                // 移動後のタイルも空なので移動
                this.boxLayer.removeTileAt(boxTile.x, boxTile.y);
                this.boxLayer.putTileAt(boxTile, boxTile.x-1, boxTile.y);
                this.player.x -= this.player.dx;
                this.player.anims.play('left', true);
            }
        } else {
            // BOXとは衝突していない
            if (tile === null) {
                // タイルもないので移動可能
                this.player.x -= this.player.dx;
                this.player.anims.play('left', true);
            }
        }
    } else if (this.input.keyboard.checkDown(this.cursors.right, 100)) {
        const tile = this.groundLayer.getTileAtWorldXY(this.player.x + this.player.dx, this.player.y);
        const tile2 = this.groundLayer.getTileAtWorldXY(this.player.x + this.player.dx * 2, this.player.y);
        const boxTile = this.boxLayer.getTileAtWorldXY(this.player.x + this.player.dx, this.player.y);
        const boxtile2 = this.boxLayer.getTileAtWorldXY(this.player.x + this.player.dx * 2, this.player.y);
        if (boxTile !== null) {
            // BOXと衝突
            if( tile2 === null && boxtile2 === null) {
                // 移動後のタイルも空なので移動
                this.boxLayer.removeTileAt(boxTile.x, boxTile.y);
                this.boxLayer.putTileAt(boxTile, boxTile.x+1, boxTile.y);
                this.player.x += this.player.dx;
                this.player.anims.play('right', true);
            }
        } else {
            // BOXとは衝突していない
            if (tile === null) {
                // タイルもないので移動可能
                this.player.x += this.player.dx;
                this.player.anims.play('right', true);
            }
        }
    } else if (this.input.keyboard.checkDown(this.cursors.up, 100)) {
        const tile = this.groundLayer.getTileAtWorldXY(this.player.x, this.player.y - this.player.dy);
        const tile2 = this.groundLayer.getTileAtWorldXY(this.player.x, this.player.y - this.player.dy * 2);
        const boxTile = this.boxLayer.getTileAtWorldXY(this.player.x, this.player.y - this.player.dy);
        const boxtile2 = this.boxLayer.getTileAtWorldXY(this.player.x, this.player.y - this.player.dy * 2);
        if (boxTile !== null) {
            // BOXと衝突
            if( tile2 === null && boxtile2 === null) {
                // 移動後のタイルも空なので移動
                this.boxLayer.removeTileAt(boxTile.x, boxTile.y);
                this.boxLayer.putTileAt(boxTile, boxTile.x, boxTile.y - 1);
                this.player.y -= this.player.dy;
                this.player.anims.play('up', true);
            }
        } else {
            // BOXとは衝突していない
            if (tile === null) {
                // タイルもないので移動可能
                this.player.y -= this.player.dy;
                this.player.anims.play('up', true);
            }
        }
    } else if (this.input.keyboard.checkDown(this.cursors.down, 100)){
        const tile = this.groundLayer.getTileAtWorldXY(this.player.x, this.player.y + this.player.dy);
        const tile2 = this.groundLayer.getTileAtWorldXY(this.player.x, this.player.y + this.player.dy * 2);
        const boxTile = this.boxLayer.getTileAtWorldXY(this.player.x, this.player.y + this.player.dy);
        const boxtile2 = this.boxLayer.getTileAtWorldXY(this.player.x, this.player.y + this.player.dy * 2);
        if (boxTile !== null) {
            // BOXと衝突
            if( tile2 === null && boxtile2 === null) {
                // 移動後のタイルも空なので移動
                this.boxLayer.removeTileAt(boxTile.x, boxTile.y);
                this.boxLayer.putTileAt(boxTile, boxTile.x, boxTile.y + 1);
                this.player.y += this.player.dy;
                this.player.anims.play('down', true);
            }
        } else {
            // BOXとは衝突していない
            if (tile === null) {
                // タイルもないので移動可能
                this.player.y += this.player.dy;
                this.player.anims.play('down', true);
            }
        }
    } else {
        this.player.anims.stop();
    }
};

実行結果

Tilemapを利用したマップを表示します。プレイヤーはマップ内を移動します。プレイヤーはgroundLayerと衝突します。プレイヤーは、boxLayerのタイルを押すことができます。boxLayerのタイルの移動後の座標にgroundLayer、boxLayerのタイルが存在する場合、押すことができません

最終的に実現したコード

https://github.com/hiroshees/phaser-game-sample/blob/main/src/work09.html

Discussion