マガジン

敵を出さないアクションゲームを作る

遊びをつくる

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

色紙の試作コース、小さな駒、ノートを並べたゲーム制作の作業机。
記事のテーマをもとに制作したAI生成イメージ。実際のゲーム画面ではありません。

小さな四角を矢印キーで動かし、画面に現れる光の粒に触れて集めるだけのゲームを考えてみます。追いかけてくる相手も、撃ってくる弾もありません。これを遊んだ人が「あと少しで取れたのに」と声を出すのは、どんな仕掛けを足したときでしょうか。 敵はアクションゲームの緊張を作る手っ取り早い部品です。当たったら負けるという条件が、動きの一つひとつに意味を与えます。敵を外すと、残るのは「動く気持ちよさ」だけになり、緊張が消えて作業になりかねません。ただ、緊張の正体が「敵」ではなく、「失うかもしれない」と「間に合わないかもしれない」だとすれば、敵以外の部品でも代わりが務まるはずです。 この記事は結果を報告するものではなく、実験の設計書です。同じ土台に緊張の源を一つずつ足した4つの版を作り、遊んだ人の記録から「どの源が効いたか」を自分で読み取る手順を示します。

この記事の目次
  1. 問いを絞る:何を「敵」とみなし、何を比べるか
  2. 準備するもの:土台のコードと4条件
  3. 手順:4条件を同じ順序にならない形で遊んでもらう
  4. 記録の取り方:何を、どの単位で残すか
  5. 結果の読み方と、誤りやすい点
  6. 次の行動

問いを絞る:何を「敵」とみなし、何を比べるか

最初に「敵を出さない」の範囲を決めます。この実験では、意思を持って自機に近づくもの、自機を狙うもの、触れると即座に失敗にするもの、を敵とみなして使いません。決めておかないと、途中で「動く障害物なら敵ではない」と抜け道を作ってしまい、何を比べたのか分からなくなります。

比べる緊張の源は、次の3つに絞ります。

  • 消える目標、仕組み:粒が一定時間で別の場所へ移る、失う可能性:取り逃し
  • 有限の資源、仕組み:動くほど燃料が減り、粒で少し戻る、失う可能性:動けなくなること
  • 揺れる環境、仕組み:周期的な横風が自機を押す、失う可能性:狙いのずれ

これに、何も足さない基準の版(base)を加えて4条件にします。基準がないと「緊張が出た」のか「そもそも粒集めが忙しい」のかを区別できません。

問いは次の形にします。「基準と比べて、どの源を足した版で、遊んだ人の緊張の自己評価が上がるか。そして、その緊張は楽しさと両立しているか」。後半が大切です。緊張が上がっても不快になっていれば、設計として成功とは言えません。

公式作品にも、この種の発想を読み取れるものがあります。たとえば「紙ひこうき郵便」は、風の異なる3ルート、制限時間、風船にぶつかると紙が傷み3回で終了、最後の6秒で赤い郵便受けへ、という作りです。また「月夜の配達」は、燃料が有限で1区間につき燃料1を使う経路パズルです。どちらも設計から読み取れるのは、時間や資源の「限り」で緊張を作っているらしいということです。細かい挙動や調整は、作品を開いて確かめてください。

準備するもの:土台のコードと4条件

用意するのは、ブラウザと1つのHTMLファイルです。下のコードは、MODE の値を書き換えるだけで4条件を切り替えられる最小構成です。外部ライブラリもサーバーも使いません。

html
<canvas id="c" width="480" height="320" style="background:#123"></canvas>
<script>
const MODE = 'base'; // 'base' | 'decay' | 'fuel' | 'wind'
const ctx = c.getContext('2d'), keys = {};
let p = {x:240, y:160}, score = 0, fuel = 100, over = false;
let t0 = performance.now(), orbAt = t0, orb = spawn(), log = [];
function spawn(){ return {x:30+Math.random()*420, y:30+Math.random()*260}; }
addEventListener('keydown', e => keys[e.key] = true);
addEventListener('keyup', e => keys[e.key] = false);
function step(now){
  const sec = (now - t0) / 1000;
  if (!over) {
    let dx = (keys.ArrowRight?1:0) - (keys.ArrowLeft?1:0);
    let dy = (keys.ArrowDown?1:0) - (keys.ArrowUp?1:0);
    let sp = 3;
    if (MODE === 'fuel') {
      if (fuel <= 0) sp = 1;
      else if (dx || dy) fuel -= 0.2;
    }
    p.x += dx * sp + (MODE === 'wind' ? Math.sin(sec * 1.5) * 1.5 : 0);
    p.y += dy * sp;
    p.x = Math.min(472, Math.max(8, p.x));
    p.y = Math.min(312, Math.max(8, p.y));
    if (MODE === 'decay' && now - orbAt > 3000) { orb = spawn(); orbAt = now; log.push('x' + sec.toFixed(1)); }
    if (Math.hypot(p.x - orb.x, p.y - orb.y) < 14) {
      score++; log.push(sec.toFixed(1)); orb = spawn(); orbAt = now;
      if (MODE === 'fuel') fuel = Math.min(100, fuel + 12);
    }
    if (sec >= 30) over = true;
  }
  draw(sec);
  requestAnimationFrame(step);
}
function draw(sec){
  ctx.clearRect(0, 0, 480, 320);
  ctx.fillStyle = '#ffd'; ctx.beginPath(); ctx.arc(orb.x, orb.y, 7, 0, 7); ctx.fill();
  ctx.fillStyle = '#8cf'; ctx.fillRect(p.x - 6, p.y - 6, 12, 12);
  ctx.fillStyle = '#fff';
  ctx.fillText(MODE + ' 粒:' + score + ' 残り:' + Math.max(0, 30 - sec).toFixed(1) + (MODE === 'fuel' ? ' 燃料:' + fuel.toFixed(0) : ''), 8, 14);
  if (over) ctx.fillText('終了 ' + log.join(' '), 8, 30);
}
requestAnimationFrame(step);
</script>

各条件の数値は、出発点として置いたものです。

  • base:粒は取るまで同じ場所に残ります。
  • decay:粒は3秒取られないと別の場所へ移ります。移った時刻はログに「x」付きで残ります。
  • fuel:動くと1フレームあたり燃料が0.2減ります。60fpsの画面では毎秒およそ12です。粒を取ると12戻ります。空になると速度が3から1に落ちるだけで、操作は止まりません。操作が止まる設計にすると、詰んだ人は残り時間を眺めるだけになり、緊張どころではなくなります。
  • wind:横方向に、約4秒周期で最大1.5の力が左右に押します。自機の速度3の半分です。

全条件で、1回は30秒固定です。時間まで揃えておくと、粒数を直接比べられます。

遊ぶ人は、同じ条件の人を最低3人は集めてください。自分一人だと、作った本人は動きを知りすぎていて、評価が甘くなります。

手順:4条件を同じ順序にならない形で遊んでもらう

手順は次のとおりです。

  1. 遊ぶ人に、粒を触れて集めるゲームであることだけを伝えます。条件の違いや、何を測っているかは伝えません。
  2. 練習として、base を2回遊んでもらいます。この2回は記録に使いません。操作に慣れる前の成績は、条件の差ではなく慣れの差を映すためです。
  3. 本番では、4条件を1回ずつ、人ごとに順序を変えて遊んでもらいます。たとえば1人目は base → decay → fuel → wind、2人目は wind → base → decay → fuel のように回します。
  4. 1条件を遊ぶごとに、緊張の自己評価を0〜5で聞き、「いちばん手に汗をかいた瞬間」を一言で書いてもらいます。
  5. 4条件を1周したら、2周目を同じ順序の逆で行います。1条件につき1人あたり2回分のデータになります。

条件を変えるたびに MODE を書き換えてリロードするのが面倒なら、location.hash から値を読むように変えてもかまいません。ただし、コードを変えたら、それ以降の記録は別実験として扱ってください。

記録の取り方:何を、どの単位で残すか

記録は1回ごとに1行です。次の列を用意します。

  • 遊んだ人、内容:匿名の番号、取り方:事前に割り振ります
  • 条件、内容:base / decay / fuel / wind、取り方:画面左上の表示を見ます
  • 周回、内容:1周目か2周目か、取り方:手で書きます
  • 粒数、内容:30秒で集めた数、取り方:画面に表示される数です
  • 取得時刻、内容:終了画面のログ、取り方:画面を写して書き写します
  • 緊張評価、内容:0〜5、取り方:遊び終えた直後に聞きます
  • 一言、内容:手に汗をかいた瞬間、取り方:本人の言葉のまま書きます
  • 観察メモ、内容:手が止まった、声が出た等、取り方:横で見ている人が書きます

取得時刻のログは、粒数だけでは見えない情報を持っています。取得の間隔が後半に詰まるのか、伸びるのか、decay で「x」が連続したのはどの辺りか、といった時間の使い方が分かります。

評価の聞き方も揃えてください。「どのくらい緊張しましたか」と毎回同じ言葉で聞き、「面白かったですか」とは別の質問にします。緊張と楽しさを一つの問いにすると、読み取りが混ざります。楽しさは0〜5でもう1列追加しておくと、後で両立を見られます。

結果の読み方と、誤りやすい点

集計の前に、判断基準を先に決めます。結果を見てから基準を決めると、都合のよい解釈ができてしまうからです。たとえば「baseより平均で1点以上、緊張評価が高ければ効いたとみなす」のように、自分で数値を置いてください。1点という値に根拠はありません。人数や評価の幅に合わせて決めます。

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

  • 緊張評価が基準より上がり、楽しさの評価が下がっていない条件は、緊張の源として使えます。
  • 緊張評価は上がったが楽しさが下がった条件は、緊張ではなく不快を作っています。「一言」の欄に理不尽、イライラといった言葉が並んでいないか確認してください。
  • 評価が基準とほぼ同じ条件は、その数値では効いていません。数値を強める前に、プレイヤーがその仕掛けに気づいていたかを観察メモで確かめます。
  • 粒数が条件ごとにあまり変わらないのに緊張評価が変わるなら、緊張は成績ではなく心理的な圧から生まれています。
  • 取得間隔のログで、燃料が少ない時期に慎重な動きに変わっていれば、資源が行動を変えたことを示す一つの手がかりです。ただし、これも観察メモと照らして判断してください。

誤りやすい点は、次のとおりです。

  • 順序効果:最後に遊んだ条件が高く出るのは、慣れや疲れのせいかもしれません。順序を回しているのは、そのためです。
  • 風が強すぎる:wind の押す力が大きいと、風が敵のように感じられます。「風に邪魔された」という一言が多ければ、力を弱めて、別の実験として再度遊んでもらいます。
  • decay の再配置の理不尽さ:取ろうとした瞬間に粒が消えると、不公平に感じやすくなります。消える直前に点滅させるなどの予告は、別の変数になるので、この実験には入れません。
  • フレームレート依存:このコードの速度と燃料消費は、1フレームあたりの値です。60fps以外の画面では、燃料の減り方が変わります。遊ぶ画面を揃えるか、時間で割る形に変えて、それ以降は別実験とします。
  • 途中での数値変更:「少し難しすぎたから」と途中で数値を変えると、前後の記録が比べられなくなります。変えるなら全員分やり直します。
  • 人数の少なさ:3人程度の記録は、傾向のメモにはなっても、結論にはなりません。「この版は次の実験に残す」という判断の材料として使ってください。

次の行動

まずコードを保存し、MODE を変えて、自分で4条件を1回ずつ触って挙動を確かめてください。そのうえで、判断基準を先に書き出し、3人に遊んでもらう計画を立てます。効いた源が見つかったら、その源を二つ組み合わせた版を、次の実験として作ってみてください。