本文へスキップ

Keiko のしくみ ​

ページ ​

play() は毎回新しい happy-dom のウィンドウを開き、それを Node のグローバル(window、document、navigator と DOM クラス群)として登録します。そこへページのマークアップをスクリプトを実行せずに書き込みます。続いて、プロジェクトの Vite 設定を使い、Vite のモジュールランナーからページのモジュールスクリプトを import します。なので TypeScript、エイリアス、プラグインは vite dev と同じように動きます。play() のたびにゲームのモジュールを評価し直すので、ステートは一切持ち越されません。

時計 ​

ゲームの実行中、Keiko は次のものを置き換えます。

置き換え先
requestAnimationFrame, cancelAnimationFrameステップごとに 1 回実行されるキュー
setTimeout, setInterval, clearTimeout, clearIntervalゲーム時間で動くタイマー
requestIdleCallbackフレームごとに 1 回、余裕のある締め切り付き
performance.now(), Date.now()ゲーム時間(日付は 2026-01-01 から始まります)
Math.random()シード付きの乱数生成器(mulberry32)

1 ステップで時間が 1 フレーム進みます。期限が来たタイマーは順番に発火し、それぞれ自分の期限時刻を見ます。そのあとフレームのタイムスタンプ付きでフレームコールバックが走ります。タイマーのコールバック内で設定されたタイマーはブラウザと同じく最低 4 ms 待つので、遅延 0 で自分を再登録し続けるタイマーがステップを止めてしまうことはありません。Node 自身の内部(fetch など)が設定したタイマーは実時間のままです。

描画と音 ​

  • Canvas 2D: あらゆる呼び出しを受け付けるコンテキスト。measureText と getImageData はそれらしい形の値を返します。
  • WebGL 1 と 2: 本物の定数を持ち、あらゆる呼び出しを受け付けるコンテキスト。シェーダーはコンパイルでき、プログラムはリンクされ、GLSL で宣言した attribute と uniform を報告します。フレームバッファは complete、上限値はミドルレンジの GPU 相当で、読み出しはゼロを返します。play({ webgl: false }) で WebGL を隠すと、エンジンは Canvas 2D にフォールバックします。
  • Web Audio: ノードがあらゆる呼び出しを受け付けるコンテキスト。デコードは即座に成功します。Promise 形式とコールバック形式のどちらにも対応しています。
  • フォント: document.fonts はすべてのフォントを読み込み済みと報告します。

アセット ​

fetch と画像の読み込みはプロジェクトの Vite サーバーに向かうので、ローダーはブラウザと同じように URL を解決します。画像は本物の <img> 要素です。Keiko はバイト列を取得してヘッダー(PNG、GIF、JPEG)からサイズを読み取り、ピクセルはデコードしません。XMLHttpRequest、オブジェクト(blob:)URL、Web Worker も動きます。Worker はページのスレッド上でインラインに実行されます。Keiko は処理中のリクエスト数を数えていて、play() はゲームの最初の読み込みが終わるまで、game.settle() はそれ以降の読み込みが終わるまで待ちます。

レイアウトと入力 ​

ページ上の canvas のボックスは、CSS サイズ(高 DPI 画面向けにエンジンが設定する style.width/height)、なければピクセルサイズで、原点に置かれます。ポインターイベントはすべての位置フィールド(clientX、pageX、offsetX、x)に同じ点を入れるので、エンジンがどのフィールドを読んでも渡した座標が届きます。キーはフォーカス中の要素に、何もフォーカスされていなければゲームの canvas に送られ、document と window までバブリングします。

エラー ​

ステップ中に起きたゲームのエラーは tick とシーン付きの GameError になり、スタックはソースマップであなたのコードに対応付けられます。ゲームの非同期処理から来るエラー(読み込み失敗、reject された Promise)は、そのとき実行中のテストを失敗させます。テストが走っていなければ、実行後に一覧表示されます。

Keiko にテストできないもの ​

  • 描かれるもの: 何も描画しないので、ピクセル出力、シェーダー、レイアウトには本物のブラウザでのスクリーンショットテストが必要です。
  • 聞こえるもの: オーディオグラフは無音で動きます。
  • 実機: GPU の上限、ブラウザ固有の癖、実際のパフォーマンスには本物のブラウザが必要です。
  • 複数ゲームの同時実行: 1 プロセスで動くゲームは一度に 1 つで、テストは順番に実行されます。

既知の制限 ​

Keiko はブラウザのタイミングをすべて再現するのではなく、よくあるケースを前提にして速さを保っています。

  • 読み込みとフレームの競合。 run() と eventually() はフレームの合間に読み込みを待ちますが、独自のスケジュールで完了する処理もあります(Worker からの返信、さらに読み込みを呼ぶ読み込み)。こうした処理は、実行ごとに違うフレームで終わることがあります。アセットの到着で画面を切り替える Bubbo Bubbo のように、その順序でロジックが変わるゲームは、フレーム単位では決定的になりません。こうしたフローは正確な tick ではなく結果(eventually(…))でテストしてください。
  • CSS レイアウトはありません。 DOM ベースのメニューは、ゲーム自身が設定した位置しか持ちません。
  • 未対応: WebGPU、WebAssembly スレッド、SharedArrayBuffer を使う Worker。