本文へスキップ

クソゲーの森の設計:PhaserとDOMをゲームごとに使い分ける

約 5 分著者 s-yoshiki
技術構成クソゲーの森
AI補助
  • クソゲーの森
  • Phaser
  • React
  • DOM
  • SSG
  • Web Worker

Phaserを前提にしない

クソゲーの森では、すべてのゲームを同じ描画エンジンへ乗せるのではなく、ゲームの性質に応じて方式を選びます。毎フレーム動くゲームや当たり判定が必要なものにはPhaserを使い、静的な盤面をクリックするオセロにはDOMを使うといった具合です。描画方式を統一することよりも、ゲームループが必要か、入力を要素として扱いたいか、アクセシビリティやテーマの仕組みをそのまま使えるかを基準に選んでいます。

ゲームの性質 描画方式 理由
座標、速度、衝突、毎フレーム更新 Phaser / Canvas ゲームループと描画をまとめやすい
盤面を選び、手番を進める React / DOM キーボード、読み上げ、テーマをそのまま使える

packagesへゲーム本体を分離する

ゲームのルールや設定は packages/<game-name> に分け、サイトのルーティングやデプロイ処理から独立させています。サイト側はゲームの名前、説明、公開パス、操作方法のようなメタデータをレジストリから読み込み、ゲーム本体の実行ロジックとは分離しています。そのため、一覧ページを生成するときに各ゲームの描画エンジンを実行する必要がありません。

Phaserを使うゲームでは、SSR時にPhaserを読み込まないよう、メタデータとクライアント起動コードを別の入口に分けています。PhaserはブラウザのCanvasやゲームループを必要とするため、サーバーやプリレンダー環境で初期化すると、windowやdocumentがないことによるエラーが起きてしまいます。そこでサーバー側は説明とルートだけを生成し、ブラウザでページがマウントされたあとにゲームを起動します。

packages/game
  ├─ metadata / config  ← SSRから安全に読み込む
  ├─ scene / create-game
  └─ client              ← ブラウザでだけ読み込む

クソゲーの森のゲームレジストリと描画方式の分岐

ゲームの一覧とルートを一つのレジストリから生成すれば、サイト側がゲーム内部の描画エンジンへ依存せずに済みます。次は、メタデータを使う境界を示す説明用の簡略例です。

type GameDefinition = {
  slug: string;
  title: string;
  renderer: "phaser" | "dom";
  loadClient: () => Promise<unknown>;
};

const games: readonly GameDefinition[] = [
  { slug: "runner", title: "ランナー", renderer: "phaser", loadClient: loadRunner },
  { slug: "othello", title: "オセロ", renderer: "dom", loadClient: loadOthello },
];

SSGやSSRが参照するのはslugtitle、操作説明のような安全なメタデータだけにし、loadClientはブラウザでゲームページを開いたあとに呼び出します。これにより、ゲームエンジンを使わない一覧ページまでCanvas初期化の影響を受けることがなく、ゲームごとのチャンク分割も維持できます。

オセロのようなDOMゲームでは、ルールとAIを純粋なTypeScriptとして保持しています。盤面の状態、合法手、石をひっくり返す処理、勝敗判定をUIコンポーネントから分離することで、表示を変更してもルールのテストを再利用できます。8×8固定の定数にせず盤面のサイズを扱えるようにすることで、設定変更がUI全体へ波及しないようにしています。

DOMを使うゲームでは、盤面をボタンや要素として表現できるため、フォーカス、キーボード操作、ラベル、テーマの仕組みをそのまま利用できます。一方で毎フレーム大量の要素を更新するゲームには向かず、ゲームの性質に合わないDOM化はかえって処理負荷と実装量を増やします。描画方式の違いを隠すのではなく、ゲームごとに理由を明示することを設計上の判断にしています。

SSGとWeb Worker

トップ、ゲーム一覧、既知のゲームURLはビルド時にプリレンダーします。初回表示ではゲームエンジンの読み込みを待たせず、ゲーム本体はマウント後に必要なチャンクだけを読み込みます。ゲーム一覧や操作説明は先にHTMLとして読めるため、利用者はJavaScriptのゲームコードが準備される前でも何を遊べるかを確認できます。

オセロの高い難易度ではAI探索が長くなるため、探索をWeb Workerへ分離しています。メインスレッドを止めず、考え中の表示やテーマ切り替えを維持するためです。Workerとの通信では盤面のコピーと探索結果の受け渡しが必要になるため、キャンセル、連続クリック、ゲーム終了後の遅いレスポンスといったケースも考慮しています。難易度を上げることだけを優先せず、操作が固まったように見えないことを重視しています。

静的生成には、公開URLを一覧へ登録し忘れるとページそのものが生成されない、という制約があります。ゲームを追加するときは、パッケージ、レジストリ、ルート、操作説明、ビルド成果物をあわせて確認します。

実装方式の詳細はプロダクト側のarchitecture.mdで更新しています。EX FOUNDRYでは利用者向けの概要と変更点を掲載し、細かい実装の契約やパッケージの構成はリポジトリのドキュメントを正としています。設計が変わったときは、両方の説明が矛盾しないように更新します。

記事の更新・修正

内容の誤り、リンク切れ、現在の仕様との不一致を見つけた場合は、お問い合わせページから知らせてください。

修正・更新を知らせる

おすすめの記事