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。