Phaser3 タイルマップ アニメーション

phaser

Phaser3 タイルマップ アニメーション

前回の記事

https://mijinc0.github.io/blog/post/20200904_phaser3_animation/

タイルマップの記事

https://mijinc0.github.io/blog/post/20200821_phaser3_tilemap/

概要

海や滝などのマップチップや鉱物がキラキラ光る鉱山のマップチップなど、マップチップにアニメーションを付けたいことはある。

結論から言うとPhaser3のデフォルトの機能でタイルマップのタイルにアニメーションを付けるものはない。なので自分で作る。自分で作るということは、マップ、タイル、レイヤーのプロパティを理解しないといけないということだ。まずはそこから。

準備: StaticLayerとDynamicLayer

https://photonstorm.github.io/phaser3-docs/Phaser.Tilemaps.Tilemap.html

Phaser3のTilemapクラスにはPhaser.Tilemaps.StaticTilemapLayerを作るcreateStaticLayeと、Phaser.Tilemaps.DynamicTilemapLayerを作るcreateDynamicLayerが用意されている。

リファレンスを読めばそこに書いてあるが、以下のように特徴に違いがある。

https://photonstorm.github.io/phaser3-docs/Phaser.Tilemaps.StaticTilemapLayer.html
https://photonstorm.github.io/phaser3-docs/Phaser.Tilemaps.DynamicTilemapLayer.html

StaticLayerは一度生成してしまうと、例えば中のタイルのindexを変更しても タイルのプロパティは変更されるが描写に反映されない という挙動をする。これは、詳しく見ていないがタイルマップを一つのオブジェクトとして管理して描写している(描写毎に各タイルの情報を確認せずに保持されたオブジェクトを描写している)からだと思われる。

実際は、下記参照の場所にある通りプロパティ変更後にstaticLayer.updateVBOData()をしてやればVBO(頂点バッファオブジェクト)の情報が更新されて描写を変更できるが、リファレンスに登場しない関数なのでPhaser3的には外から叩く関数ではないということなんだろう。

https://github.com/photonstorm/phaser/blob/v3.22.0/src/tilemaps/staticlayer/StaticTilemapLayer.js#L364

対してDynamicLayerは生成後に各タイルのプロパティを変更するとそれが次の描写に反映される。今回はアニメーションをさせたいので、こっちを使う。 先に結論を言えば、DynamicLayerの各タイルのindexを時間経過によって変化させれば、アニメーションするタイルマップが出来上がる ことになる。

処理速度を犠牲にするのを嫌うのであれば、基本はStaticLayerで描写して、必要な場所だけDynamicLayerを使うと良いだろう。

タイルをアニメーションさせる

タイルをアニメーションさせる具体的な方法は、要するにupdateループの中に目的のタイルのindexを変化させるルーチンを仕込めば良いことなり、その方法は沢山ありすぎるので、ここでは書けないが、どこかに以下のような雰囲気の処理を入れる。
(以下のコードは疑似コードを書いているぐらいに雰囲気で書いているので雰囲気だけ参考にしてください)

update(time: number, delta: number): void {
  this.elaspedTime += delta;

  const currentId = this.getCurrentTileId();
  const nextId = this.getNextTileId(curentId, this.elaspedTime);

  if (currentId !== nextId) {
    // なんと指定インデックスのタイルのインデックスを全て変えてしまう便利な関数が用意されている
    this.dynamicTilemapLayer.replaceByIndex(currentID, nextId);
  }
}

アニメーションさせるだけであればコレでOK、素材の規格をしっかり決めて、変なタイルが描写されたりしないようにindexを上手いこと調整してやればよいことになる。

とはいっても、実際それをするためには “アニメーションするタイルセットと静的なタイルセットを別々の素材、別々のオブジェクトとして管理したい” などの要望が出ると思う。以降、それらを管理するために必要になるであろう知識を書いていく。アニメーションの話は一旦ここで終わり。

Tilemap,Tileset,Layerってどうやって生成されるのという話

実際、どうやってTilema,Tileset,Layer(Static or Dynamic)が生成されるのという話は複数パターンがある。というのも、Phaser3がタイルマップの生成にライブラリの利用者がTiledと呼ばれるフリーのマップエディタを利用することを想定しているからだ。

Tiledが出力したマップデータ(基本はjson)のフォーマットに従って便利にタイルマップを生成できるようにしたい、しかし、もっと原始的な(例えば生のマップデータnumber[][]を使って生成する)方法にも対応したい、ということで、複数のパターンがある。

今回はTiledを使うことは想定しないので、number[][]をレイヤーの生データとして使うものとして以降書いていく。

リファレンスなどの情報に従ってレイヤーを作ると、以下のようにして作ることになると思う。

// this === Phaser.Scene
const layerData = [
  [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ],
  [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ],
  [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ],
  [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ],
  [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ],
];

const tilemap = this.make.tilemap({
  data: layerData,
  tileWidth: 40,
  tileHeight: 40,
  width: 10,
  height: 5,
});

const tileset = tilemap.addTilesetImage('キャッシュに登録されたtilesetの画像のkey');

const layer = tilemap.createDynamicLayer(0, tileset);

何のことはないが、なんのことがなさすぎて何をやっているのか謎だ。なので、上から順番に見ていくことにする。

tilemapの生成

const tilemap = this.make.tilemap({
  data: layerData,
  tileWidth: 40,
  tileHeight: 40,
  width: 10,
  height: 5,
});

この時点で、TilemapCreator(this.make.tilemap)は…

を生成している。要するに色々生成している。

ここで意識しておけばよいのは、 生のデータは分かっている(各タイルと紐づけされるタイルセットのindexは分かっている) が、肝心のタイルセットの情報がない。 ということ。

余談だがタイルマップオブジェクトは専用のキャッシュが用意されていて、グローバルな空間に貯めておくことが出来る。何度も生のデータから作らなくても、タイルマップのデータを貯めておいて必要な時に取り出して createStatic(Dynamic)Layer ですぐにレイヤーが作れるようになっている(必要であれば)。

tilesetの生成

const tileset = tilemap.addTilesetImage('キャッシュに登録されたtilesetの画像のkey');

次はこの部分。

上記で、tilemapにはまだタイルセットの情報が無い、と書いた。タイルセットはaddTilesetImageによって生成され、生成されたタイルセットはtilemap内に保管される。複数個生成して、あとから取り出したければ取得用の関数があるのでそれを使って取り出す。上記の例では、戻値が生成されたタイルセットになっているのでそれを直接使っている。

ここで、“どのid(index)のタイルを割り当てるか"は分かっていたが、肝心のタイルセットが無かったtilemapに、待望のタイルセットが用意された。あとは、これらを組み合わせて実際に描写されるレイヤーを作れば良い、となる。

layerの生成

const layer = tilemap.createDynamicLayer(0, tileset);

最後にこの部分、以前の記事でも書いたが上記のようにこの手順によって生データからレイヤーオブジェクトを生成するときには常に第一引数のidは0になる。これは、tilemapオブジェクトを生成した時に渡した生データを指定しているのだと思ってもらって良い。

自分で複数のレイヤーデータオブジェクトをマップデータに持たせた場合には他のidも使えるが、そういうい人はもうこの記事を読む必要自体ないだろう。

指定したレイヤーデータオブジェクトの各タイルに、タイルセットの情報を渡してあげて、レイヤーオブジェクトを生成して完了となる。

ちなみに、タイルセットは配列にして複数渡すことも可能。この時、gidを調整して各タイルセットに含まれる各タイルのidが重複しないようにしないといけないので、次はgidについて説明する。

gidについて

各タイルセットに含まれるタイルは、一番左上のタイルが0,その一つ右が1…と順番にidが付くように鳴っているが、最初のidを0ではない値に設定することが出来る。その設定値がgidと呼ばれる。例えば、gid === 4であれば、そのタイルセットの一番左上のタイルは4,その右が5…と番号が振られていく。(gidは"基準ID"という意味だと思う)

このgidは、レイヤーオブジェクトを生成する時に、複数のタイルセットを指定した時に、タイルセットのidが重なってしまうのを防ぐ役割を持つ。

以下のような状況があったとする。

const tilesetA = tilemap.getTileset('tilesetA');
const tilesetB = tilemap.getTileset('tilesetB');
const tilesetC = tilemap.getTileset('tilesetC');

tilemap.createDynamicLayer(0, [tilesetA, tilesetB, tilesetC]);

この時、タイルセット内のタイルのidが重複した時、配列の後ろにあるタイルセット(上記の場合だとtilesetC)が優先されて描写される。

これでは、せっかく複数指定できるのにあまりに扱いづらい。ということで、gidを各タイルセットに設定して、idが重複しないようにしてやる。

draw1

gidの設定について

gidは任意の値を設定できる。なので、細かい調整をせずに10002000にしてしまって良い。こうしておけば、“千の位の値はタイルセット固有のIDで、それ以下がそのタイルセットに含まれる各タイル固有のID"のようにして素材を管理できる。

実際には、10002000とかではなくて(それでも良いけど)、最下位2byteが各タイルセットの中のID,3byteめがタイルセット自体のIDのように、numberをバイト単位で区切って管理したほうがかっこいいと思う。

draw1

これは、gidに限らず、たとえばdepthを管理するときなどにも使える。MAX_SAFE_INTEGERの範囲で管理するとして、これを書いている時点で(bit単位まで"かつかつ"で領域を使わないとして)6byteの領域が使えるのだから、例えば2byteずつで分割して"大分類、中分類、小分類"として値を扱うことも出来る。ただのnumberに情報が入れば色々と便利。