ゲーム開発

【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クラスもあるとしましょう。

// 敵パラメータ
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を定義しません。
代わりにInitManagedUpdateを定義して、外から呼び出せるように作っておきます。

// 敵マネージャクラス
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を制御した方が良い場合を見極めて設計していきましょう。

タイトルとURLをコピーしました