マガジン

「ステージ選択」を作り込む:解放・未解放・クリア済みの表示

遊びをつくる

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

一人が小さなパズルを試し、制作者がノートを持って様子を観察するテストプレイ風景
記事のテーマをもとに制作したAI生成の写真風イメージ。

ステージが8つあるゲームを作り、ステージ選択画面に8個のボタンを並べた場面を考えてみます。最初に遊べるのは1つだけで、残りは順番に開いていく仕組みです。このとき、ボタンの見た目を「灰色にする」だけで済ませると、遊ぶ人は「壊れているのか、まだ遊べないのか」を判断できません。クリア済みのステージが他と同じ見た目だと、「自分がどこまで進んだか」も伝わりません。 ステージ選択は、ゲームの中で唯一「進み具合」だけを見せる画面です。この記事の問いは、解放・未解放・クリア済みの3状態をどうデータで持ち、どう見せれば、遊ぶ人が迷わず次の一手を選べるか、です。 結論を先に言うと、保存するのは「クリアしたステージの集合」だけにして、解放状態はそこから毎回計算します。見せ方は色だけに頼らず、記号と文字を併用し、未解放を押したときには理由を返します。以下の手順で、単独で動く最小のHTMLを作りながら確かめます。

この記事の目次
  1. 手順1:状態を三つに絞り、保存するものを一つに決める
  2. 手順2:最小のステージ選択画面を作る
  3. 手順3:動かして、三つの状態を順に確かめる
  4. 手順4:未解放の見せ方を、ゲームに合わせて選ぶ
  5. 手順5:アソバースに置くときの保存の考え方
  6. 手順6:公式作品でステージ選択の見せ方を観察する
  7. 次の行動

手順1:状態を三つに絞り、保存するものを一つに決める

最初にコードを書く前に、状態の持ち方を決めます。

  1. 状態を「未解放(locked)」「挑戦可(open)」「クリア済み(cleared)」の3つに分けます。「挑戦可」は、解放済みでまだクリアしていない状態です。ここを分けておくと、「次に遊ぶのはここ」という目印を出せます。
  2. 保存するのは「クリアしたステージ番号の集合」だけにします。「解放済みステージの一覧」や「現在の到達番号」は保存しません。
  3. 解放状態は、集合と解放ルールから毎回計算します。今回のルールは「ステージ1は常に解放、それ以外は一つ前をクリアしていれば解放」です。

「到達番号」を保存する設計は手軽ですが、後から困ります。たとえば「ステージ5は、ステージ2と3の両方をクリアしたら解放」というルールに変えたくなったとき、到達番号では表現できません。クリア済みの集合だけを持っていれば、ルールを関数として差し替えるだけで済み、保存データの形は変わりません。

状態ごとの見せ方と振る舞いを、先に表で決めておきます。

  • 未解放、条件:解放ルールを満たさない、見た目:破線の枠、薄い色、🔒、押したとき:遊べない理由を表示する、読み上げ用の文言:「ステージN 未解放」
  • 挑戦可、条件:解放済みで未クリア、見た目:実線の枠、▶、押したとき:ステージを開始する、読み上げ用の文言:「ステージN 挑戦可」
  • クリア済み、条件:クリアの集合に含まれる、見た目:緑の背景、✓、押したとき:再挑戦として開始する、読み上げ用の文言:「ステージN クリア済み」

この表の要点は、状態を「色」「枠線」「記号」「文言」の複数の手がかりで区別していることです。色の見分けにくい人や、画面を白黒に近い設定で見ている人でも、記号と文字で判断できます。

手順2:最小のステージ選択画面を作る

次のコードを、stage-select.html という名前で保存してください。外部ライブラリは不要で、ブラウザで開けばそのまま動きます。

html
<!DOCTYPE html>
<meta charset="utf-8">
<title>ステージ選択</title>
<style>
 body{font-family:sans-serif;padding:1rem}
 #grid{display:grid;grid-template-columns:repeat(4,5.5rem);gap:.5rem}
 button{padding:.6rem;font-size:1rem;border:2px solid #345;border-radius:8px;background:#fff}
 button[data-s="locked"]{border-style:dashed;color:#666;background:#eee}
 button[data-s="cleared"]{background:#dff3e0}
 button:focus-visible{outline:3px solid #e90}
</style>
<div id="grid"></div>
<p id="msg" role="status"></p>
<button id="reset">最初から</button>
<script>
const N = 8, MARK = {locked:'🔒', open:'▶', cleared:'✓'};
const LABEL = {locked:'未解放', open:'挑戦可', cleared:'クリア済み'};
let cleared = new Set();
const grid = document.getElementById('grid'), msg = document.getElementById('msg');
const stateOf = i => cleared.has(i) ? 'cleared'
  : (i === 0 || cleared.has(i - 1)) ? 'open' : 'locked';
const serialize = () => JSON.stringify({version: 2, cleared: [...cleared]});
function restore(text){
  try{
    const d = JSON.parse(text);
    if(d.version === 2 && Array.isArray(d.cleared))
      cleared = new Set(d.cleared.filter(n => Number.isInteger(n) && n >= 0 && n < N));
  }catch(e){}
}
const btns = [...Array(N)].map((_, i) => {
  const b = document.createElement('button');
  b.onclick = () => play(i);
  grid.append(b);
  return b;
});
function render(){
  btns.forEach((b, i) => {
    const s = stateOf(i);
    b.dataset.s = s;
    b.textContent = MARK[s] + ' ' + (i + 1);
    b.setAttribute('aria-label', 'ステージ' + (i + 1) + ' ' + LABEL[s]);
    b.setAttribute('aria-disabled', s === 'locked');
  });
}
function play(i){
  if(stateOf(i) === 'locked'){
    msg.textContent = 'ステージ' + (i + 1) + 'は未解放です。ステージ' + i + 'をクリアすると遊べます。';
    return;
  }
  cleared.add(i);
  msg.textContent = 'ステージ' + (i + 1) + 'をクリアしました(テスト用)。';
  render();
}
document.getElementById('reset').onclick = () => { cleared = new Set(); render(); };
render();
</script>

play は本来、ステージを開始する関数です。ここではテスト用に「押すとクリアになる」処理を入れています。実際のゲームでは、ステージの終了時に cleared.add(番号) を呼び、選択画面へ戻って render() を呼ぶ形にします。

構造のポイントは3つあります。

  • 状態を返す stateOf は、クリア集合だけを材料にしています。解放ルールを変えたくなったら、この関数だけを直します。
  • ボタンは最初に8個作り、render では中身と属性を更新するだけです。毎回作り直すと、キーボードで操作中のフォーカス位置が失われます。
  • 未解放のボタンには disabled を使わず、aria-disabled を付けています。disabled のボタンはフォーカスできず、押しても反応しないため、理由を伝えられません。

手順3:動かして、三つの状態を順に確かめる

ブラウザでファイルを開き、次の順に操作してください。

  1. 開いた直後の見た目を確認します。ステージ1だけが「▶ 1」の実線で、2〜8は「🔒」の破線になっているはずです。
  2. 🔒の付いたステージ5を押します。画面下に「ステージ5は未解放です。ステージ4をクリアすると遊べます。」と出れば、理由の表示は動いています。
  3. ステージ1を押します。「✓ 1」の緑の表示に変わり、ステージ2が「▶ 2」になるはずです。
  4. Tabキーでボタン間を移動し、Enterで押します。マウスなしでも同じ操作ができ、フォーカスの枠(橙色)が見えることを確認してください。
  5. ブラウザの開発者ツールのコンソールで serialize() を実行します。ステージ1と2をクリアした状態なら、{"version":2,"cleared":[0,1]} という文字列が返ります。保存されるのはこの短い文字列だけで、解放状態は含まれません。
  6. 「最初から」を押して、表示が最初の状態に戻ることを確認します。

起こりやすい誤りも挙げておきます。

  • ステージの順序を入れ替えたあとで、保存された番号とステージの対応がずれることがあります。ステージを追加・並べ替えする予定があるなら、番号ではなく "tide-3" のような固定のIDで集合を持つ設計にしてください。上のコードは短くするために番号を使っています。
  • 解放ルールを描画側にも書き写すと、表示と実際の挙動がずれます。判定は stateOf の一箇所に置き、描画も play もそれを呼ぶようにします。
  • 保存データを読み込むときに、範囲外の番号や配列でない値を弾かないと、壊れた保存で画面全体が動かなくなります。restore の検証は省かないでください。

手順4:未解放の見せ方を、ゲームに合わせて選ぶ

未解放の表示は、ゲームの性格によって適した方法が変わります。次のように、遊ぶ人に何を知らせたいかで選びます。

  • 条件を明示する:「ステージ4をクリアで解放」と書く方法です。パズルのように順に進むゲームに向きます。上のコードの方式です。
  • 存在だけ見せる:🔒とステージ番号だけ出し、条件は伏せる方法です。発見の楽しみを残したいときに向きますが、遊ぶ人が詰まったときの手がかりが減ります。
  • 見えること自体を解放にする:未解放のステージは名前も伏せ、「?」と表示する方法です。ステージ数そのものを秘密にしたいときだけ使います。

どの方法でも、「押せないものを押したときに何も起きない」状態は避けてください。何も起きないと、遊ぶ人は操作が受け付けられなかったのかと疑います。

クリア済みの扱いも決めておきます。クリア済みのステージを再挑戦できるかどうかは、選択画面の設計に直結します。再挑戦できるなら、緑の✓と「押せる」ことを両立させます。上のコードでは、クリア済みも押せます。もし再挑戦を認めないなら、押したときに「クリア済みです」と返すか、ボタンをやめて一覧表示に変えます。

さらに進めるなら、「挑戦可」と「クリア済み」に付加情報を足せます。たとえば最少手数や最速記録などです。ただし、記録は保存の範囲を決めてから出してください。保存されない記録を画面に出すと、遊ぶ人は「前に出した記録が消えた」と感じます。

手順5:アソバースに置くときの保存の考え方

アソバースに投稿した作品では、保存の扱いに注意が必要です。確認済みの範囲で整理します。

  • アップロードした作品の中の localStorage と sessionStorage は、一時メモリーの互換処理です。つまり、ブラウザ標準のような永続保存を期待せず、画面を開いている間だけ残るものとして設計するのが安全です。
  • 進行を残したいときは、アソバースのクラウドセーブを使います。作品ごとにJSONで128KiBまで保存でき、アップロードした作品にはSDKが自動で追加されます。呼び出し方は、現在の公式案内で確認してください。
  • 手順1の設計は、この制約と相性が良いです。クリア済みの番号を並べた短い文字列なので、サイズ上限に近づく心配はほとんどありません。serialize の出力をそのまま保存の中身にできます。
  • ログインしていない人の扱いは、作品側で決めておく必要があります。アソバースはログインなしでも遊べ、ゲストの履歴や進行は端末内に保存され、ログイン後のアカウントへ自動では混ざりません。ゲストの進行を引き継ぐかどうかは、画面の説明にも書いておくと親切です。

保存の形式には、最初から version を入れておいてください。コードの {version: 2, ...} がそれにあたります。あとからステージ数を変えたり形式を変えたりするときに、古い保存を見分けて移行する手がかりになります。

手順6:公式作品でステージ選択の見せ方を観察する

自分の画面を作ったら、他の作品の「進み具合の見せ方」と見比べてください。アソバースの公式作品には、ステージや段階を持つものがあります。

  • プリズムの道は8ステージ構成で、解放済みステージの選び直しができます。
  • 月夜の配達は3つの夜があり、各夜の最少区間数はプレイ中だけ記録されます。
  • 紙ひこうき郵便は海辺・丘・山の3ルートです。
  • 小さな軌道は3つの星系を持ちます。

それぞれを開いて、次の点を自分の目で確かめてください。この部分の具体的な画面は、作品を開いて確認するものです。

  1. 遊べるものと遊べないものは、何で区別されているでしょうか。色だけでしょうか、記号や文字も使われているでしょうか。
  2. 一度クリアしたものに戻れるでしょうか。戻れるなら、その入口はどこにあるでしょうか。
  3. 記録は、画面を閉じても残るでしょうか。残らない場合、その旨は画面上で分かるでしょうか。
  4. 「動きを抑える」設定にしたとき、選択画面の表示はどう変わるでしょうか。

観察の結果は、ご自身の画面設計のチェック項目に変えてください。たとえば「他の作品は未解放の理由を出していたか」を見て、自分の作品にも入れるかどうかを決める、といった使い方です。

次の行動

まず上のコードを保存し、手順3の6項目を順に確かめてください。次に、自分のゲームの解放ルールを stateOf の中に書き換えてみてください。「一つ前をクリア」から「2つ以上クリアで解放」などに変えても、保存の形が変わらないことを確かめられます。最後に、完成したゲームを一人に遊んでもらい、未解放のボタンを押したときに迷わないかを聞いてみてください。