弾幕・敵の群れ・大量のアイテム——動くオブジェクトが数百を超えたあたりから、Profiler のUpdate.ScriptRunBehaviourUpdateがじわじわ膨らんでいく。各オブジェクトのUpdate()の中身は軽いのに、です。
実は、Update()は呼び出されること自体にコストがあります。この記事 では、その仕組みと、呼び出しを1箇所に集約する高度な最適化 カスタムアップデートマネージャー(Custom Update Manager) を解説します。
この記事でわかること
Update()の呼び出しになぜコストがかかるのか(ネイティブ⇄マネージドのブリッジ)- 呼び出しを1回に集約するマネージャーパターンの考え方
- IUpdatable+シングルトンによる実装
- 適用すべき場面とやりがちな落とし穴
- 実践:画面外の敵を更新ループから丸ごと外す
動作確認環境: Unity 6 (6000.x)
なぜUpdate()は遅くなるのか
MonoBehaviourのUpdate()は、Unityエンジン本体(C++のネイティブコード)から呼び出されます。C#側のUpdate()を1回呼ぶたびに、ネイティブとマネージド(C#)の世界をつなぐ「橋」を渡って往復する必要があり、この橋の通行料はUpdate()の中身が空でもかかります。

オブジェクトが1,000個あれば、毎フレーム1,000回の橋往復です。しかもUpdate()の冒頭で「今は何もしない」と早期リターンしているオブジェクトも、リターンするためだけに通行料を払っています。1回あたりはごくわずかでも、数が揃うと Profiler にはっきり現れる規模になります。
カスタムアップデートマネージャーの考え方
解決のアイデアはシンプルです。
「UnityからのUpdate()はマネージャー1つだけが受け取り、他の全オブジェクトの更新メソッドはマネージャーがC#内で直接呼ぶ」

橋を渡るのは毎フレームマネージャーの1回だけ。マネージャーから各オブジェクトへの呼び出しはすべてC#内で完結するため、橋の通行料がかかりません。ただし、ManagedUpdateの中でtransformなどUnityの機能を使えば、その分の行き来は残ります。減るのは「呼び出すための往復」です。
実装方法
ステップ1: 更新される側の「ルール」を決める
全体の構成は「ルール(インターフェース)・呼ぶ側(マネージャー)・呼ばれる側」の3点セットです。
