Phaser3 タイルマップ アニメーション
Posted
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
が用意されている。
リファレンスを読めばそこに書いてあるが、以下のように特徴に違いがある。
- StaticLayer : 描写スピードを重視、一度生成してしまうとalpha値を随時変えるなどの柔軟な対応は出来ない。
- DynamicLayer : 様々な操作を生成後にも出来るが、描写スピードを犠牲にしている。
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的には外から叩く関数ではないということなんだろう。
対して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)
は…
- Static,DynamicLayerになる前のレイヤーデータオブジェクト
Array.<Phaser.Tilemaps.LayerData>
- レイヤーデータオブジェクト、他設定値を保持するマップデータオブジェクトPhaser
Phaser.Tilemaps. MapData
- マップデータオブジェクトを保持するタイルマップオブジェクト
Phaser.Tilemaps. 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が重複しないようにしてやる。
gidの設定について
gid
は任意の値を設定できる。なので、細かい調整をせずに1000
や2000
にしてしまって良い。こうしておけば、“千の位の値はタイルセット固有のIDで、それ以下がそのタイルセットに含まれる各タイル固有のID"のようにして素材を管理できる。
実際には、1000
や2000
とかではなくて(それでも良いけど)、最下位2byteが各タイルセットの中のID,3byteめがタイルセット自体のIDのように、number
をバイト単位で区切って管理したほうがかっこいいと思う。
これは、gid
に限らず、たとえばdepth
を管理するときなどにも使える。MAX_SAFE_INTEGER
の範囲で管理するとして、これを書いている時点で(bit単位まで"かつかつ"で領域を使わないとして)6byte
の領域が使えるのだから、例えば2byte
ずつで分割して"大分類、中分類、小分類"として値を扱うことも出来る。ただのnumber
に情報が入れば色々と便利。