本文へスキップ

Phaser のゲームがカクつくのはなぜ? ​

あなたの Phaser のゲームは 60 fps でなめらかに動いています。ところが、ときどきそうでなくなります。敵が出現するときの一瞬の停止、新しいシーンが始まるときのヒッチ、なぜか他人のノート PC でだけ起きるカクつき。

Phaser 内蔵の FPS カウンターはフレームが落ちたことは教えてくれますが、理由は教えてくれません。Myaku は理由を教えてくれます。すべてのフレームを見張り、遅いフレームをわかりやすい言葉で説明します。

ゲームに追加する ​

bash
npm install -D myaku
ts
// main.ts の 1 行目
import "myaku/auto";
import Phaser from "phaser";

画面の隅に小さなフレームグラフが現れます。赤いスパイクがヒッチです。グラフにマウスを乗せると止まるので、スパイクをクリックしてみましょう。

どのシーンが遅いのかを見る ​

ゲームループを握っているのは Phaser なので、ブラウザから見ると遅いフレームは「Phaser の step 関数に 40 ms かかった」としか見えません。正しいけれど役に立ちません。Myaku は Phaser 自身のループにスパンを追加するので、判定はその中のどこで時間を使ったのかを教えてくれます。

  • update:すべてのシーンの update 処理
  • update:<シーンのキー>:シーンごとの処理。たとえば update:level や update:hud
  • render:描画

Phaser 3 や、script タグで読み込んだ Phaser なら自動です。Myaku がグローバルの Phaser を見つけて、すべてのゲームとシーンを計測します。Phaser 公式の Runner サンプルで、ゲームには一切手を加えずに試したところ、すべてのフレームで update、update:game、render が取れました。

ES モジュールで使う Phaser 4 にはグローバルがないので、ゲームを Myaku に渡してください。

ts
import { adapt, myaku } from "myaku";

const game = new Phaser.Game(config);
adapt(myaku(), game);

すると、遅いフレームの判定はたとえばこうなります。

Frame 812 took 38 ms (budget 25 ms). The game's frame code ran 35 ms. Spans: update 33 ms, update:level 31 ms, render 2.0 ms.

(フレーム 812 に 38 ms かかりました(予算 25 ms)。ゲームのフレーム処理に 35 ms。スパン:update 33 ms、update:level 31 ms、render 2.0 ms)

これで、描画ではなく level シーンの update が原因だとわかります。さらに絞り込むには、その中のシステムを自分でスパンに包みましょう。

ts
const profiler = myaku();

class Level extends Phaser.Scene {
  update(time: number, delta: number) {
    profiler.span("enemies", () => this.updateEnemies(delta));
    profiler.span("physics", () => this.physics.world.update(time, delta));
  }
}

Phaser でよくある容疑者 ​

Phaser のゲームで Myaku がよく報告する原因と、それぞれの対策です。

  • upload:プレイ中に作られるテクスチャ。 this.add.text() は canvas テクスチャを作ります。そのため update の中でテキストオブジェクトを作ったり、毎フレーム文字を変えたりすると、そのたびに GPU へのアップロードが起きます。this.textures.addCanvas や実行時のテクスチャ生成も同じです。create() で作って使い回すか、ビットマップテキストを使いましょう。Myaku はバイト数も数えます。あるテストでは 1 フレームで 64 MB がアップロードされていました。
  • compile:シェーダーの初回使用。 ポストプロセスのパイプラインや FX は、初めて使うときにシェーダーをコンパイルします。ローディングシーンの間に温めておきましょう。
  • gc:ホットパスでのメモリ確保。 update 内の new Phaser.Math.Vector2()、毎フレーム全部の敵に対して行う配列の filter/map、毎フレーム作られるトゥイーンのクロージャ。オブジェクトを使い回し、プーリング付きのグループ(group.get())を使いましょう。
  • 場所付きの script。 遅い関数が Phaser のループの外、たとえばローダーのコールバックや入力ハンドラーで動いた場合、Chrome がどの関数かを正確に Myaku に伝えます。Myaku はそれをファイル名付きで示します。
  • throttled:ゲームのせいではありません。 ウィンドウが隠れていたり最小化されていたりすると、ブラウザはフレームを遅くします。Myaku はゲームのせいにせず、そう伝えます。

共有できる数字 ​

window.__MYAKU__.report() は、フレーム時間のパーセンタイル(p50、p95、p99)、1 分あたりのヒッチ回数、判定付きのワーストのヒッチ、各スパンの平均コストを返します。ただの JSON なので、issue に貼ったり、Playwright のスクリプトから確かめたりできます。

Myaku を始める → · スパンとエンジン · 判定の読み方