Phaser3 Loaderプラグイン
Posted
Phaser3 Loaderプラグイン
version : 3.52.0
Phaser3を使っていて、画像やサウンドなどのアセットを外部からロードするとき、Phaser3のチュートリアル通りにコードを書いていると、各シーンの preload
関数に、たとえば以下のように書いてアセットをロードすることになると思う。
// this === Phaser.Scene
preload(): void {
// loadはPhaser.Loader.LoaderPlugin
this.load.json(...args);
this.load.spritesheet(...args);
}
通常はこれで良い。が、いろいろとやっていくとPhaser3のプリセットにないファイル形式のファイルを自分でロードしたくなることがある。例えば以下のような場合。
多言語対応したが、全ての言語で同じフォントを使うわけにはいかないし、かといって 全ての言語用のフォントを(必要ないものも含めて)全てロードしてしまうのも乱暴。 出来れば、“言語設定” => “loacleデータから必要なフォントを把握” => “必要なフォントを必要になったタイミングで外部からロードする” というのができれば理想。
もちろん、目的が同じでも様々な実装が可能なので上記の例だと無理にLoaderプラグインに独自のファイルタイプを追加する必要はないかもしれないが、それを使うのが"スマートである"場合があれば、やはりそうしたいところ。
Phaser LoaderPlugin周りのクラス
カスタムクラスを作ってLoaderPluginの機能を拡張するのに必要になる、必要な部分だけ説明。 おそらくこれだけ読んでも何が何だかわからないと思うので、この後に書く “ファイルがロードされる処理の流れ” の項と行ったり来たりしながら読む必要があると思う。
class Phaser.Loader.LoaderPlugin
各シーンで loader
にアクセスするとこの LoaderPlugin
オブジェクトを操作することになる。このプラグインは、この後に説明する Loader.File
クラスのオブジェクトを操作してどのファイルオブジェクトが通信しているのか、通信が完了してその後の処理(通信によって得た生データをPhaserオブジェクトに加工するステップや加工されたデータをキャッシュに登録するステップ)をすべきファイルオブジェクトはどれか、などを仕分けしてそれぞれに必要な処理を行う。
このクラスのオブジェクト内には 開始前 , データ通信中 , データ通信後 のステップ用に3つのリストを持っている。それぞれ list (開始前)
, inflight (データ通信中)
, queue (データ通信後)
である。LoaderPluginがFileオブジェクトをこの3つのリストに仕分けしてFileオブジェクトを管理していることはまず最初に覚えておく必要がある。
class Phaser.Loader.File
実際に外部と通信する処理、通信して得た生データを加工する処理、加工してゲーム内で使えるようになったデータをキャッシュに登録する処理などの具体的な処理はこの File
クラスに書かれている。
その中で、LoaderPluginの関数をFileクラス内から叩いてLoaderPluginに各ステップの完了を知らせる必要があるので、このFileオブジェクトとLoaderPluginがどのように連携して一連のロード処理を行っているのか理解しないと、カスタムタイプのFileを扱うことは難しい。
カスタムタイプのFileクラスを作るときには、原則としてこのクラスを継承して作ることになる。
class Phaser.Loader.FileTypeManager
scene.load.json
や scene.load.spritesheet
などの関数をLoaderPluginオブジェクトに生やすのはこの FileTypeManager
の仕事。それ以外は特に何もしていない。
FileTypeManagerによってLoaderPluginに登録される関数は、その内部で必要なタイプのFileオブジェクトを生成してLoaderPluginの addFile
関数でLaderPluginに追加する処理が書かれていることを期待している。
ファイルがロードされる処理の流れ
まず最初に、あるシーンのpreload関数内で scene.load.json(...args)
が呼び出されたとする。これをサンプルに処理の流れを追っていく。
まず最初にシーンで scene.load.json(...args)
が呼び出されると、scene.load.json
関数は以下のような処理を行ってLoaderPluginにFileオブジェクトを生成して追加する。
if (Array.isArray(key)){
// 指定のkeyが配列だった場合はJSONFileオブジェクトを必要な分だけ生成して
// addFileでLoaderPluginに追加する
for (var i = 0; i < key.length; i++){
loaderPlugin.addFile(new JSONFile(/* 引数は省略 */));
}
} else {
// 指定のkeyが単一キーだった場合はJSONFileオブジェクトを一つだけ生成して
// addFileでLoaderPluginに追加する
loaderPlugin.addFile(new JSONFile(/* 引数は省略 */));
}
LoaderPluginの addFile
関数によって追加された File
オブジェクト(ここでは JSONFile
オブジェクト)は、LoaderPlugin内の list
と呼ばれるロード開始前のFileオブジェクトが入るリストに入れられる。
addFile
関数によって laderPlugin.list
に入れられたFileオブジェクトは、次のフレームにLoaderPluginがupdateされることによって(checkLoadQueue関数を介して) load関数が呼ばれる。
Fileオブジェクトのload関数には、各形式のファイルのデータを通信して得る処理が書かれていなければならない。
Phaser.Loader.File
クラスのload関数にはXHRLoaderによって外部からデータをロードする処理が書かれているので、もしその機能で十分であればその機能をそのまま使えばよいが、もし、独自のファイルタイプのロードを必要とするのであれば、場合によってはこのload関数を継承したクラスでオーバーライドして必要な通信処理を書く必要がある。
また、この時Fileオブジェクトは LoaderPlugin内の list
から inflight
に移動する。
file.load
によって通信が開始されて、それが終了した段階で、通信に成功すれば file.onLoad
関数が、失敗すれば file.onError
が呼ばれる。それぞれ成功時、失敗時に必要な処理を行った後、Fileオブジェクトは通信の終了をLoaderPluginに知らせるために loaderPlugin.nextFile
関数を叩く。(この時、成功、失敗を引数で渡してLoaderPluginに知らせている)
loaderPlugin.nextFile
によって通信が終了したことをLoaderPluginが知ると、nextFile
関数内部で inflight
から queue
に移される。この過程で、もし通信に成功していれば file.onProcess
が呼ばれる。Fileオブジェクトはこの onProcess
関数が呼ばれると、次の addToCache
に備えて file.data
にキャッシュに追加するオブジェクトを用意する。
loaderPlugin.nextFile
が呼ばれ、 file.onProcess
によってキャッシュに登録するデータを準備したFileオブジェクトがLoaderPlugin内の inflight
から queue
に移されると、最後のステップに進む。
loaderPlugin.queue
に移動したFileオブジェクトは、通信に成功していると addToCache
によってデータをキャッシュに登録する。繰り返しになるが、キャッシュに登録されるデータは通常であれば file.data
にセットされたデータである。これをキャッシュに登録すれば、一連のロード処理が終わり、Fileオブジェクトは queue
からも削除され、破壊処理まで _deleteQueue
で待機する。
以上のことを簡単にまとめると、
- loaderPlugin.addFileによってFileオブジェクトがloaderPluginに追加される
- 追加されたFileオブジェクトはfile.loadによって通信を開始する
- 通信が終了したFileオブジェクトは自分でloaderPlugin.nextFile関数を叩いて成功、失敗後の処理を行う
- 成功したFileオブジェクトはキャッシュに登録するデータをfile.onProcess関数内で生成する。生成されたオブジェクトは通常file.dataにセットしておく
- 通信が終わり、データの加工処理(通信に成功した場合)も終了したFileオブジェクトは、file.addToCache関数でキャッシュへの登録処理を行う
- 以上すべての処理が終わったら、Fileオブジェクトは削除リストに追加され、最後にまとめてdestroyされる
という流れになっている。
で、結局のところ独自ファイルのロード機能を作るにはどうすればよいのか
- Phaser.Loader.Fileクラスを継承する
- load関数に通信開始処理を書く。この時、通信が終わったら
loaderPlugin.nextFile
関数を叩く処理をいれること - onProcess関数に通信したデータを元にキャッシュに登録するデータを生成する処理を書く。生成したデータはdataフィールドにセットしておけばデフォルトであればaddToCacheのタイミングでconfigで指定したキャッシュに指定したkeyで登録される
scene.load.XXX
と書けるように、FileTypeManagerに関数を登録する。登録する関数が行うのは、Fileオブジェクトを生成してloaderPlugin.addFile関数でLoaderPluginにFileオブジェクトを追加する処理を書かないといけない
である。
以上。実際難しいので必要な部分はphaserの仕組みを無視したオレオレ実装をしてもよいと思うが、フレームワークの仕組みに沿ってスマートにやりたいなら挑戦する必要がある。