ゲーム開発

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

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

MonoBehaviour - Unity スクリプトリファレンス
MonoBehaviour is a base class that many Unity scripts derive from.

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

Unity ブログ
Unity ブログ

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をコピーしました