敵を出さないアクションゲームを作る
遊びをつくる執筆にClaudeを使用しています。

小さな四角を矢印キーで動かし、画面に現れる光の粒に触れて集めるだけのゲームを考えてみます。追いかけてくる相手も、撃ってくる弾もありません。これを遊んだ人が「あと少しで取れたのに」と声を出すのは、どんな仕掛けを足したときでしょうか。 敵はアクションゲームの緊張を作る手っ取り早い部品です。当たったら負けるという条件が、動きの一つひとつに意味を与えます。敵を外すと、残るのは「動く気持ちよさ」だけになり、緊張が消えて作業になりかねません。ただ、緊張の正体が「敵」ではなく、「失うかもしれない」と「間に合わないかもしれない」だとすれば、敵以外の部品でも代わりが務まるはずです。 この記事は結果を報告するものではなく、実験の設計書です。同じ土台に緊張の源を一つずつ足した4つの版を作り、遊んだ人の記録から「どの源が効いたか」を自分で読み取る手順を示します。
この記事の目次
問いを絞る:何を「敵」とみなし、何を比べるか
最初に「敵を出さない」の範囲を決めます。この実験では、意思を持って自機に近づくもの、自機を狙うもの、触れると即座に失敗にするもの、を敵とみなして使いません。決めておかないと、途中で「動く障害物なら敵ではない」と抜け道を作ってしまい、何を比べたのか分からなくなります。
比べる緊張の源は、次の3つに絞ります。
- 消える目標、仕組み:粒が一定時間で別の場所へ移る、失う可能性:取り逃し
- 有限の資源、仕組み:動くほど燃料が減り、粒で少し戻る、失う可能性:動けなくなること
- 揺れる環境、仕組み:周期的な横風が自機を押す、失う可能性:狙いのずれ
これに、何も足さない基準の版(base)を加えて4条件にします。基準がないと「緊張が出た」のか「そもそも粒集めが忙しい」のかを区別できません。
問いは次の形にします。「基準と比べて、どの源を足した版で、遊んだ人の緊張の自己評価が上がるか。そして、その緊張は楽しさと両立しているか」。後半が大切です。緊張が上がっても不快になっていれば、設計として成功とは言えません。
公式作品にも、この種の発想を読み取れるものがあります。たとえば「紙ひこうき郵便」は、風の異なる3ルート、制限時間、風船にぶつかると紙が傷み3回で終了、最後の6秒で赤い郵便受けへ、という作りです。また「月夜の配達」は、燃料が有限で1区間につき燃料1を使う経路パズルです。どちらも設計から読み取れるのは、時間や資源の「限り」で緊張を作っているらしいということです。細かい挙動や調整は、作品を開いて確かめてください。
準備するもの:土台のコードと4条件
用意するのは、ブラウザと1つのHTMLファイルです。下のコードは、MODE の値を書き換えるだけで4条件を切り替えられる最小構成です。外部ライブラリもサーバーも使いません。
<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条件を同じ順序にならない形で遊んでもらう
手順は次のとおりです。
- 遊ぶ人に、粒を触れて集めるゲームであることだけを伝えます。条件の違いや、何を測っているかは伝えません。
- 練習として、base を2回遊んでもらいます。この2回は記録に使いません。操作に慣れる前の成績は、条件の差ではなく慣れの差を映すためです。
- 本番では、4条件を1回ずつ、人ごとに順序を変えて遊んでもらいます。たとえば1人目は base → decay → fuel → wind、2人目は wind → base → decay → fuel のように回します。
- 1条件を遊ぶごとに、緊張の自己評価を0〜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人に遊んでもらう計画を立てます。効いた源が見つかったら、その源を二つ組み合わせた版を、次の実験として作ってみてください。

