spryte vs Phaser
2dbench の同じマシン・同じシーンで、spryte は動くスプライトを Phaser 4 の Image オブジェクトより 2.2〜3.1 倍速く、バルク描画の手段より 5〜11 倍速く描きます。JavaScript の量は約 24 分の 1(起動時 11.3 kB 対 276.6 kB)です。計測の前に、すべてのフレームがピクセル単位で一致することを確認しています。spryte はレンダラーだけで、Phaser はフレームワーク全体なので、これは描画の比較であってフレームワークの比較ではありません。Phaser のほうが向いている場面は最後にまとめています。
結果
RTX 5060 搭載ノート PC(Ryzen 7 260)、Chrome 155、AC 電源、シーンごとに交互実行で 5 ラウンド。シミュレーションから GPU 完了までのフレーム時間の中央値です。spryte 0.2.1、Phaser 4.2.1、参考として PixiJS 8.22.0。生データ:results/community/*-rtx-5060-laptop-phaser-*.json。
| 負荷、スプライト 5 万個 | spryte | Phaser 4 | PixiJS 8 | spryte / Phaser |
|---|---|---|---|---|
Bunnymark、位置のみ:draw.sprite / Image / Sprite | 4.8 ms | 15.0 ms | 13.5 ms | 3.1 倍 |
| Mixed:テクスチャ 4 種、回転、拡大縮小、色、透明度 | 7.4 ms | 20.0 ms | 23.0 ms | 2.7 倍 |
| Particles:すべてのプロパティが変化 | 6.9 ms | 19.9 ms | 21.4 ms | 2.9 倍 |
Particles、バルク:draw.sprites / SpriteGPULayer / ParticleContainer | 1.9 ms | 9.5 ms | 7.0 ms | 5.0 倍 |
バルクで 20 万個になると差は広がります。位置だけが動くバニーでは 2.7 ms 対 SpriteGPULayer の 23.2 ms、Blitter の 29.9 ms(8.6〜11 倍)、すべてが動くパーティクルでは 6.2 ms 対 35.2 ms(5.7 倍)です。
60 fps で描けるスプライト数
| シーン | spryte | Phaser 4 |
|---|---|---|
| bunny、スプライトごとに1オブジェクトまたは1呼び出し | 100,000 | 50,000 |
| mixed | 100,000 | 20,000 |
| particles | 100,000 | 20,000 |
bunny、バルク(Blitter または SpriteGPULayer) | 800,000 | 100,000 |
particles、バルク(SpriteGPULayer) | 400,000 | 50,000 |
| raycaster(敵の数) | 10,000 | 5,000 |
数は最大 2 倍刻みの段階で測っているので、下限値です。raycaster(数千のテクスチャの帯で描く一人称視点の迷路)では、敵 1 万体のフレームが spryte 10.1 ms、Phaser 22.3 ms(2.2 倍)、PixiJS 37.3 ms でした。
Phaser の良いところも書いておきます。Phaser の Image は mixed と particles で PixiJS の Sprite に勝ち(5 万個で 20.0 ms 対 23.0 ms、19.9 対 21.4)、bunnymark でもわずかな差でした。raycaster では大きく上回っています。「PixiJS は速く、Phaser は重い」というよくある思い込みは、スプライトごとの描画については当てはまりません。
ダウンロードサイズ
回転したスプライト1枚を描くアプリ、Vite 8 の本番ビルド、brotli 品質 11(bench/size):
| 起動時の JS | |
|---|---|
| spryte 0.3.0 | 11.3 kB |
| PixiJS 8.22 | 87.8 kB |
| Phaser 4.2.1 | 276.6 kB |
Phaser の npm エントリはすべての機能を含むビルド済みバンドル1つなので、バンドラーでツリーシェイキングできません。カスタムビルドならもっと小さくなりますが、標準の使い方ではありません。また、そのサイズの大半は spryte が持たないもの、つまり物理、入力、オーディオ、トゥイーン、ローダー、シーンです。
差が生まれる理由
Phaser の Image はゲームオブジェクトです。Phaser は毎フレーム表示リストをたどり、トランスフォームを更新し、カリングし、バッチ処理します。spryte はオブジェクトを持ちません。draw.sprite の呼び出しは型付き配列にいくつかの数値を書くだけで、draw.sprites は位置の Float32Array をそのままアップロードし、スプライトごとの JavaScript はまったく走りません。
Phaser のバルク描画には、それぞれ制約があります。Blitter は位置だけの Bob を描き、回転と拡大縮小はできません。SpriteGPULayer は回転、拡大縮小、色を扱えますが、めったに変わらないメンバーのための仕組みです。ドキュメントにも頻繁な編集は高コストとあり、毎フレーム全メンバーを書き換えるとバッファ全体が再アップロードされます。動くパーティクルのシーンではそれが必要なので、ここでの数字は Phaser で使える手段の公平な評価であって、SpriteGPULayer の本来の用途の評価ではありません。patchMember で生のワードを書けば速くなるかもしれません。まだ計測していないので、それを試す 2dbench への PR を歓迎します。
計測方法
- 同じ処理: シード付きの共通シミュレーションと同じテクスチャ。960×540、ニアレストフィルタ、アンチエイリアスなし、整数ピクセルへの丸め(Phaser は
pixelArt: true、roundPixels: true)。 - まずピクセル一致: スプライト 2 千個と 5 万個で、各アダプターのフレームを基準と比較します。Phaser はすべてのシーンでスプライト内部の差が 0% で、縁の差も spryte と一致しました。
- Phaser はゲームと同じように動かす: Phaser 自身のループを止め、ハーネスが毎フレーム
game.step()を呼びます。これは描画に加えてシーンの更新リストとカメラも処理します。PixiJS にはない小さなコストですが、Phaser のゲームはすべてこれを払っています。 - WebGL1: Phaser 4 のレンダラーは WebGL1 コンテキストを作り、WebGL2 を選ぶ設定はありません。spryte は WebGL2 を使います。
Phaser アダプターは公開しています。もっと速い正攻法を知っている Phaser ユーザーからの提案を歓迎します。
この結果からわからないこと
- 1台のマシンだけ。 これは RTX 5060 搭載ノート PC 1台の数字です。spryte の PixiJS との基準結果と同じマシンですが日が違うので、ミリ秒ではなく同じ実行内の比率で比べてください。同じノート PC の内蔵 GPU(Radeon 780M)では差は小さめでした(2.0〜2.3 倍と 3.0〜4.6 倍、生データ)。お手元のデバイスで 2dbench を実行できます。
- 静的なシーン。 差は毎フレームのスプライトごとの処理から生まれます。動くものが少なければ縮まります。
- 描画以外のすべて。 物理、入力、オーディオ、シーン管理は計測しておらず、spryte はそれらを持ちません。
- Phaser でまだ計測していないもの: 2dbench のタイルマップ、churn、クリッピング、アップロード、テキストのシーン。
Phaser を選ぶべきとき
すべてをこなすフレームワークが欲しいなら Phaser です。Arcade と Matter の物理、トゥイーン、ローダー、シーン、入力、オーディオ、ビジュアルエディタ、そして長年の事例があります。ほとんどのゲームはスプライト数で詰まることはなく、Phaser のスプライトごとの描画も十分速いです。
描画がボトルネックなら(弾幕、パーティクル、群衆、大きなタイルマップ)、ダウンロードサイズが大事なら、あるいは自分のコードや debuff の下にレンダラーを置きたいなら spryte です。debuff はループ、入力、オーディオ、テストを加えます。
すでに Phaser を使っているなら、Phaser からの移行に対応表と、何も書き換えずに一番重い演出で spryte を半日で測る方法があります。