BROWSER GAME BASICS
ブラウザゲームはどう動いているか: Canvasとゲームループ入門
ボタンを押すと機体が動き、敵と弾が飛び、次の瞬間には衝突の結果が表示される。その裏では、入力・計算・描画を短い周期で繰り返す仕組みが動いています。
公開日: 2026年7月14日
1枚の画面を何度も描き直している
ブラウザゲームの表示には、HTMLの<canvas>要素がよく使われます。Canvasは、JavaScriptから長方形、画像、文字などを座標指定で描ける領域です。たとえば横座標100、縦座標200へ自機を描き、次の更新で横座標104へ描けば、目には右へ移動したように見えます。
一般的な処理順は「入力を読む→物体の位置を更新する→衝突を調べる→画面を消して描き直す」です。この一周をゲームループと呼びます。60Hzの画面では、およそ16.7ミリ秒ごとに新しい絵を用意できると滑らかです。ただし、常に60回動くとは限りません。端末の性能や別タブの状態によって間隔は変わるため、移動量をフレーム数だけに依存させない設計が必要です。
requestAnimationFrameで画面更新に合わせる
requestAnimationFrameは、ブラウザが次に描画できるタイミングで指定した関数を呼びます。単純な骨格は次のようになります。
let previous = performance.now();
function loop(now) {
const dt = Math.min((now - previous) / 1000, 0.05);
previous = now;
update(dt);
draw();
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
dtは前回から経過した秒数です。速度が毎秒240ピクセルなら、位置へ240 * dtを加えます。30fpsへ落ちても、同じ実時間でほぼ同じ距離を進めます。例ではdtの最大値を0.05秒に抑えています。タブを戻した直後に数秒分を一度に計算し、自機が画面外へ飛ぶ事故を防ぐためです。
入力は「押した瞬間」と「押している間」を分ける
キーボードならkeydownとkeyupを受け、現在押されているキーを集合へ記録します。移動は押している間ずっと参照し、メニュー決定やボムは押した瞬間だけ処理すると、意図しない連続発動を避けられます。
- 左右移動: 毎フレーム、左右キーの状態から方向を計算する
- ショット: 一定の発射間隔を設け、押し続けても間隔を守る
- ポーズ: 押した瞬間に一度だけ状態を切り替える
タッチ操作では、指の座標をCanvas内の座標へ変換します。CSSで表示を縮小している場合、画面上の幅とCanvas内部の幅は異なります。getBoundingClientRect()で表示領域を取得し、内部サイズとの比率を掛けるのが要点です。画面外へ指が出た場合や、端末が回転した場合の解除処理も欠かせません。
当たり判定は見た目より単純にする
すべての画像の輪郭を1ピクセルずつ比較すると負荷が高く、プレイヤーにも境界が読みにくくなります。そこで、長方形同士や円同士の判定へ置き換えます。円なら、2つの中心間距離が半径の合計より小さいかを調べます。平方根を計算せず、「距離の二乗」と「半径合計の二乗」を比べれば軽量です。
シューティングでは、自機の見た目より当たり判定を小さくすることがあります。重要なのは小ささそのものではなく、中心がどこかをプレイヤーが把握できることです。自機中心に点を表示する、被弾時に判定位置と合う反応を返すなど、視覚情報と計算結果を一致させます。
物体が増えたときは、画面外の弾を配列から外します。1000個の弾と100個の敵を総当たりで比べると10万回の判定になります。格子状に領域を分け、近い物体だけ比較する方法もありますが、小規模なゲームでは、まず不要な物体の削除と簡単な図形判定から始める方が実装を確かめやすくなります。
ゲームの状態を分ける
タイトル、プレイ中、ポーズ、結果画面を同じ処理へ詰め込むと、ゲームオーバー後も敵が動くといった不具合が起きます。現在の状態をtitle、playing、resultのように明示し、状態ごとに入力と更新を限定します。
プレイ中のデータも役割を分けます。自機は位置・速度・残機、敵は種類・体力・行動時間、弾は位置・半径・所属を持つ、と決めると見通しが良くなります。スコア表示は計算結果を読むだけにし、描画処理から得点を増やさないことも大切です。描画回数が変わってもルールが変化しないためです。
localStorageで小さなデータを保存する
ハイスコアや設定はlocalStorageへ文字列として保存できます。同じブラウザ、同じサイトで次回も読み出せ、サーバーを用意せずに済みます。オブジェクトはJSON.stringifyで文字列へ変換し、読み出すときはJSON.parseを使います。
const data = { highScore: 12800, volume: 0.6, version: 1 };
localStorage.setItem("game-save", JSON.stringify(data));
読み込みでは、データが存在しない場合と壊れている場合を考え、try...catchで初期値へ戻します。将来項目を増やせるよう、保存形式にバージョン番号を持たせると更新しやすくなります。
localStorageは機密情報の保管場所ではありません。ページ上のJavaScriptから読め、ブラウザのサイトデータ削除、プライベートブラウズ、端末故障でも失われます。ハイスコアには向きますが、復元できない重要記録にはエクスポート機能など別の備えが必要です。また、端末間で自動同期されない点も、利用者へ明記します。
小さな試作から確認する手順
- Canvasへ自機を1つ描き、キーで動かす。
- 画面端を越えないよう座標を制限する。
- 弾を1種類追加し、画面外で削除する。
- 円同士の衝突を追加し、判定範囲を線で可視化する。
- タイトル・プレイ中・結果の状態を分ける。
- 最後にハイスコアを保存し、再読み込みで復元を試す。
最初から敵を何十種類も置くより、1つの入力と1つの衝突が安定してから広げる方が、原因を切り分けやすくなります。開発中は座標、経過時間、当たり判定を一時表示し、目で見える動きと内部の数値が一致しているかを確認します。
関連リンク
この記事について
執筆: rin0420。本サイトで公開しているゲームとツールの開発者です。掲載しているスクリーンショットと数値は、公開中の実物を操作して取得したものです。
公開: 2026年7月14日 / 最終更新: 2026年7月25日(スクリーンショットを追加)