機能を10回足し続けると、どこから壊れ始めるか:止める基準を作る実験設計
遊びをつくる執筆にClaudeを使用しています。

小さなブラウザゲームが一本あるとします。遊べる状態になったので、スコア表示を足し、残機を足し、敵の種類を増やし、一時停止を付ける、と続けていく場面を考えてみます。どの追加も一つひとつは小さく、足している最中は順調に感じます。ところがある日、敵を一種類足しただけでリトライ後のスコアがおかしくなり、その直し方を探すだけで一晩かかる、ということが起こりえます。 この記事が確かめるのは、機能を一つずつ10回足していくと、どの追加から、何が、どんな形で崩れ始めるかです。崩れ方には動作の不具合、操作感の悪化、コードの読みにくさ、次の追加にかかる時間の増加があり、それぞれ別の指標で測れます。目的は崩れる瞬間を見つけることではなく、崩れる前に止まる、または整理に切り替えるための自分用の基準を作ることです。 ここに結果は書きません。結果は作るゲームも書き方も人によって大きく変わります。以下は、読者が自分の試作で同じ問いに答えを出すための設計書です。
「壊れる」を4つに分けて定義する
「壊れた」と曖昧に言うと、記録のたびに判断がぶれます。実験を始める前に、壊れ方を次の4種類に分けて、測り方まで決めておきます。
- 動作の回帰、何が起きるか:前に動いていたことが動かなくなる、測り方:固定チェックリストの失敗数
- 操作感の劣化、何が起きるか:入力の遅れ、動きの重さ、わかりにくさ、測り方:フレーム間隔と、遊んだ後の5段階評価
- 読みにくさ、何が起きるか:自分のコードの役割が説明できない、測り方:「この関数が何をするか説明できるまで」の秒数
- 追加コストの増大、何が起きるか:次の機能を入れるのに手間がかかる、測り方:追加にかかった分数と、触った箇所の数
回帰とは、新しい変更で既存の機能が壊れることです。この実験の中心は、4種類のうちどれが最初に動き出すかを見ることです。動作の回帰が先に出る人もいれば、動作は保たれたまま追加コストだけが先に膨らむ人もいます。順番そのものが、その人の書き方の癖を映します。
準備するもの
用意するものは次のとおりです。
- 土台のゲーム:一つのHTMLファイルで動く、機能の少ないゲームです。たとえば、落ちてくる障害物をよけるだけのもので、スコアも残機もなく、操作は左右だけにします。外部ライブラリは使いません。
- バージョンの保存場所:追加のたびにフォルダを丸ごとコピーし、v00からv10まで11個を残します。Gitを使える方はコミットでも構いませんが、後で任意の版をすぐ開いて遊べることが条件です。
- 固定チェックリスト:毎回同じ手順で確認する10項目です。例を挙げます。
- 起動して開始できる
- 左右の操作に反応する
- ゲームオーバーになる
- リトライで初期状態に戻る
- 再読み込み後に初期状態で始まる
- ブラウザ画面を拡大・縮小しても遊べる
- 狭い画面幅で表示が収まる
- 音を消しても遊べる
- 一時停止から復帰できる(追加後に有効)
- 直前に足した機能が意図どおり働く
- 記録シート:スプレッドシートか紙です。ゲーム内のlocalStorageには書かず、手元の表に残します。実験の記録を、実験対象のゲームの中に混ぜないためです。
- 計測用の小さな仕掛け:下のコードのような、フレーム間隔を表示するだけの部品です。
- 時計:追加時間と読み返し時間を測るためのストップウォッチです。
フレーム間隔を見える化する
フレーム間隔とは、画面が一回描き直されてから次に描き直されるまでの時間(ミリ秒)です。平均が大きければ全体に重く、最大値が突出していれば、ときどき引っかかっていることを意味します。次のコードは、直近300フレームの平均と最大を画面に出す単独で動く最小構成です。updateとdrawの中身は、ご自身のゲームに置き換えます。
<!doctype html>
<meta charset="utf-8">
<canvas id="c" width="320" height="180"></canvas>
<pre id="log"></pre>
<script>
const cv = document.getElementById('c');
const ctx = cv.getContext('2d');
const logEl = document.getElementById('log');
const times = [];
let last = performance.now();
let x = 0;
function update(dt) {
x = (x + dt * 0.1) % cv.width;
}
function draw() {
ctx.clearRect(0, 0, cv.width, cv.height);
ctx.fillRect(x, 80, 20, 20);
}
function loop(now) {
const dt = now - last;
last = now;
times.push(dt);
if (times.length > 300) times.shift();
update(dt);
draw();
requestAnimationFrame(loop);
}
setInterval(() => {
if (!times.length) return;
const sum = times.reduce((a, b) => a + b, 0);
const avg = sum / times.length;
const max = Math.max(...times);
logEl.textContent =
'平均 ' + avg.toFixed(1) + ' ms / 最大 ' + max.toFixed(1) + ' ms';
}, 1000);
requestAnimationFrame(loop);
</script>この表示を毎回同じ位置に出し、各バージョンで同じ操作を30秒続けたあとの数値を読み取ります。端末の状態やほかのタブの影響を受けるため、同じ端末で、ほかのアプリを閉じた状態に揃えます。
10回の追加を、先に決めておく
実験で最も大切なのは、追加する機能を始める前に順番まで決めて書き出しておくことです。作りながら選ぶと、無意識に「入れやすいもの」ばかりを選び、崩れにくい結果になってしまいます。
追加の候補を、既存の仕組みへ触る度合いが小さいものから大きいものへ並べると、たとえば次のようになります。これは並べ方の一例で、ゲームに合わせて入れ替えて構いません。
- スコア表示
- 残機
- 敵の種類を一つ追加
- 時間とともに難しくなる仕組み
- 効果音
- 一時停止
- パワーアップアイテム
- タイトル画面とリトライ画面
- スマホ向けのタッチ操作
- ハイスコアの保存
ルールは三つです。
- 一回に一機能:二つ同時に入れると、どちらが原因か切り分けられません。
- 追加のたびに固定チェックを全項目実行する:失敗がなくても記録します。
- 順番と内容は途中で変えない:変えたくなったら、その理由を記録シートの備考に書き、実験は続けます。
各バージョンで記録するもの
1回の追加が終わったら、次の項目を同じ単位で記録します。
- 追加にかかった時間、単位・回数:分(着手から、チェックが全部通るまで)
- 触った関数の数、単位・回数:個(追加した関数ではなく、既存で書き換えたもの)
- 差分の行数、単位・回数:行(追加と削除の合計)
- チェックリストの失敗数、単位・回数:10項目中の件数
- 想定外の副作用、単位・回数:件数(機能Aを足したらBがおかしくなった、という事例)
- 操作感の評価、単位・回数:1〜5点、同じ条件で3回遊んだ平均
- フレーム間隔、単位・回数:平均と最大(ミリ秒)
- 読み返し時間、単位・回数:秒(下記)
読み返し時間は、バージョンを保存した翌日以降に、ゲーム内のランダムに選んだ関数を一つ開き、「これは何をするものか、なぜ必要か」を声に出して説明できるまでの時間です。説明が詰まる箇所があれば、備考に書きます。コードの読みにくさは数値にしにくいので、この方法で近似します。
記録する時点で評価のずれを防ぐため、チェックリストの合否は「読んで判断」ではなく「操作して判断」と決めておきます。項目の追加も途中ではしません。
結果の読み方と、止める基準の作り方
11行(v00〜v10)の記録ができたら、項目ごとに折れ線グラフにします。確認するのは値の大きさよりも、線の形です。
- ほぼ直線で増える:機能が増えた分だけ手間が増えているだけで、構造は保たれている可能性があります。
- 途中で急に折れ曲がる:折れ曲がった追加が分岐点です。その機能の内容より、「何と何がその時点で結びついたか」を見ます。
- 失敗数が増えたり戻ったりする:直したつもりで別の場所を壊している状態です。状態変数の共有が疑われます。
折れた箇所の原因は、たとえば次の観点で調べます。
- 複数の機能が同じ変数(例:ゲーム状態を示す一つのフラグ)を読み書きしていないか
- 入力処理に、後から条件が何重にも足されていないか
- リセット処理が、新しい機能の初期化を取りこぼしていないか
これらは調べるための観点であり、あなたの試作で必ず当てはまるという意味ではありません。
止める基準を数字にする
観察から、自分用の停止基準を作ります。次の数字は書き方の例であり、実験で得られた結果ではありません。閾値は自分の記録の形を見て決めます。
- 追加にかかった時間が、直前の3回の平均の2倍を超えたら、次の追加の前に整理を挟む
- チェックの失敗が2回連続で増えたら、新機能を止めて原因を一つずつ潰す
- 一つの機能で触る既存関数が3つ以上になったら、構造の見直しを検討する
- 読み返しで説明に詰まる関数が出たら、名前と役割を整える
基準は、実験の記録を見たうえで、次の試作から使うものです。同じ実験の途中で変えると、結果が基準に合わせて見えてしまいます。
発展:整理を挟む版との比較
さらに踏み込むなら、同じ10回の追加を二通り行います。
- A版:整理せず、10回すべて足し続ける
- B版:5回目の追加の後に、一度だけ整理の時間を取る(状態の置き場所を一か所にまとめる、リセット処理を一つの関数にするなど)
整理の時間は上限を決め、たとえば60分以内とします。B版は整理にかけた時間も記録に含めます。比較では、6〜10回目の追加時間と失敗数が、A版とB版でどう違うかを見ます。整理にかけた時間を、後半の追加で取り戻せたかどうかが判断材料になります。
ただし、同じ人が同じゲームを二回作ると、二回目は構造を覚えているため有利になります。A版とB版の順序を入れ替える、または片方を別の日に作るなど、慣れの影響を弱める工夫が必要です。それでも完全には消えないので、差が小さいときは「差がある」と結論しないことが大切です。
誤りやすい点
実験の途中で、次のようなことが起こりやすいので先に確認しておきます。
- 機能が偏る:自分が得意な種類の機能ばかり選ぶと、崩れる場所が見えません。順番は事前に確定します。
- 体感で書く:「重くなった気がする」ではなく、フレーム間隔の数値で残します。
- チェック項目を途中で増やす:失敗数の比較ができなくなります。足したくなったら次の実験用の項目として別にメモします。
- 直しながら記録する:追加時間には、直しの時間も含めます。直した時間を除くと、崩れが見えなくなります。
- 自分のコードに慣れる:読み返し時間は、書いた直後ではなく翌日以降に測ります。
- 一度の結果を一般化する:一本のゲームで出た形は、そのゲームと書き方の結果です。別のジャンルでも試してはじめて、自分の癖か、ゲームの性質かが分かれます。
次の行動
まず土台の小さなゲームを一本決め、追加する10個の機能を紙に書き出して順番を固定してください。そのうえでv00を保存し、固定チェックの10項目を作ってから、1回目の追加に入ります。
アソバースに投稿する場合は、直下にindex.htmlを置いた一つのフォルダを、20MiB以内でアップロードできます。下書き保存もできるため、v00と、自分で決めた停止基準を守って止めたバージョンを下書きとして並べ、遊び比べてみるのも一つの確かめ方です。公開前には、表示の確認の段階で、チェックリストの項目を一通り操作してください。

