マガジン

30分で作る「落ちてくるものを避ける」ゲーム:最小の構成

遊びをつくる

執筆にClaudeを使用しています。

週末の明るい机に置かれた、一つの仕組みで遊べる小さな完成済みゲーム。
記事のテーマをもとに制作したAI生成の写真風イメージ。

「避けるだけのゲーム」は、何から作ればよいのでしょうか。背景、効果音、スコア表示、難易度の上昇と、足したくなるものはいくらでもあります。けれども、最初の30分でそれらに手を出すと、肝心の「遊べる状態」が遠のきます。この記事では、入力・移動・判定・終了の4つだけで動く試作を、コピーして動かせるコードと番号付きの手順で組み立てます。 完成形は、画面下の四角を左右キーで動かし、上から降ってくる四角に当たらないようにするゲームです。当たったらそこで終わり、Enterキーでやり直せます。画像も音もライブラリも使わず、HTMLファイル1つで動きます。 読み終えるころには、手元に「動く最小のゲーム」と、次に何を足すかを選ぶための基準が残ります。結果の数字は書いていません。調整の記録は、読者自身が取る形にしています。

この記事の目次
  1. 4つだけに絞る理由
  2. 準備:ファイルとコード全体(5分)
  3. 手順:4つの役割を順に確かめる(20分)
  4. 30分の配分と詰まりやすい点
  5. 調整の判断基準:遊べるかを自分で測る
  6. 次の行動

4つだけに絞る理由

避けるゲームの面白さは、「いつ当たるか分からない緊張」と「あと少しで避けられた悔しさ」にあります。この2つを生むのに必要な仕組みは、実は多くありません。

  • 入力:プレイヤーが動かす手段があること
  • 移動:自分と降ってくるものが、毎フレーム位置を変えること
  • 判定:重なったかどうかを調べること
  • 終了:終わりがあり、やり直せること

どれか1つでも欠けると、ゲームとして成立しません。入力がなければ眺めるだけです。判定がなければ、何が当たってもすり抜けます。終了がなければ、緊張が生まれません。逆に言えば、この4つが動いた時点で「遊べる試作」です。得点、音、見た目は、その後に1つずつ足せば十分です。

足す順番を間違えないための目安も、先に決めておきます。「4つが動くまでは、見た目に関わる作業をしない」というルールです。色を変えたくなっても、四角のままで進めます。

準備:ファイルとコード全体(5分)

テキストエディタで index.html という名前の新しいファイルを作り、下のコードを貼り付けて保存します。ブラウザにドラッグすれば動きます。サーバーや外部ライブラリは要りません。

html
<!doctype html>
<meta charset="utf-8">
<canvas id="c" width="320" height="480" style="border:1px solid #444"></canvas>
<script>
const c = document.getElementById('c'), g = c.getContext('2d');
const me = { x: 150, y: 440, w: 20, h: 20 };
let rocks = [], keys = {}, frames = 0, over = false;

addEventListener('keydown', e => { keys[e.key] = true; if (over && e.key === 'Enter') reset(); });
addEventListener('keyup', e => { keys[e.key] = false; });

function reset() { rocks = []; frames = 0; over = false; me.x = 150; loop(); }

function update() {
  if (keys.ArrowLeft) me.x -= 4;
  if (keys.ArrowRight) me.x += 4;
  me.x = Math.max(0, Math.min(c.width - me.w, me.x));
  if (frames % 30 === 0) rocks.push({ x: Math.random() * 300, y: -20, w: 20, h: 20 });
  for (const r of rocks) {
    r.y += 3;
    if (r.x < me.x + me.w && r.x + r.w > me.x && r.y < me.y + me.h && r.y + r.h > me.y) over = true;
  }
  rocks = rocks.filter(r => r.y < c.height);
  frames++;
}

function draw() {
  g.clearRect(0, 0, c.width, c.height);
  g.fillStyle = '#2a7'; g.fillRect(me.x, me.y, me.w, me.h);
  g.fillStyle = '#c55';
  for (const r of rocks) g.fillRect(r.x, r.y, r.w, r.h);
  g.fillStyle = '#000'; g.fillText('生存 ' + (frames / 60).toFixed(1) + ' 秒', 8, 16);
  if (over) g.fillText('終了:Enterでもう一度', 100, 240);
}

function loop() {
  update(); draw();
  if (!over) requestAnimationFrame(loop);
}
loop();
</script>

開いたら、まず左右キーで緑の四角が動くかを確かめます。動かない場合は、ページをクリックしてキー入力の対象にしてから試します。ここまでで約5分です。

手順:4つの役割を順に確かめる(20分)

コードを貼っただけでは、仕組みが身につきません。4つの役割を1つずつ取り出し、わざと値を変えて挙動を確かめます。変えるのは毎回1か所だけにします。

手順1:入力を確かめる(5分)

  1. keydown で keys[e.key] = true、keyup で false にしている部分を探します。
  2. 移動を「キーが押された瞬間」に書かず、「押されている状態」として保存する理由を考えます。押しっぱなしのときのキーの繰り返し入力は、最初の1回の後に間が空くことがあり、動きがぎこちなくなりがちです。状態を保存して毎フレーム読めば、押している間は滑らかに動きます。
  3. ArrowLeft と ArrowRight を a と d に書き換え、動作を確かめます。

手順2:移動を確かめる(5分)

  1. update 内の me.x -= 4 の「4」を 8 にして、操作の感覚を比べます。
  2. Math.max(0, Math.min(...)) の1行を消して、四角が画面外へ出ることを確かめてから元に戻します。この1行が、移動の範囲を画面内に収めています。
  3. r.y += 3 が、降ってくる四角の速さです。数字を大きくするほど速く落ちます。

手順3:判定を確かめる(5分)

当たり判定は、長方形どうしが重なっているかを調べる方法です。軸に平行な長方形の重なりは、次の4つの条件がすべて成り立てば重なっています。

  • 相手の左端が、自分の右端より左にある
  • 相手の右端が、自分の左端より右にある
  • 相手の上端が、自分の下端より上にある
  • 相手の下端が、自分の上端より下にある

コードの長い if 文は、この4つを && でつないだものです。確かめ方として、me.w と me.h を 40 に変えてみてください。当たりやすさが変わるはずです。見た目の大きさと当たり判定が同じ値を使っているので、四角の大きさを変えると判定も連動します。

手順4:終了を確かめる(5分)

  1. over = true になると、loop が次のフレームを予約しなくなり、画面が止まります。
  2. 止まったあとの Enter で reset が呼ばれ、降ってくる四角の一覧、経過フレーム、位置を初期状態に戻して loop を再開します。
  3. 終了の仕組みで大切なのは、「止める条件」と「戻す処理」を対で用意することです。止める処理だけだと、遊び直せません。

30分の配分と詰まりやすい点

時間配分の目安は次のとおりです。

  • 準備、内容:ファイル作成とコード貼り付け、動作確認、目安:5分
  • 手順1〜4、内容:4つの役割を1か所ずつ変更して確認、目安:20分
  • 調整の記録、内容:数値を1つ変えて数回遊ぶ、目安:5分

詰まりやすい点は次のとおりです。

  • キーが反応しない:ページが選択されていないことが多いです。画面内を一度クリックします。
  • ファイルを保存し忘れる:エディタで保存してから、ブラウザを再読み込みします。
  • 一度に複数の数字を変える:原因が分からなくなります。1回に変えるのは1か所だけにします。
  • 時間表示がずれる:コードは1秒に60回の更新を前提に、フレーム数を60で割っています。画面の更新頻度が違う環境では、実際の秒数とずれる場合があります。試作の段階では目安として使い、正確な時間が要る場合は、時刻を取得する方法に置き換えます。
  • 終了後に速くなる:reset で loop を呼ぶのは、over のときに loop が止まっているからです。止まっていない状態で呼ぶと、二重に動きます。over を確認する部分は変えないでおきます。

調整の判断基準:遊べるかを自分で測る

動いたあとの課題は、「面白いか」を何で判断するかです。主観だけでは迷うので、簡単な記録を取ります。

  1. 同じ設定で5回遊び、生存秒数をメモします。1回ごとに表示される秒数をそのまま書き写せば足ります。
  2. 数字を1つだけ変えます。たとえば降る速度の 3 を 4 にします。
  3. また5回遊び、メモを並べます。

読み方の目安は次のとおりです。

  • 5回とも数秒で終わるなら、難しすぎる可能性があります。落下の速度を下げるか、降る間隔(frames % 30)を広げます。
  • 5回とも長く続いて退屈なら、簡単すぎる可能性があります。間隔を狭めるか、速度を上げます。
  • 回ごとの秒数に大きなばらつきがある場合は、乱数の影響が強いか、運に左右されている可能性があります。ばらつきが気になるなら、位置の出し方を工夫します。

「終わりまでの秒数が数回の挑戦で少しずつ伸びる」状態は、上達が実感できる目安の1つです。ただし、この基準が自分のゲームに合うかどうかは、実際に遊んで確かめる必要があります。他の人に1回遊んでもらい、どこで終わったかを見るのも、短い時間でできる確認です。

次の行動

まず、上のコードを index.html として保存し、5回遊んで秒数をメモしてください。次に、速度か間隔のどちらか1つだけを変えて、もう5回遊びます。

形になったら、アソバースに投稿できます。アップロードは直下の index.html が入口で、外部通信やサーバー用コードは使えません。この記事のコードはその条件に収まります。投稿は「入力→表示の確認→公開」の3段階で、下書き保存もでき、公開にはメールアドレスの確認が必要です。最初は下書きで動作を確かめるところから始めてみてください。