本文へスキップ

ブラウザなしで Phaser のゲームをテストする方法 ​

Phaser のゲームは、たいてい遊んでテストします。それで済むのはゲームが大きくなるまでです。ジャンプを変えたら遊び直さなかったステージが壊れていたり、リファクタリングで敵の振り向きが 1 フレーム遅れたり。このガイドでは、Keiko を使って Phaser のゲームに自動テストを追加します。Keiko は本物のゲームを Node で動かします。ブラウザは不要で、Phaser の動かし方も変えません。

Phaser 4 を使った Vite プロジェクトを前提にしていますが、Phaser 3 でも同じ手順で動きます。

1. Keiko をインストールする ​

bash
npm install -D keiko

2. テストからゲームを読めるようにする ​

Keiko は最初からゲームを遊べます。仮想クロックで Phaser のループを進め、本物のキーボードイベントとポインターイベントを送ります。何が起きたかを確かめるには、気になる状態のスナップショットを公開しましょう。公開するのは開発ビルドのときだけです。

ts
// src/main.ts
const game = new Phaser.Game(config);

if (import.meta.env.DEV) {
  window.__KEIKO__ = {
    state: () => {
      const level = game.scene.getScene("level") as Level;
      if (!level?.player) return undefined; // まだ作られていない
      return {
        player: { x: Math.round(level.player.x), y: Math.round(level.player.y), onGround: level.player.body.blocked.down },
        score: level.score,
      };
    },
    scene: () => game.scene.getScenes(true)[0]?.scene.key,
  };
}

Phaser は最初の数フレームをかけてシーンを作るので、ステージができるまで state() は undefined を返します。テストはそれを待ちます。

3. テストを書く ​

ts
// tests/player.test.ts
import { expect, play, test } from "keiko";

const ready = (g) => g.scene === "level" && g.state !== undefined;

test("the player runs right while the arrow key is held", async () => {
  const game = await play({ ready });
  const start = game.state.player.x;
  game.hold("ArrowRight", "1s");
  expect(game.state.player.x).toBeAbove(start + 100);
});

test("a jump leaves the ground and lands again", async () => {
  const game = await play({ ready });
  game.until((s) => s.player.onGround, { within: "2s" });
  game.press("Space");
  const y = game.trace("player.y", "1.5s");
  expect(y.start - y.min()).toBeAbove(40);
  expect(game.state.player.onGround).toBe(true);
});

test("clicking the coin collects it", async () => {
  const game = await play({ ready });
  game.tap(220, 140).wait(2);
  expect(game.state.score).toBe(1);
});

game.hold、game.tap、game.wait はフレームを進めます。1 ステップは毎秒 60 フレームの 1 フレームで、長さはステップ数(30)でも時間("0.5s")でも指定できます。Phaser の delta は仮想クロックから来るので、移動は毎回同じになります。ポインターの座標はゲーム内のピクセルで、Phaser が読む pageX も Keiko が埋めてくれるので、タップは狙ったところにぴったり当たります。

4. 実行する ​

bash
npx keiko
text
pass tests/player.test.ts › the player runs right while the arrow key is held 212 ms
pass tests/player.test.ts › a jump leaves the ground and lands again 96 ms
pass tests/player.test.ts › clicking the coin collects it 41 ms

✔ 3 passed in 1180 ms

ブラウザは起動しません。Phaser は WebGL か Canvas のレンダラーで動きますが、相手はどんな呼び出しも受け付けて何も描かないコンテキストです。

5. 決定的にする ​

Phaser の乱数生成器 Phaser.Math.RND は、シードを与えない限り時計からシードを取ります。Keiko はゲームごとに Math.random() と時計を固定しますが、リプレイやバグ報告も含め、どこでもゲームを再現できるようにするには、自分でシードを与えましょう。

ts
Phaser.Math.RND.sow(["my-game"]);

そのうえで、Keiko に証明させます。

ts
test.deterministic("thirty seconds of play are reproducible", (game) => {
  game.hold("ArrowRight", "5s").tap("Space").wait("25s");
}, { play: { ready } });

シードなしの乱数や実時間の時計に依存しているところがあれば、Keiko は最初に食い違った値と、それが起きたフレームを教えてくれます。

次へ ​