反射神経ゲームの作り方:反応時間の計測と表示
遊びをつくる執筆にClaudeを使用しています。

反射神経ゲームの肝は「正確な時間計測」と「プレイヤーへのフィードバック」の2点に集約されます。見た目のはなやかさよりも、「自分の反応時間が正確に測られている」という信頼感がゲームの面白さを支えています。この記事では、ブラウザのJavaScriptで反応時間を計測し、わかりやすく表示するための具体的な実装方法と、よくある落とし穴を説明します。 反射神経ゲームには大きく分けて「合図に反応して押す」タイプと「動くターゲットをタイミングよく押す」タイプがあります。どちらも「いつ合図が出たか」と「いつプレイヤーが反応したか」の差を計測するという構造は同じです。この記事では前者の「合図に反応して押す」タイプを中心に解説し、後者への応用方法も末尾で触れます。
反応時間の計測に使う道具
ブラウザで高精度な時刻を取得するには performance.now() を使います。Date.now() はミリ秒単位の整数しか返しませんが、performance.now() はサブミリ秒の精度を持ち、ページ読み込みからの経過時間を小数点以下まで返します。反射神経ゲームで人間の反応時間(一般に150〜400ms程度)を扱う場合、この精度の差は実用上ほぼ無視できますが、performance.now() を使う習慣にしておくと、後から精度を要求する実装に発展させやすくなります。
計測の基本パターンは次の通りです。
- 合図を表示した瞬間に startTime = performance.now() を記録する。
- プレイヤーが入力した瞬間に endTime = performance.now() を記録する。
- reactionTime = endTime - startTime を計算してミリ秒値として扱う。
この3ステップだけで計測自体は完成します。問題はこれを「どのイベントで取得するか」と「どう表示するか」にあります。
イベントの選び方と早押し対策
キーボード入力なら keydown、マウスやタッチなら pointerdown または touchstart を使います。click イベントは「押して離した」タイミングで発火するため、mousedown より数十ms遅れる場合があります。反応時間を正確に測りたいなら pointerdown を選ぶのが無難です。
一方で「合図が出る前に押す」フライング(フォールスタート)への対処が必要です。実装方法は主に2通りあります。
フライング検出の実装パターン
パターンA:入力を無視する合図が出るまでの待機中は入力自体を受け付けず、startTime が未設定のときに入力があれば「フライング」と表示してラウンドをリセットします。
if (!startTime) {
showMessage("フライング!もう一度");
reset();
return;
}パターンB:ペナルティを加算するフライングが発生しても計測を継続し、反応時間にペナルティ(例:500ms加算)を乗せてスコアを下げます。カジュアルなゲームで途中でリセットしたくない場合に向いています。
どちらを選ぶかはゲームのトーンによります。競技色が強いなら A、気軽に遊べるものにしたいなら B が合っています。
合図のタイミングをランダムにする理由
合図が一定間隔で出ると、プレイヤーはタイミングを予測して押せてしまいます。setTimeout の待機時間をランダムにするだけで予測押しを防げます。
const delay = 1500 + Math.random() * 2000; // 1.5〜3.5秒のランダム待機
setTimeout(showSignal, delay);最小待機時間を短くしすぎると(例:0〜1秒)前のラウンド終了直後に合図が出てフライングが頻発します。1.5秒以上を下限にするとプレイ感が安定します。
結果の表示:数字だけでは伝わらない
「243ms」という数字をそのまま表示しても、プレイヤーはそれが速いのか遅いのか判断できません。反応時間の表示には「評価ラベル」と「視覚的なフィードバック」を組み合わせるのが効果的です。
評価ラベルの設計例
反応時間に応じてメッセージを変えるシンプルな方法です。閾値はゲームの難易度感に合わせて自由に設定できますが、一般的な目安として次のような区切りが使いやすいです。
- 200ms 未満、評価例:超人的!
- 200〜250ms、評価例:素晴らしい
- 250〜300ms、評価例:良い
- 300〜400ms、評価例:普通
- 400ms 以上、評価例:もう少し
この閾値はあくまで一例です。ターゲット読者層(子ども向けか上級者向けかなど)に合わせて調整してください。
視覚的なフィードバックの作り方
数字と評価ラベルに加えて、背景色や文字色を変えるだけで直感的な満足感が生まれます。例えば「200ms未満なら画面が金色に光る」「400ms以上なら画面が少し揺れる」といった演出です。CSSの transition と animation を短時間(0.3〜0.5秒)で組み合わせると、反応が遅れても「悔しいからもう一度やる」という動機につながります。
複数回の平均を出す
1回の結果だけでなく「5回の平均」を表示すると、プレイヤーが継続してプレイする理由になります。配列に結果を蓄積し、平均値を計算するだけで実装できます。
results.push(reactionTime);
const avg = results.reduce((a, b) => a + b, 0) / results.length;平均の他に「ベスト記録」を別途保持しておくと、改善の手応えをより強く感じてもらえます。
動くターゲットへの応用
「動くものを押す」タイプの場合も基本構造は同じで、「ターゲットが正解位置に来た瞬間」を startTime として扱います。ただし完全な反応時間ゲームというよりも「タイミングゲーム」に近く、計測値はずれた距離(ピクセルやdeg)で表すほうが自然です。performance.now() で取ったタイムスタンプの差を、ターゲットの移動速度と掛け合わせてピクセルずれに換算する方法が一般的です。
反応時間ゲームとタイミングゲームは混在させると評価基準が曖昧になるため、最初は一方に絞って完成させるほうがデバッグも表示設計もシンプルに保てます。
実装を壊しやすいポイント
setTimeout のドリフト:setTimeout は指定時間通りに実行される保証がなく、ブラウザのタブが非アクティブのときや高負荷時は大幅に遅れることがあります。startTime は setTimeout のコールバック内で記録するのではなく、合図を実際に表示する処理が完了した直後に記録することで、この誤差を最小化できます。
タッチデバイスでの遅延:モバイルブラウザは click イベントに約300msの遅延を加える場合があります(ダブルタップズームの検出のため)。touch-action: manipulation をCSSに設定するか、touchstart を直接使うことで回避できます。
表示コストによるラグ:結果表示のDOM操作が重いと、入力を受け取ってから endTime を記録するまでの間に処理が割り込む場合があります。endTime の記録はイベントハンドラの先頭1行目で行い、表示処理はその後に書くのが基本です。
次の行動
performance.now() と pointerdown を使った最小構成の計測ループをまず1本作り、フライング検出と評価ラベルを追加して動作を確認してください。完成したHTMLファイルはそのままアソバースへ投稿でき、ほかのプレイヤーに反応時間を試してもらいながら閾値や演出を調整していくことができます。

