30分で作る文字落とし:降ってくる文字を打つ
遊びをつくる執筆にClaudeを使用しています。

タイピングゲームを作ろうとすると、最初に「お題の文字列を表示して、打てたら次へ」という形を思い浮かべがちです。ただ、この形は時間の圧力がなければ、ただの入力練習のままです。画面の上から文字が降ってきて、床に着くと痛い目に遭う。この一点を足すと、同じ「文字を打つ」作業が、どれから打つかを決めるアクションに変わります。 この記事では、その最小構成を30分で作ります。使うのはHTMLファイル1つだけで、外部ライブラリもサーバーも要りません。キーを押す、降ってくる文字を消す、取りこぼすとライフが減る、ライフが尽きたら終わる。この4つが動いた時点で、遊べるゲームの骨格はできています。 作る前に、確かめたいことを1つ決めておきます。「タイピングとアクションの境目は、コードのどこにあるか」です。答えは、後で調整する3つの数字(落ちる速さ、出てくる間隔、文字の種類)と、ミスにつける罰の4か所に集まります。手順の中で、それぞれを動かして確かめます。
この記事の目次
核になる問い:何が「打つ」を「遊ぶ」に変えるのか
タイピングは、正解がはっきりしている作業です。見えている文字を打てば合っているので、迷う余地がありません。迷いがないと、ゲームとしては判断が生まれません。
そこで、次の3つを足します。
- 時間切れがある:文字は一定の速さで落ち、下端に着くとライフが減ります。
- 同時に複数ある:画面に2〜3文字が同時にあると、どれから打つかを選ぶ判断が生まれます。
- ミスに代償がある:やみくもに打つと点が減るので、押す前に見る習慣が生まれます。
この3つのうち、コードに直接現れるのは速さ・出現間隔・減点の3つで、「同時に複数ある」は出現間隔を短くすると自然に起きます。つまり調整の中心は数字を触ることです。難しい仕組みは要りません。
最初は、ひらがなやローマ字の単語ではなく、アルファベット1文字から始めます。日本語入力が絡むと、変換の途中状態やIMEの挙動を扱う必要があり、30分では収まらないためです。1文字落としを先に完成させて、単語版は余裕があれば後から足します。
手順1:1つのHTMLファイルで動く土台を作る
テキストエディタで新しいファイルを作り、index.html という名前で保存します。次のコードをそのまま貼ります。後でアソバースに投稿するときも、この名前が入口になります。
<!doctype html>
<meta charset="utf-8">
<title>文字落とし</title>
<canvas id="c" width="480" height="640" style="background:#12202b;max-width:100%"></canvas>
<script>
const c = document.getElementById("c"), g = c.getContext("2d");
const LETTERS = "ASDFJKL";
let drops, score, life, spawnAt, last, over;
function reset() { drops = []; score = 0; life = 3; spawnAt = 0; over = false; last = performance.now(); }
addEventListener("keydown", e => {
if (e.key === "Enter" && over) { reset(); return; }
if (over) return;
const k = e.key.toUpperCase();
const hit = drops.filter(d => d.ch === k).sort((a, b) => b.y - a.y)[0];
if (hit) { drops.splice(drops.indexOf(hit), 1); score += 10; }
else if (k.length === 1) score = Math.max(0, score - 5);
});
function frame(now) {
const dt = Math.min((now - last) / 1000, 0.1); last = now;
if (!over) {
spawnAt -= dt;
if (spawnAt <= 0) {
drops.push({ ch: LETTERS[Math.floor(Math.random() * LETTERS.length)], x: 40 + Math.random() * 400, y: 0 });
spawnAt = Math.max(0.4, 1.2 - score / 400);
}
const speed = 60 + score / 5;
for (const d of drops) d.y += speed * dt;
drops = drops.filter(d => { if (d.y > 620) { life--; return false; } return true; });
if (life <= 0) over = true;
}
g.clearRect(0, 0, 480, 640);
g.fillStyle = "#fff"; g.font = "32px sans-serif"; g.textAlign = "center";
for (const d of drops) g.fillText(d.ch, d.x, d.y);
g.font = "18px sans-serif"; g.textAlign = "left";
g.fillText("SCORE " + score + " LIFE " + life, 10, 24);
if (over) { g.textAlign = "center"; g.fillText("GAME OVER Enterで再開", 240, 320); }
requestAnimationFrame(frame);
}
reset(); requestAnimationFrame(frame);
</script>保存したらブラウザにファイルをドラッグして開きます。文字が上から降り始め、同じ文字のキーを押すと消えれば成功です。入力は必ず半角英数モード(IMEオフ)にしてください。日本語入力がオンだと、キーの値が文字として届かず、何を押しても反応しないことがあります。
ここまでを5〜10分の目安にします。動かない場合は、ブラウザの開発者ツールのコンソールにエラーが出ていないかを見ます。貼り付けの際に引用符が全角に変わっていることが、よくある原因です。
手順2:コードの要所を3か所だけ読む
全体を理解する必要はありません。調整に関わる3か所だけ押さえます。
- keydown の中の hit:押されたキーと同じ文字を持つ落下物を集め、y が大きい(画面の下にある)ものを1つ選んで消します。同じ文字が2つあるとき、危ない方から消えるルールです。
- spawnAt = Math.max(0.4, 1.2 - score / 400):次の文字が出るまでの秒数です。点が増えるほど短くなり、0.4秒を下限にしています。
- const speed = 60 + score / 5:落ちる速さです。単位は1秒あたりのピクセル数で、点が増えるほど速くなります。
dt(前のフレームからの経過秒)を掛けて動かしているので、画面の更新頻度が違う端末でも落ちる速さがそろいます。固定値を毎フレーム足す書き方にすると、端末によって難しさが変わります。
手順3:調整を1項目ずつ動かして確かめる
ここからが核心です。1回に1つだけ数字を変え、5回ほど遊んで感触を書き留めます。記録は、紙やメモアプリに次の3項目を書けば足ります。
- 変えた数字と、変更前後の値
- ゲームオーバーまでの時間(スマートフォンのストップウォッチで十分です)
- 「忙しい」「退屈」「理不尽」のうち、どれを感じたか
3-1:文字の種類を変える
LETTERS の中身を、"ASDFJKL" から "ASDFJKLGH" のように増やします。逆に "AS" まで減らす比較もします。判断基準は、画面を見て目的の文字を探す時間が長くなったと感じるかどうかです。2種類だけだと探す時間がほぼなくなり、反射の勝負になります。9種類では、探してから押す流れになります。どちらを遊びの中心にするかを、ここで決めます。
3-2:落ちる速さの伸び方を変える
score / 5 を score / 3 に変えると、点が増えたときの加速が急になります。逆に score / 10 なら緩やかです。判断基準は、ゲームオーバーまでの時間が毎回大きくばらつくか、そろうかです。加速が緩すぎると、上手い人は延々と終わらなくなります。急すぎると、誰でも似た時間で終わり、上達の手応えが見えにくくなります。
3-3:出現間隔の下限を変える
0.4 を 0.8 や 0.25 に変えます。下限が短いと、画面に同時にいる文字が増え、「どれから打つか」の判断が頻繁に起きます。ここは、判断が生まれる頻度を決める数字です。同時に複数の文字がほとんど現れないなら、下限が長すぎる合図です。
3-4:ミスの罰を変える
score - 5 を score - 0 にして、罰のない状態を作ります。そのうえで、A S D F J K L のキーを順に連打してみます。罰がなければ、連打だけで文字が次々と消えて点が稼げるはずです。点が稼げたなら、設計の穴が確認できたことになります。罰を戻し、同じ連打で点がどうなるかを見ます。連打が損になる値を探してください。
手順4:「床」を見えるようにする
プレイヤーは、どこを越えるとライフが減るのかが見えないと、なぜ減ったのか分かりません。frame 関数の中の g.clearRect(0, 0, 480, 640); の直後に、次の1行を足します。
g.fillStyle = "#e4572e"; g.fillRect(0, 620, 480, 2);その下の g.fillStyle = "#fff" で白に戻るので、文字の色は変わりません。赤い線の位置は、判定の d.y > 620 と同じ数字です。この2つがずれると、「線を越えていないのにライフが減った」という不満につながります。数字を変えるなら両方をそろえます。
細かいことですが、「ライフが減った瞬間」も見せたくなります。余裕があれば、減った直後の0.2秒だけ背景の色を変える処理を足します。これは今回の範囲外ですが、次の改良の候補です。
手順5:つまずきやすい点を先に潰す
動いているのに遊びにくいときは、次の4つを順に確認します。
- キーが反応しない:IMEがオンになっていないか確認します。ブラウザ上の別の入力欄にフォーカスがある場合も、反応が変わることがあります。
- 大文字小文字で外れる:コードは toUpperCase() でそろえているので、CapsLock の状態では変わりません。LETTERS に小文字を入れると一致しなくなります。
- タブを切り替えて戻ると文字が固まっている:dt の上限を0.1秒にしているためです。タブを離れている間は、時間を進めない仕様になっています。
- ゲームオーバー後にすぐ再開してしまう:Enter 長押しで連続して再開します。気になるなら、再開できるまでに少し待つ処理を足します。
手順6:アソバースに出せる状態にして確認する
遊べる状態になったら、投稿前に次を確認します。
- 入口のファイル名が index.html で、素材を使う場合は相対パスで参照していること。今回は素材なしのため、この1ファイルだけで足ります。
- 外部のURLやライブラリを読んでいないこと。今回のコードは該当しません。
- 公開にはメールアドレスの確認が必要なので、アカウントの状態を事前に確かめておくこと。
- 投稿は「入力、表示の確認、公開」の3段階です。表示の確認でゲームが実際に動くことを見てから公開します。
最高スコアを残したくなる場面もあります。ただ、アソバースにはゲーム内スコアの共通ランキングはありません。画面の中に「今回のベスト」を表示する程度にするのが、現時点では素直です。アップロードした作品でのブラウザ保存は、画面を開いている間の一時的なものとして扱われます。作品ごとのクラウドセーブを使う方法は、投稿画面や公式の案内で現在の仕様を確認してください。
次の行動
まず手順1のコードを貼って動かし、手順3の4項目から1つを選んで、5回遊んで感触を書き留めてください。数字を1つ変えるだけで、遊びの印象がどれだけ変わるかを、自分の手で確かめられます。そこで「これなら人に遊んでもらえる」と思えたら、index.html をアソバースに投稿してみてください。単語版への拡張は、その後でも遅くありません。

