Phaser3 Loaderプラグイン

phaser

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.jsonscene.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 で待機する。

以上のことを簡単にまとめると、

  1. loaderPlugin.addFileによってFileオブジェクトがloaderPluginに追加される
  2. 追加されたFileオブジェクトはfile.loadによって通信を開始する
  3. 通信が終了したFileオブジェクトは自分でloaderPlugin.nextFile関数を叩いて成功、失敗後の処理を行う
  4. 成功したFileオブジェクトはキャッシュに登録するデータをfile.onProcess関数内で生成する。生成されたオブジェクトは通常file.dataにセットしておく
  5. 通信が終わり、データの加工処理(通信に成功した場合)も終了したFileオブジェクトは、file.addToCache関数でキャッシュへの登録処理を行う
  6. 以上すべての処理が終わったら、Fileオブジェクトは削除リストに追加され、最後にまとめてdestroyされる

という流れになっている。

で、結局のところ独自ファイルのロード機能を作るにはどうすればよいのか

  1. Phaser.Loader.Fileクラスを継承する
  2. load関数に通信開始処理を書く。この時、通信が終わったら loaderPlugin.nextFile 関数を叩く処理をいれること
  3. onProcess関数に通信したデータを元にキャッシュに登録するデータを生成する処理を書く。生成したデータはdataフィールドにセットしておけばデフォルトであればaddToCacheのタイミングでconfigで指定したキャッシュに指定したkeyで登録される
  4. scene.load.XXX と書けるように、FileTypeManagerに関数を登録する。登録する関数が行うのは、Fileオブジェクトを生成してloaderPlugin.addFile関数でLoaderPluginにFileオブジェクトを追加する処理を書かないといけない

である。

以上。実際難しいので必要な部分はphaserの仕組みを無視したオレオレ実装をしてもよいと思うが、フレームワークの仕組みに沿ってスマートにやりたいなら挑戦する必要がある。