MonoBehaviourって便利ですよね。
C#のスクリプト用意して、MonoBehaviourを継承したクラスを作ってGameObjectにアタッチするだけでUnityの様々なイベントを利用できるようになります。
しかし、便利さの裏にはパフォーマンスの犠牲が隠れていることもあります。
ずいぶん前にUnity公式のブログでUpdateイベントについて興味深い記事が紹介されました。
MonoBehaviourを継承したクラスに、StartやUpdateなどマジックメソッドが定義されていると、Unityは所定のリストに情報をキャッシュし、ゲーム中は単にイテレーションしてメソッドを呼んでいきます。UnityはC++で実装されていますが、スクリプトはC#なので、C++とC#の間で呼び出しのオーバーヘッドがあります。サクッと動かす分には便利ですが、数が多いと無視できないコストになってきます。
そして、MonoBehaviourのスクリプトはロード順に処理されるので、実行順序はランダムです。
→こちらの記事で紹介しました。
複雑なゲームになってくると、プレイヤーが動いた後、敵が動いて、各オブジェクトの状態に応じてAIが思考して~というようにUpdateが呼び出される順番が非常に重要になってきます。
例えば、MonoBehaviourを継承したEnemyクラスがあるとしましょう。
Enemyはたくさん出したいし、検索できるようにしておきたいので、InstantiateしたEnemyを管理するための EnemyManagerクラスもあるとしましょう。
// 敵パラメータ
public class EnemyParam
{
public int Hp { get; set; }
public int Atk { get; set; }
public int Def { get; set; }
//...
}
// 敵クラス
public class Enemy : MonoBehaviour
{
// 初期化
public void Init( EnemyParam param )
{
// EnemyParamを見て初期化する
}
// Updateの呼び出しを制御する
public void ManagedUpdate()
{
// 敵の制御ロジックを書く
}
}
EnemyクラスはMonoBehaviourを継承しますが、StartやUpdateを定義しません。
代わりにInitとManagedUpdateを定義して、外から呼び出せるように作っておきます。
// 敵マネージャクラス
public class EnemyManager : MonoBehaviour
{
[SerializeField] GameObject _enemyPrefab = null;
// 敵リスト
private LinkedList<Enemy> _enemies = new LinkedList<Enemy>();
// 敵を生成する
public Enemy CreateEnemy( EnemyParam param )
{
var obj = Instantiate(_enemyPrefab);
var enemy = obj.GetComponent<Enemy>();
if (enemy != null)
{
// 初期化
enemy.Init(param);
// 敵リストに追加する
_enemies.AddLast(enemy);
return enemy;
}
return null;
}
// Updateの呼び出しを制御する
public void ManagedUpdate()
{
// 生成されている敵のUpdateを呼び出し
foreach (var enemy in _enemies)
{
enemy.ManagedUpdate();
}
}
}
EnemyManagerは、CreateEnemyでInstantiateしたEnemyクラスを敵リストに追加します。
ManagedUpdateは、作成されたEnemyのManagedUpdateを順番に呼び出します。
public class BattleScene : MonoBehaviour
{
[SerializeField] EnemyManager _enemyManager = null;
private void Update()
{
_enemyManager.ManagedUpdate();
}
}
最後にシーンを制御するクラスのUpdateでEnemyManagerのManagedUpdateを呼びます。
うまく設計すると、MonoBehaviourのUpdateはシーンを制御するクラスに1つあれば事足ります。
この方法のメリットは、実行順序を制御できること、C++とC#の間の呼び出しのオーバーヘッドが最低限になることです。
Enemyの出現数が予め決まっているならLinkedListである必要もありません。
Listの方が高速ですし、最初にまとめてInstantiateしておき、実行時はGameObjectをSetActiveでOn/Offするだけにした方がパフォーマンスは良いです。ここはゲーム内容でケースbyケースですが、Managerがいれば最適化もしやすくなります。
これはUnityに限らずゲームではよくある作り方です。
Unityはゲーム作りに便利なコンポーネントが大量に用意された描画エンジンだと思ってしまえば、ゲーム自体の作り方は昔から大きく変わりません。
何か複数のオブジェクトを管理するためのマネージャクラスを作る場合には、このように実行順序の制御とパフォーマンスの最適化を意識した設計にすることができます。
ManagedUpdateを呼び出す実装の手間と、呼び忘れるリスクもあるので、そこは一長一短です。
作るものに合わせて、MonoBehaviourに任せた方が良い場合と、自分でUpdateを制御した方が良い場合を見極めて設計していきましょう。