本文へスキップ

Phaser ゲームの中で spryte を使う ​

Phaser 4 のゲームは、今あるものをすべて残したまま、一番重い描画(弾、パーティクル、群れ)だけを spryte に任せられます。spryte は Phaser のフレームの中で、指定した深度に、同じ WebGL2 コンテキスト上で描きます。重ねるキャンバスも、テクスチャの二重持ちも不要で、Phaser のオブジェクトを spryte の描画の手前に置くこともできます。spryte 0.4 以降と Phaser 4(4.2.1 で検証)が必要です。

ts
import Phaser from "phaser";
import { createPhaserContext, spryteExtern } from "spryte/phaser";

class Main extends Phaser.Scene {
  create() {
    // Phaser が深度 0 に背景を、深度 2 に HUD を描き…
    this.add.image(400, 225, "background").setDepth(0);
    this.add.text(12, 12, "score 0").setDepth(2);

    // …その間に spryte が弾を描く。
    const extern = this.add.extern().setDepth(1);
    spryteExtern(extern, (draw) => {
      draw.sprites(bullet, positions, count);
    });
  }
}

new Phaser.Game({
  type: Phaser.WEBGL,
  ...createPhaserContext(), // WebGL2 コンテキスト。Phaser 自身は WebGL1 を作る
  width: 800,
  height: 450,
  scene: Main,
});

仕組み ​

  1. createPhaserContext() は WebGL2 コンテキストを作り、ゲーム設定に展開する { canvas, context } を返します(type: Phaser.WEBGL と一緒に使います)。Phaser はデフォルトでは WebGL1 コンテキストを作りますが、spryte には WebGL2 が必要で、Phaser は WebGL2 の上でも動きます。第 2 引数でコンテキストの属性を指定でき、デフォルトは Phaser 自身のもの(不透明、アンチエイリアスあり、premultiplied、ステンシルバッファあり)です。ゲームにすでにキャンバスがあるなら、第 1 引数に渡してください。
  2. spryteExtern(extern, render) は Extern の render を設定します。毎フレーム、Phaser の描画順の中で render(draw) が呼ばれます。その Draw の変換はすでに Phaser のもの、つまりカメラ(スクロール、ズーム、回転)と、Extern が入っている Container の分が適用済みです。座標は Phaser が使うものと同じで、draw.sprites には Phaser のエミッターならオブジェクトが必要だった型付き配列をそのまま渡せます。
  3. spryte は、Phaser が想定していない状態を残しません。 Phaser は Extern のあとに自分の状態をリセットし、spryte は Phaser の描画コンテキストの下で描きます。つまり Phaser のフレームバッファへ、Phaser のシザーとステンシルの下で描きます。

Draw は最初のフレーム以降に存在します。spryteExtern は { draw, dispose } を返し、dispose は Extern が破棄されると自動で呼ばれるので、シーンの終了で spryte の GPU オブジェクトも解放されます。

そのまま効くもの ​

spryte は Phaser の描画コンテキストに、Phaser の状態の下で描くので、Phaser の機能がそのまま効きます。

  • フィルター。 extern.enableFilters() と extern.filters.internal.addBlur(...) は spryte が描いたものにかかります。カメラのフィルターにも含まれます。
  • カメラのビューポート。 複数のカメラ(ミニマップ、画面分割)があれば、spryte はそれぞれのカメラのビューポートの中に、そのカメラの変換で描きます。
  • Stencil マスク。 Extern より前の Stencil ゲームオブジェクトは、Phaser のオブジェクトと同じように spryte の描画をマスクします。
  • 深度。 Extern の setDepth で、spryte の描画を表示リストのどこにでも置けます。深度が複数必要なら Extern を複数使います。それぞれに render があります。

効かないもの:Extern 自身の alpha、tint、blendMode プロパティです。Phaser のドキュメントにあるとおり、Extern では使われません。透明度、色、ブレンドは draw の呼び出し側で指定してください。

シェーダーシムが必要な理由 ​

Phaser のシェーダーは GLSL ES 1.00 で書かれています。WebGL2 コンテキストでは、Phaser 4.2.1 の 3 つの機能がコンパイルに失敗し、ゲームの描画が止まります。Gradient ゲームオブジェクト、noiseNormalMap を使うノイズオブジェクト、そしてレンダー設定の smoothPixelArt です。これらはスクリーン空間の微分(dFdx)を使っており、ES 1.00 では OES_standard_derivatives 拡張から来ますが、ES 3.00 には微分が組み込まれているため WebGL2 にはこの拡張がありません。

createPhaserContext() は shimShaders(gl) を組み込みます。これはコンテキストがコンパイルするすべての ES 1.00 シェーダーを ES 3.00 に変換します。そのため、これらの機能は WebGL1 と同じように描画されます(spryte のテストにおいてピクセル単位で確認済み)。自作の ES 1.00 シェーダーも同じように変換され、すでに #version 300 es を宣言しているシェーダーはそのまま通ります。変換では ES 3.00 で予約語になった識別子(sample、smooth、texture など)をリネームし、uniform と attribute の検索でも同じリネームを行います。コンテキストを自分で作りたい場合は、new Phaser.Game の前に shimShaders(gl) を呼んでください。

ヒント ​

  • 一括の状態は型付き配列に。 draw.sprites(frame, positions, count, options, { scale, rotation, color }) はそれらをそのままアップロードします。弾ごとの JavaScript も、プールするオブジェクトもありません。Phaser のエミッターは最初から多くのこと(放出ゾーン、重力、寿命)をしてくれるので、spryte ではその挙動は自前のシミュレーションを数行書くことになります。一括スプライトとパーティクルを参照してください。
  • テクスチャは自由。 spryte は HTMLImageElement、キャンバス、ImageBitmap を描けます。Phaser が読み込んだ画像なら this.textures.get(key).getSourceImage() で取り出せます。
  • 単位は Phaser のピクセル。 render が呼ばれる時点で、描画コンテキストのサイズでの begin(null, width, height, 1) は済んでいます。テキストは Phaser 自身と同じく、1 単位 1 ピクセルでラスタライズされます。
  • レンダーターゲットとシェーダーも中で使えます。 draw.into、draw.createShader、withShader はすべて Extern のスロット内で動きます。ターゲットとシェーダーは render の外で一度だけ作り、使い回してください。
  • Phaser 3 の Extern はコールバックと状態の扱いが異なります。サポートしていません。

計測する ​

2dbench の particles シーン(すべてのプロパティが変化、スプライト 5 万個、RTX 5060 搭載ノート PC、5 ラウンド)で測りました。Phaser の Extern の中の spryte の draw.sprites は 1 フレーム 1.4 ms、Phaser の SpriteGPULayer は 7.1 ms でした。スプライトごとに呼ぶ draw.sprite は 6.9 ms、Image は 17.8 ms です。spryte 単体のキャンバスは 1.3 ms なので、Phaser の中にいるコストは 0.1 ms、計測の誤差の範囲です。生データ付きの数値は Phaser の中の spryte にあります。自分のゲームで測るには、Extern が描く場合と描かない場合のフレーム時間を比べるか、演出を 1 つ移して比較してください。

関連:レンダラー全体を移すなら Phaser からの移行、一括スプライトとパーティクル、API リファレンス。