ブラウザゲームの作り方|HTML1枚で動くキャッチゲームと、当たり判定が抜ける条件

ブラウザゲームは、HTMLファイル1つで作れます。この記事のコードは、落ちてくるパンをかごで受ける30秒のゲームで、マウス・タッチ・キーボードのどれでも動きます。サイズは3,783バイトです。
コードは、画面を出さないChromeで動かして確かめました。画面の更新が遅い場合を想定した自動プレイで、当たり判定が抜ける条件も測っています。AIにゲームを書かせるときに、出てきたコードで確認したい点も、あわせてまとめました。
この記事で分かること
- HTML1枚で動くゲームのコード全体と、5つの部品
- スマートフォンとパソコンの画面に収める方法と、確かめた結果
- 当たり判定が抜ける条件と、抜けない判定のしかた(自動プレイで測定)
- AIが書いたコードで確認する5つの点
- 確かめられていないこと
作ったゲームを公開する方法は、ブラウザゲームの公開方法にまとめています。AIにゲームを作らせる全体の流れは、ChatGPT・Geminiでゲームを作る手順を参考にしてください。
この記事の目次
1枚のHTMLに入る5つの部品

コード全体
次のコードを、index.htmlという名前で保存してブラウザで開くと、そのまま遊べます。サーバーは要りません。かごをマウスやタッチで動かします。キーボードは、左右の矢印キーです。黄色い玉を取ると1点、黒い玉を取ると3点減ります。
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no">
<title>パンキャッチ</title>
<style>
html,body{margin:0;height:100%;background:#223;overflow:hidden;touch-action:none}
canvas{display:block;margin:0 auto;height:100vh;max-width:100vw;aspect-ratio:9/16;background:#9cd}
</style>
</head>
<body>
<canvas id="c" width="360" height="640"></canvas>
<script>
const W = 360, H = 640;
const cv = document.getElementById('c'), g = cv.getContext('2d');
const TIME = 30; // 1回の長さ(秒)
let best = 0;
try { best = +localStorage.getItem('pan-best') || 0; } catch (e) {} // 保存できない環境でも動かす
let player, items, score, left, spawn, running;
function start() {
player = { x: W / 2, w: 70 }; items = []; score = 0; left = TIME; spawn = 0; running = true;
}
start();
// 入力: マウス・タッチ・キーボード
function moveTo(clientX) {
const r = cv.getBoundingClientRect();
player.x = Math.max(player.w / 2, Math.min(W - player.w / 2, (clientX - r.left) / r.width * W));
}
cv.addEventListener('pointermove', (e) => moveTo(e.clientX));
cv.addEventListener('pointerdown', (e) => { if (!running) start(); else moveTo(e.clientX); });
const keys = {};
addEventListener('keydown', (e) => { keys[e.code] = true; if (!running && e.code === 'Space') start(); });
addEventListener('keyup', (e) => { keys[e.code] = false; });
function update(dt) {
if (!running) return;
left -= dt;
if (left <= 0) { left = 0; running = false; if (score > best) { best = score; try { localStorage.setItem('pan-best', best); } catch (e) {} } return; }
const k = (keys.ArrowRight ? 1 : 0) - (keys.ArrowLeft ? 1 : 0);
player.x = Math.max(player.w / 2, Math.min(W - player.w / 2, player.x + k * 320 * dt));
spawn -= dt;
if (spawn <= 0) {
spawn = 0.7;
items.push({ x: 20 + Math.random() * (W - 40), y: -20, bad: Math.random() < 0.25 });
}
const speed = 180 + (TIME - left) * 6; // 時間がたつほど速くなる(1秒に px)
for (const it of items) { it.py = it.y; it.y += speed * dt; }
for (const it of items) {
// 前の位置から今の位置までの間に、かごの高さ(H-60〜H-30)を通ったかで判定する
const crossed = it.py < H - 30 && it.y > H - 60;
if (!it.done && crossed && Math.abs(it.x - player.x) < player.w / 2 + 10) {
it.done = true; score = Math.max(0, score + (it.bad ? -3 : 1));
}
}
items = items.filter((it) => !it.done && it.y < H + 20);
}
function draw() {
g.clearRect(0, 0, W, H);
g.fillStyle = '#6b4'; g.fillRect(0, H - 30, W, 30);
g.fillStyle = '#a63'; g.fillRect(player.x - player.w / 2, H - 60, player.w, 20);
for (const it of items) {
g.fillStyle = it.bad ? '#333' : '#fb3';
g.beginPath(); g.arc(it.x, it.y, 14, 0, 7); g.fill();
}
g.fillStyle = '#000'; g.font = '20px sans-serif'; g.textAlign = 'left';
g.fillText('スコア ' + score + ' のこり ' + Math.ceil(left) + '秒', 10, 28);
if (!running) {
g.fillStyle = 'rgba(0,0,0,.6)'; g.fillRect(40, 220, W - 80, 180);
g.fillStyle = '#fff'; g.textAlign = 'center';
g.fillText('スコア ' + score + '(ベスト ' + best + ')', W / 2, 290);
g.fillText('タップかスペースでもう一度', W / 2, 340);
}
}
// 2. ループ。前の画面からの秒数(dt)で動かす。長く止まったあとの飛びを防ぐため、0.1秒で頭打ちにする
let last = 0;
function loop(t) {
const dt = Math.min((t - last) / 1000, 0.1);
last = t;
update(dt); draw();
requestAnimationFrame(loop);
}
requestAnimationFrame((t) => { last = t; loop(t); });
</script>
</body>
</html>
画面の大きさに合わせる
canvasのサイズは、内部では360×640に決めています。CSSで、高さを画面いっぱいにして、縦横の比を9対16に固定しました。画面が横長のパソコンでは、中央に細長く出ます。座標は内部の360×640で扱うので、画面の大きさが変わっても、ゲームの中身は変わりません。

スマートフォンの大きさ(375×667)では、canvasは375×667で表示されました。パソコン(1280×720)では405×720でした。タップした位置の横の座標へ、かごが動くことも確かめています。画面の70%の位置をタップして、かごの位置は360のうちの252(70%)になりました。
当たり判定が抜けるとき
落ちてくるものの判定は、いちばん間違えやすい部分です。最初は、「玉の高さが、かごの高さ(30px幅)の中にあるか」だけを調べる書き方が浮かびます。この書き方は、画面の更新が遅いと、玉がかごの高さを一度で飛び越えて、取れなくなります。
コードのupdateでは、前の位置(py)から今の位置(y)までの間に、かごの高さを通ったかを調べています。違いを確かめるために、2つの書き方で比べました。
- 高さの範囲だけを調べる書き方
- 前の位置から今の位置の間を調べる書き方(この記事のコード)
どちらも、いちばん低い良い玉の真下へ、かごを一瞬で動かす自動プレイで、30秒分を動かしました。1秒に画面を更新する回数を、60・30・15・10・6回に変えています。

1秒に10回までは、どちらもすべて取れました。1秒に6回のときは、範囲だけを調べる書き方は26個のうち18個で、8個が抜けました。通った間を調べる書き方は、26個すべてを取れています。更新の遅さを作るため、この試験では、更新関数を、決まった秒数で直接呼んでいます。実際のループでは、秒数の上限が0.1秒なので、抜けるのは、1秒に10回を下回って、玉が速くなった終盤です。
落ちる速さが、画面の更新の速さに関係ないことも測りました。玉を1秒分動かした距離は、1秒に60回の更新で183px、10回の更新で183.3pxでした。
ベストスコアを保存する
ベストスコアは、localStorageに保存しています。ブラウザによっては、保存が禁止されていて、書き込みでエラーになります。そのため、保存と読み出しを、try〜catchで囲みました。保存できない環境でも、ゲームは止まりません。
確かめたのは、スコアを7にして終わらせ、ページを再読み込みしたあとも、ベストが7のまま残ることです。保存が禁止された環境での動作は、確かめていません。
AIが書いたコードで確認する5つの点
AIにゲームのコードを書かせると、動くものが出てきます。ただ、動くことと、どの環境でも正しく動くことは別です。この記事で試した範囲から、確認する点を5つにまとめました。

確かめられていないこと
- 実際の端末での操作スマートフォンの画面は、画面を出さないChromeで再現したものです。実機のタッチ操作と、画面の回転は、確かめていません。
- タブを切り替えて戻ったとき、dtの上限が効くか
- 音を鳴らす処理(ブラウザは、操作の前に音を出せない場合があります)
- 保存が禁止された環境での、localStorageのエラー処理
- Safariなど、Chrome以外のブラウザでの動作
- AIにこのゲームを書かせたときに、同じコードが出るか
確かめていないものは、動かして確かめてから、別の記事で扱います。
うまくいかないとき
| 症状 | 確認すること |
|---|---|
| 画面が真っ白 | ブラウザのコンソールにエラーがないかを見ます。コードを途中で切って貼っていないかも確かめます |
| かごが動かない | canvasの上で、マウスやタッチを動かしているかを確かめます。キーボードは、左右の矢印キーです |
| 玉が取れないことがある | 当たり判定を、前の位置から今の位置の間を調べる書き方にします |
| スマホで画面が切れる | meta viewportの行があるか、canvasのCSSで縦横の比を固定しているかを確かめます |
| ベストが保存されない | プライベートモードや、保存を禁止する設定ではないかを確かめます |
関連記事ブラウザゲームの公開方法|無料ホスティング4社の比較と手順【2026年版】
よくある疑問
サーバーがなくても、遊べますか
このゲームは、1枚のHTMLを開くだけで動きます。外のファイルを読み込む形にすると、サーバーが要ります。
音は、どう足しますか
この記事では扱っていません。確かめてから、別の記事で扱う予定です。素材の作り方は、ゲームのBGM・効果音をAIで作る方法にあります。
ゲームエンジンは、要りませんか
この規模のゲームなら、なくても作れます。大きなゲームでの比較は、この記事では扱っていません。
この記事のまとめ
この記事のまとめ
- HTML1枚(3,783バイト)で、マウス・タッチ・キーボードのどれでも動くゲームが作れる
- 動きは秒数(dt)で決め、当たりは、前の位置から今の位置の間で判定する
- 高さの範囲だけで判定すると、1秒に6回の更新で、26個のうち8個が抜けた
- AIが書いたコードも、この5点で確認できる
- 実機のスマートフォンでの操作は、確かめていない












