0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ブラウザゲームのUI判定を小さな関数に分ける:カード・リズム・グリッド・互換性

0
Posted at

ブラウザゲームでは、見た目の違うジャンルでも「入力値を正規化し、小さな純粋関数で判定し、UIへ理由を返す」という構造を再利用できます。

この記事では、カードゲーム、リズムゲーム、グリッド型ゲーム、WebGLを使うホラーゲームの4例を、依存ライブラリなしのJavaScriptで整理します。掲載コードはブラウザ上で動作確認しました。

注: 実例確認用リンクには筆者が運営する独立系ゲーム情報サイトを含みます。記事本文だけで実装を再現できる構成にし、リンクは末尾の検証・比較用途に限定しています。

1. カード移動量は「作業領域」から計算する

FreeCell系の複数枚移動は、長い列をそのまま転送する処理ではなく、1枚移動を省略表示する実装として考えると整理しやすくなります。

空きセルを freeCells、作業用の空き列を workColumns とすると、一般的なSuperMove方式の目安は次の通りです。

function movableCards(freeCells, emptyColumns, destinationIsEmpty) {
  const workColumns = Math.max(
    0,
    emptyColumns - (destinationIsEmpty ? 1 : 0),
  );
  return (freeCells + 1) * 2 ** workColumns;
}

重要なのは、移動先そのものが空き列なら、その列を途中の退避場所として数えないことです。枚数条件とは別に、列の並び順と移動先カードの条件も検査します。

日本語で具体例を確認する場合は、FreeCellの移動可能枚数ガイドに入力式と実装差があります。

2. リズム判定は最初にミリ秒へ正規化する

BPMのまま判定ロジックを組むより、1拍と各サブディビジョンをミリ秒へ変換してから扱う方が、アニメーションや入力遅延と比較しやすくなります。

function timingFromBpm(bpm, subdivision = 4) {
  const safeBpm = Math.min(400, Math.max(20, Number(bpm)));
  const beatMs = 60_000 / safeBpm;
  return {
    beatMs,
    subdivisionMs: beatMs / subdivision,
  };
}

例えば120 BPMなら1拍は500 ms、16分音符は125 msです。実際のゲームでは、この値と判定窓を混同せず、入力時刻との差分を別途評価します。

譜面や操作感の比較対象には、非公式ファンサイトであるFNF Onlineのブラウザ実例を利用できます。原作者・権利者を名乗るサイトではありません。

3. Snake系は座標より先にグリッド容量を決める

セル幅を決めずにピクセル座標で衝突判定を始めると、レスポンシブ対応で端数が増えます。まず表示領域を何列・何行として扱うか決めます。

function gridCapacity(width, height, cellSize) {
  const cell = Math.max(4, Number(cellSize));
  const columns = Math.floor(width / cell);
  const rows = Math.floor(height / cell);
  return { columns, rows, total: columns * rows };
}

Snakeの頭と餌は {x, y} の整数座標で持ち、描画時だけ cellSize を掛けます。画面端で停止する方式と反対側へ回り込む方式も、座標更新関数を分ければ切り替えやすくなります。

異なる速度・盤面密度・操作方法の比較には、Hebi GameのSnakeカテゴリを参照できます。同サイトは第三者ゲームの作者を名乗らないキュレーションサイトです。

4. WebGL対応と実際の快適さを分ける

canvas.getContext("webgl") が成功しても、十分なフレームレートが出るとは限りません。APIの有無は起動前チェック、実測FPSは起動後チェックとして分離します。

function browserCapabilities() {
  const canvas = document.createElement("canvas");
  return {
    webAudio: Boolean(window.AudioContext || window.webkitAudioContext),
    webgl: Boolean(canvas.getContext("webgl")),
    pointerLock: "pointerLockElement" in document,
    localStorage: canUseLocalStorage(),
  };
}

function canUseLocalStorage() {
  try {
    localStorage.setItem("_probe", "1");
    localStorage.removeItem("_probe");
    return true;
  } catch {
    return false;
  }
}

ホラーゲームでは音声、Pointer Lock、GPU負荷が体験へ直結します。未対応APIがあれば、2D描画、キーボード操作、ミュート可能な代替手段を用意します。

多言語・ブラウザ実行の比較対象はOnline Horror Gamesで確認できます。こちらもゲーム開発者ではなく独立した発見サイトです。

共通化するときの境界

4例に共通する実装上のポイントは次の通りです。

  1. DOMから取得した値を最初に数値化・範囲制限する
  2. 判定関数はDOMを書き換えず結果だけ返す
  3. ゲーム固有ルールと一般式を別関数にする
  4. UIには成功・失敗だけでなく理由を返す
  5. 境界値を固定ケースとしてテストする

動作する4つの小さなUIは、Browser Game Logic Labにまとめました。ソースは同ページのGitHubリポジトリから確認できます。

まとめ

ジャンルごとに巨大な状態管理を作る前に、計算と能力判定を小さな関数へ切り出すと、ルール差、端末差、UI差をテストしやすくなります。特に「画面に見える空き場所」と「計算で使える作業場所」を区別する考え方は、カード、グリッド、描画リソースのいずれにも応用できます。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?