【Unity】MonoBehaviourのUpdateに頼らないクラス設計

MonoBehaviourって便利ですよね。
C#のスクリプト用意して、MonoBehaviourを継承したクラスを作ってGameObjectにアタッチするだけでUnityの様々なイベントを利用できるようになります。

UnityEngine.MonoBehaviour - Unity スクリプトリファレンス
MonoBehaviour is the base class from which every Unity script derives.

しかし、便利さの裏にはパフォーマンスの犠牲が隠れていることもあります。
ずいぶん前にUnity公式のブログでUpdateイベントについて興味深い記事が紹介されました。

Update()を10000回呼ぶ – Unity Blog
Unityにはメッセージングシステムと呼ばれる、ゲームの実行中に特定のイベントが発生した時に自動的に呼ばれる魔法のメソッドを定義できる機能が備わっています。これはとてもシンプルかつ簡単なコンセプトなので、特に初心者にはありがたい機能です。たとえば短に下記のようにUpdateメソッドを定義するだけで、自動的に毎...

MonoBehaviourを継承したクラスに、StartやUpdateなどマジックメソッドが定義されていると、Unityは所定のリストに情報をキャッシュし、ゲーム中は単にイテレーションしてメソッドを呼んでいきます。UnityはC++で実装されていますが、スクリプトはC#なので、C++とC#の間で呼び出しのオーバーヘッドもあります。サクッと動かす分には便利ですが、数が多いと無視できないコストになってきます。

そして、MonoBehaviourのスクリプトはロード順に処理されるので、実行順序はランダムです。
→こちらの記事で紹介しました。

複雑なゲームになってくると、世界のオブジェクトの状態からAIが思考して、プレイヤーが動いて、敵が動いて~というようにUpdateが呼び出される順番が重要になってきます。

例えば、MonoBehaviourを継承したEnemyクラスがあるとしましょう。
Enemyはたくさん出したいし、ターゲットを検索したりしたいので、InstantiateしたEnemyを管理するためのEnemyManagerクラスもあるとしましょう。

EnemyクラスはMonoBehaviourを継承しますが、StartやUpdateを定義しません。
代わりにInitManagedUpdateを定義して、外から呼び出せるように作っておきます。

EnemyManagerは、CreateEnemyでInstantiateしたEnemyクラスを敵リストに追加します。
ManagedUpdateは、作成されたEnemyManagedUpdateを順番に呼び出します。
スクリプトで明示的に記述していくので、わかりやすく実行順序を制御することができます。

最後にシーンを制御するクラスのUpdateでEnemyManagerManagedUpdateを呼びます。
うまく設計すると、MonoBehaviourのUpdateはシーンを制御するクラスにしか必要ありません。
C++とC#の間の呼び出しのオーバーヘッドも気にする必要はなくなるでしょう。

これはUnityに限らずゲームではよくある作り方です。
Unityは便利なコンポーネントを備えた描画エンジンだと思ってしまえば、ゲームの作り方は昔から大きく変わらないのです。

何らか複数のオブジェクトを管理するためのマネージャクラスを作る場合には、このように実行順序の制御とパフォーマンスの最適化を意識した設計にすることができます。
ManagedUpdateを呼び出す実装の手間と、呼び忘れるリスクもあるので、そこは一長一短です。
作るものに合わせて、MonoBehaviourに任せた方が良い場合と、自身でUpdateを制御した方が良い場合を見極めて設計していきましょう。