マガジン

「連続プレイ」を作り込む:結果からすぐ次へ

遊びをつくる

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

一つだけ空いた場所へ、丸い駒を置こうとする初めての一手。
記事のテーマをもとに制作したAI生成の写真風イメージ。

短いゲームを作って遊ばせると、結果画面で「もう一回」を押した直後に、前の回の何かが残っていることがあります。残り時間が半端な値から始まる、得点が前回の続きから増える、1回のクリックで2点入る。こうした不具合は、1回目のプレイでは絶対に見つかりません。2回目以降に初めて現れます。 連続プレイの作り込みには、二つの仕事があります。一つは結果画面から次のプレイまでの距離を縮める「導線」です。もう一つは、前の回の状態を過不足なく片づける「リセット」です。導線だけ短くしても、リセットが雑だと「もう一回」が不具合の入口になります。リセットだけ丁寧でも、導線が長ければ遊び手は画面を閉じます。 この記事では、5秒間ボタンを連打する最小のゲームを土台に、二つを順に作ります。最後に、10回連続で遊んで不具合を洗い出す確認手順も用意しました。コードは単独のHTMLファイル1枚で動きます。

この記事の目次
  1. 1. 何を残し何を捨てるかを先に決める
  2. 2. 状態を一か所にまとめる
  3. 3. 結果画面から一操作で戻す
  4. 4. 10回連続で検証する
  5. 5. 記録を残すか、作品を開いて確かめるか
  6. 次の行動

1. 何を残し何を捨てるかを先に決める

コードを書く前に、ゲームが持っている状態を書き出して三つに分けます。最初にこの仕分けをしておくと、あとで「リセットし忘れ」を探すときの基準になります。

  • 毎回捨てるもの、例:得点、残り時間、ミス回数、進行中のタイマー、扱い:新しい回の開始時に必ず初期値へ戻す
  • 回をまたいで残すもの、例:最高記録、選んだ難易度、音のオン/オフ、扱い:意図して持ち越す。置き場所を分ける
  • 迷うもの、例:前回の盤面、直前の操作の履歴、ヒントの使用回数、扱い:作品の性格で決める。決めた理由を一行で書いておく

「迷うもの」は、公式作品の機能名から考え方を借りられます。たとえば「かたちのあいだ」には「同じ配置の再挑戦」があります。ここから、やり直しには「同じ問題をもう一度」と「新しい問題で最初から」の二種類がありうると読み取れます。前者は問題の初期配置を残し、手数だけを捨てます。後者は両方を捨てます。ボタンを一つにするか二つに分けるかは、この仕分けで決まります。

「小さな軌道」には、前回の軌道を細い実線で見せる機能があります。これは前の回の情報を、意図して次の回に持ち込む設計と読めます。持ち越しは、不具合とは限りません。ただし、持ち越すものは「持ち越す」と決めたデータとして管理します。

手順1:状態の一覧を作る

  1. ゲーム中に変化する値を、変数名ごとにすべて書き出します。タイマーのID、イベントの登録、アニメーションの進行状態も含めます。
  2. 各行に「捨てる」「残す」「迷う」のどれかを書きます。
  3. 「迷う」には、遊び手が困る場面と、助かる場面を一つずつ書いて判断します。
  4. 「残す」ものは、ページを閉じても必要かを確認します。必要なら保存が要ります(後述)。

2. 状態を一か所にまとめる

仕分けができたら、「毎回捨てるもの」を一つのオブジェクトに集め、新しい回の開始時にまるごと作り直します。個別の変数を一つずつ0に戻す書き方は、変数が増えたときに戻し忘れが出ます。

次のコードが土台です。5秒間、ボタンを押した回数が得点になります。「残す」ものは best に、「捨てる」ものは run に分けています。

html
<!doctype html>
<meta charset="utf-8">
<title>連続プレイの骨組み</title>
<section id="play">
  <p>残り <span id="t"></span> 秒 / 得点 <span id="s"></span></p>
  <button id="hit">押す</button>
</section>
<section id="result" hidden>
  <p id="msg"></p>
  <button id="again">もう一回</button>
</section>
<script>
const best = { score: 0 };      // 回をまたいで残す
let run = null;                 // 毎回作り直す
let lockUntil = 0;              // 結果直後の誤入力を防ぐ
const $ = (id) => document.getElementById(id);

function newRun() { return { score: 0, timeLeft: 5, timerId: null }; }
function draw() { $("t").textContent = run.timeLeft; $("s").textContent = run.score; }

function start() {
  if (run && run.timerId) clearInterval(run.timerId);
  run = newRun();
  run.timerId = setInterval(tick, 1000);
  $("play").hidden = false;
  $("result").hidden = true;
  $("hit").focus();
  draw();
}
function tick() {
  run.timeLeft--;
  if (run.timeLeft <= 0) return finish();
  draw();
}
function finish() {
  clearInterval(run.timerId);
  run.timerId = null;
  const isBest = run.score > best.score;
  if (isBest) best.score = run.score;
  $("msg").textContent = `得点 ${run.score}` + (isBest ? "(最高記録)" : `(最高 ${best.score})`);
  $("play").hidden = true;
  $("result").hidden = false;
  lockUntil = performance.now() + 500;
  $("again").focus();
}
// イベントは起動時に一度だけ登録する
$("hit").addEventListener("click", () => {
  if (run && run.timerId) { run.score++; draw(); }
});
$("again").addEventListener("click", () => {
  if (performance.now() >= lockUntil) start();
});
start();
</script>

このコードには、リセットで事故が起きやすい箇所に対する備えが4つ入っています。

  • 状態の作り直し:newRun() が毎回新しいオブジェクトを返すので、戻し忘れる変数が構造上できません。
  • タイマーの後始末:start() の冒頭と finish() で clearInterval を呼びます。前の回のタイマーが生きたまま新しいタイマーを作ると、時間が倍速で減ります。
  • イベントの登録位置:addEventListener は start() の外、起動時に一度だけ呼びます。start() の中に書くと、回を重ねるごとに登録が増え、1クリックで得点が2、3と増えます。
  • 終了後の入力ガード:run.timerId が null のとき、hit は何もしません。

手順2:自作ゲームに当てはめる

  1. 手順1の「捨てる」行を、newRun() と同じ形のオブジェクトにまとめます。
  2. 開始処理を start() の一つに集約します。初回起動も「もう一回」も、同じ関数を通します。
  3. タイマー、requestAnimationFrame、setTimeout のIDは、すべて run の中に持たせ、終了時と開始時に止めます。
  4. addEventListener が関数の中に入っていないか、全文を検索して確認します。

初回起動と再開始で別の経路を使うと、初回だけ正しく動いて2回目以降が壊れる、という典型的な構造になります。

3. 結果画面から一操作で戻す

導線の目標は、結果が見えた状態から次のプレイが始まるまでを、操作1回にすることです。測り方は簡単で、結果画面で「次のプレイが始まるまでに何回クリックまたはキーを押したか」を数えます。メニューを経由する、確認ダイアログが出る、といった理由で2回以上になっていれば、削れる余地があります。

手順3:導線を整える

  1. 結果画面に「もう一回」のボタンを置き、finish() の最後でそのボタンにフォーカスを移します(上のコードの $("again").focus())。これでEnterキーやスペースキーだけで再開できます。
  2. ボタンの文言は「もう一回」のように、次に起きることが分かる言葉にします。「OK」や「閉じる」では、再開なのか終了なのか判断できません。
  3. 「別のコースを選ぶ」「タイトルへ戻る」は、「もう一回」より目立たない位置と大きさにします。主たる行動を一つ決めて、残りは脇に置きます。
  4. 最高記録の更新は結果画面で見えるようにします。更新したか否かは、次の回へ向かう動機になります。

手順4:連打による「飛ばし」を防ぐ

見落としやすいのが、プレイ中にEnterやスペースを連打していた遊び手の手が止まらず、結果画面が出た瞬間に「もう一回」が押されてしまう問題です。結果を一度も見ないまま次の回が始まります。

対策は、結果表示の直後に短い入力無効時間を置くことです。上のコードでは lockUntil で500ミリ秒としています。この値は固定の正解ではありません。長すぎると「押したのに反応しない」と感じられ、短すぎると飛ばしが起きます。自分のゲームの操作(連打型か、じっくり考える型か)に合わせて、手元で調整してください。じっくり考えるパズルなら、ほぼ不要な場合もあります。

4. 10回連続で検証する

実装したら、1回ごとの動作ではなく「連続」で確かめます。連続プレイの不具合は、回数を重ねて初めて見えるものが大半です。

手順5:記録表を作って10回遊ぶ

  1. 紙か表計算に、列を作ります。「回」「開始直後の得点」「開始直後の残り時間」「1クリックで増えた得点」「残り時間が1減る間隔(体感で1秒か、速いか)」「結果画面で記録表示が正しいか」。
  2. 「もう一回」で、同じ操作を10回続けます。途中で画面を再読み込みせず、再開ボタンだけで進めます。
  3. 各回の開始直後と、1回目のクリック直後の値を記録します。
  4. 次に、中断を混ぜた回も3回ほど作ります。プレイの途中で一時停止やブラウザの別タブへの切り替えを行い、戻って最後まで遊び、その後の回の開始値を見ます。
  5. 可能なら、キーボードだけで10回、マウスだけで10回、それぞれ行います。

結果の読み方

記録の見方は、症状から原因を逆引きする形で整理できます。

  • 開始直後の得点が0でない:newRun() を通っていない変数があります。状態の一覧に漏れがないか見直します。
  • 1クリックで得点が2以上増える:イベント登録の重複です。addEventListener が start() の中にないか確認します。
  • 回を重ねるごとに残り時間の減りが速くなる:前の回のタイマーが止まっていません。clearInterval の呼び出し箇所を確認します。
  • 結果画面を見ないうちに次が始まる:入力無効時間がありません(手順4)。
  • 最高記録が突然0に戻る:記録を run の側に置いている可能性があります。「残す」ものの置き場所を確認します。
  • 中断の後だけ値がおかしい:一時停止やタブ切り替え時の処理が、タイマーの停止と再開で食い違っています。

原因を特定したら、直した後に同じ表で再度10回遊びます。1か所を直して別の場所が壊れることがあるためです。

5. 記録を残すか、作品を開いて確かめるか

最高記録を持ち越す場合は、「いつまで残るか」を決めておく必要があります。上のコードの best は、ページを開いている間だけ有効です。再読み込みで消えます。

アソバースでは、アップロードしたゲーム内の localStorage や sessionStorage は、一時メモリーとして動く互換処理の扱いです。永続的な保存先として当てにしないでください。ずっと残したい記録は、クラウドセーブ(作品ごとにJSONで128KiBまで)の対象にします。最高記録の数個の数値なら、容量には十分な余裕があります。ただし、保存する内容を増やすほど、「どれを回ごとに捨て、どれを保存するか」の仕分けが大事になります。保存の呼び出し方は、現在の投稿ガイドで確認してください。

公式作品を開いて、連続プレイの作り方を観察する視点も挙げておきます。結果は作品ごとに異なるので、実際に開いて確かめてください。

  • こだまの森:最高記録はゲームを開いている間だけ残る設計です。コースを変えて続けて遊び、最高記録がどう扱われるかを見ます。
  • 時計じかけの台所:時間制限のない見習いと、制限のある本番があります。皿を終えたあと、モードが引き継がれるか確かめます。
  • かたちのあいだ:同じ配置の再挑戦ができます。再挑戦と新しい配置で、何が戻り何が残るかを比べます。
  • 小さな軌道:前回の軌道が細い実線で残ります。残すものを意図的に決めている例として観察します。

観察の際は、結果画面から次の操作までの回数と、次の回の開始直後に初期化されているものを、手順5の表の形式で記録すると比較しやすくなります。

次の行動

まず自分のゲームの状態を書き出し、「捨てる」「残す」「迷う」に分けてください。次に、開始処理を一つの start() にまとめ、手順5の表で10回連続の確認をします。すべての行が想定どおりになったら、投稿画面で表示を確認してから公開に進みます。