AIゲームの作り方 約11分で読めます

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

「HTML1枚でゲームを作る」の文字と、背景に映るキャッチゲームの画面

ブラウザゲームは、HTMLファイル1つで作れます。この記事のコードは、落ちてくるパンをかごで受ける30秒のゲームで、マウス・タッチ・キーボードのどれでも動きます。サイズは3,783バイトです。

コードは、画面を出さないChromeで動かして確かめました。画面の更新が遅い場合を想定した自動プレイで、当たり判定が抜ける条件も測っています。AIにゲームを書かせるときに、出てきたコードで確認したい点も、あわせてまとめました。

この記事で分かること

  • HTML1枚で動くゲームのコード全体と、5つの部品
  • スマートフォンとパソコンの画面に収める方法と、確かめた結果
  • 当たり判定が抜ける条件と、抜けない判定のしかた(自動プレイで測定)
  • AIが書いたコードで確認する5つの点
  • 確かめられていないこと

作ったゲームを公開する方法は、ブラウザゲームの公開方法にまとめています。AIにゲームを作らせる全体の流れは、ChatGPT・Geminiでゲームを作る手順を参考にしてください。

この記事の目次
  1. 011枚のHTMLに入る5つの部品
  2. 02コード全体
  3. 03画面の大きさに合わせる
  4. 04当たり判定が抜けるとき
  5. 05ベストスコアを保存する
  6. 06AIが書いたコードで確認する5つの点
  7. 07確かめられていないこと
  8. 08うまくいかないとき
  9. 09よくある疑問
  10. 10この記事のまとめ

1枚のHTMLに入る5つの部品

1枚のHTMLに入っている5つの部品の図。1は画面。canvasを1つ置き、縦横の比だけ決めて画面に合わせて拡大する。2は入力。マウスとタッチはpointermove、キーボードはkeydownとkeyup。3は更新。前の画面からの秒数をかけて位置を動かし、当たりも判定する。4は描画。毎回画面を消して描き直す。5は保存。ベストスコアはlocalStorageへ。保存できなくても止まらないようにする
どのブラウザゲームも、この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の縦画面で、かごと落ちてくる玉が表示されている。右は、パソコン1280×720の横長の画面で、中央に縦長の画面が表示されている
画面を出さないChromeで確かめました

スマートフォンの大きさ(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秒に60回、30回、15回、10回のときは、どちらの書き方も、落ちてきた良い玉をすべて取れた。1秒に6回のときは、高さの範囲だけを調べる書き方は26個のうち18個、前の位置から今の位置の間を調べる書き方は26個すべて取れた
1回の更新で落ちる距離が判定の高さより大きいと、取れなくなります

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つにまとめました。

AIが書いたゲームのコードで確認する5つの点。1は、動きにdt(秒数)をかけているか。1秒に落ちる距離が、更新の回数によらず同じ。2は、当たり判定がすり抜けないか。前の位置から今の位置の間を調べる。3は、dtに上限があるか。長い停止のあとの飛びを防ぐ。4は、保存が失敗しても止まらないか。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点で確認できる
  • 実機のスマートフォンでの操作は、確かめていない