弾と敵が同時に数百個出るゲームを作りました。ここで初めて「毎フレーム作って壊す」が問題になります。
同じゲーム・同じ場面で、2つの書き方を測りました。
19.06ms
作って壊す
1フレーム(361個)
0.15ms
使い回す
同じ場面
1.9MB
作って壊すと確保
60フレームぶん
0バイト
使い回すと確保
同じ60フレーム
何を比べたか(2つの書き方)
比べたのは描画だけです。ゲームの中身(弾や敵の位置を進める処理)は同じで、画面に出す部分だけを2通り書きました。
毎フレーム作って壊す
- フレームの最初に、前のフレームのものを全部 Destroy
- いま必要な数だけ GameObject を作る
- 書くのは簡単。数が少ないうちは何も起きない
作ったものを使い回す(プール)
- 作ったものは持っておく。足りないときだけ作る
- 余ったぶんは消さずに隠す
- 位置と大きさを入れ直すだけ
使い回すほうの中身は、これだけです。
public SpriteRenderer Next(float x, float y, float diameter)
{
SpriteRenderer sr = _used < _items.Count ? _items[_used] : Create(); // 足りないときだけ作る
_used++;
sr.transform.localPosition = new Vector3(x, y, 0f);
sr.transform.localScale = new Vector3(diameter, diameter, 1f);
if (!sr.gameObject.activeSelf) sr.gameObject.SetActive(true);
return sr;
}
public void End()
{
for (int i = _used; i < _items.Count; i++) // 使わなかったぶんは「隠す」だけ
{
if (_items[i].gameObject.activeSelf) _items[i].gameObject.SetActive(false);
}
}
測った結果
画面に敵60・自機の弾300・自機1=361個が出ている状態で、描画を60回くり返して測りました。
画面にあるもの:敵60 自機の弾300 合計361個 プールあり: 60回 9.0ms(1回 0.15ms)確保 0バイト プールなし: 60回 1143.8ms(1回 19.06ms)確保 1904640バイト プールあり: 60回 8.8ms(1回 0.15ms)確保 0バイト
60fps を保つには、1フレームを 16.7ms に収める必要があります。作って壊す書き方は、描画だけでそれを超えました。しかもフレームごとに約31KB を確保するので、そのうち掃除(GC)が走ります。
同じ絵を出すのに、127倍の時間がかかっていた。
中身(ロジック)側も同じ作り方にする
使い回すのは絵だけではありません。弾や敵の入れものも、毎フレーム作り直さないようにしています。
このゲームでは、弾・敵を最初に確保した配列に入れ、生きているものだけを前に詰めています。消すときは、最後のものを穴に移して数を1減らすだけです。
// 消し方:並べ替えない・new しない
void RemoveShot(int i) => _shots[i] = _shots[--_shotCount];
void RemoveEnemy(int i) => _enemies[i] = _enemies[--_enemyCount];
これで「消えた印を見に行く」無駄も、詰め直しの並べ替えも要らなくなります。効いているかどうかは、テストで見張っています。
[Test]
public void 回収後の保持メモリが増えていないことを確認する()
{
...
long before = GC.GetTotalMemory(true);
for (int i = 0; i < 6000; i++) battle.Tick(Frame, input);
long after = GC.GetTotalMemory(true);
Assert.That(after - before, Is.LessThanOrEqualTo(0L), $"6000フレームで {after - before} バイト増えた");
}
この測り方で分かること・分からないこと
GC.GetTotalMemory(true) の前後差で見ているのは、回収したあとに残っている量の差です。途中で一度も確保していないことの証明ではありません(それを見るならプロファイラのフレームごとの割り当てを使います)。ここで守りたいのは「積み上がっていかないこと」です。
このテストだけでは割り当ての増加を見逃す
上のコードは測定の意図が分かるようにテスト名を変えた抜粋です。途中で確保しても回収されれば前後差には出ません。保持メモリの検査と、フレーム中の割り当ての検査は別にします。描画の比較値もエディター内の特定条件で測ったもので、ゲーム全体やスマホのフレーム時間を示すものではありません。
ふだんの場面では差が出なかった
正直に書くと、遊んでいる最中の画面では差が出ませんでした。実際に出ているのは数十個で、ブラウザで測るとどちらも 60fps・最長フレーム 16.9ms。
{"url":"https://aigame.game-union-libra.com/play/mureuchi/","fps":60.1,"worstMs":16.9,"frames":601}
{"url":"https://aigame.game-union-libra.com/play/mureuchi/?nopool=1","fps":60.1,"worstMs":16.9,"frames":601}ブラウザの描画は 60 回/秒で頭打ちになるので、余裕がどれだけあるかは fps には出ません。差を見るには、数を上限まで増やした場面(361個)で、1フレームにかかる時間そのものを測る必要がありました。
⚠️ 「fps 60 だから大丈夫」ではない
fps は頭打ちの数字です。余裕を知りたいなら、1フレームにかかった時間(や最長フレーム)を見てください。余裕が無い状態は、スマホなど遅い機械でそのまま処理落ちになります。
この数が出ている画面

私は最初、絵の出し方を「毎フレーム作り直す」で書きかけました。数十個なら本当に何も起きないからです。使い回す形に最初からしたのは、あとから直すと描画まわりを書き直すことになると分かっていたためで、速さのためではありません。効き目の数字は、あとで測って初めて分かりました。
まとめ
- 画面に361個ある場面で、19.06ms → 0.15ms・確保は1.9MB → 0バイト
- 使い回すのは絵だけでなく、中身の配列も。消すときは最後のものを穴に移す
- 効き目はテストで固定する(確保が増えたら落ちる)
- ただし数十個の場面では差が出ない。fps ではなく、1フレームの時間で見る