🤖

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

2024/03/20に公開

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

Enemy.cs
// 敵パラメータ
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()
	{
		// 敵の制御ロジックを書く
	}
}

EnemyManagerは、CreateEnemyでInstantiateしたEnemyクラスを敵リストに追加します。
ManagedUpdateは、作成されたEnemyのManagedUpdateを順番に呼び出します。

Enemy.cs
// 敵マネージャクラス
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();
		}
	}
}

シーンを制御するクラスのUpdateでEnemyManagerのManagedUpdateを呼びます。
うまく設計すると、MonoBehaviourのUpdateはシーンを制御するクラスに1つあれば事足ります。
この方法のメリットは、実行順序を制御できること、C++とC#の間の呼び出しのオーバーヘッドが最低限になる
Enemyの出現数が予め決まっているならLinkedListである必要もありません。
Listの方が高速ですし、最初にまとめてInstantiateしておき、実行時はGameObjectをSetActiveでOn/Offするだけにした方がパフォーマンスは良いです。ここはゲーム内容でケースbyケースですが、Managerがいれば最適化もしやすくなります。

BattleScene.cs
public class BattleScene : MonoBehaviour
{
	[SerializeField] EnemyManager _enemyManager = null;

	private void Update()
	{
		_enemyManager.ManagedUpdate();
	}
}

引用
https://noracle.jp/unity-monobehaviour-update/

Editorでも実行する

ExecuteAlways属性をクラスに設定するとスクリプトが編集モードと再生モード時の両方で実行されるようになります。

using UnityEngine;

[ExecuteAlways]
public class ExampleClass : MonoBehaviour
{
    void Start()
    {
        if (Application.IsPlaying(gameObject))
        {
            // Play logic
        }
        else
        {
            // Editor logic
        }
    }
}

https://docs.unity3d.com/ja/current/ScriptReference/ExecuteAlways.html

https://docs.unity3d.com/ja/current/ScriptReference/Application.IsPlaying.html

https://bluebirdofoz.hatenablog.com/entry/2023/11/28/233217

ライフサイクル

スクリプトライフサイクルフローチャート
https://docs.unity3d.com/ja/2018.2/Manual/ExecutionOrder.html

AwakeはScene上にインスタンスされたときに呼ばれ、Awakeの時点では他のインスタンスは作成されていない可能性があるので、参照してはいけません。
Awakeは自身のパラメータに対する初期化を書き、Startで制御の初期化を行うようにすると良いです。

public class Sample : MonoBehaviour
{
    private float _leftTime;
    private Transform _transform;
    
    private void Awake()
    {
        // Transformコンポーネントをキャッシュする
        _transform = transform;
    }
    
    private void Start()
    {
        // 制限時間をセットする
        _leftTime = 60.0f;
        
        // ボーナスが発動している場合は+10秒!
        // 他のインスタンス(GameManager)を参照して判断する
        if (GameManager.Instance.IsTimeBonus)
        {
            _leftTime += 10.0f;
        }
    }
}

プロパティの扱いに気をつける

プロパティには複雑な処理を書かず、用途に応じて変数とメソッドを使い分ける
  - 単に変数アクセスなのか、内部で処理しているかの見分けが付かない。
  - 値をsetしている箇所とgetしている箇所をコード全体から検索することが難しい。
  - 変数名からは想像できない実装が暗黙的に処理される可能性がある。
  - 変数アクセスにように多用すると、オーバーヘッドがパフォーマンスの低下を招く

public class Sample
{
    public int Count { get; set; }
    private int _count;

    private void Update()
    {
        // Countプロパティのsetが呼ばれる
        Count = 100;
        
        // Countプロパティのgetが呼ばれる
        int count = Count;
        
        // プロパティの中身はメソッドなのでオーバーヘッドがある
        for (int i = 0; i < 100; i++)
        {
            Count++;
        }
        
        // 変数はプリミティブに処理されるので速い(プロパティより4倍以上高速)
        for (int i = 0; i < 100; i++)
        {
            _count++;
        }
    }
}

https://noracle.jp/unity-programmer-guide/

https://www.midnightunity.net/csharp-optimisation/

マルチスレッド

https://light11.hatenadiary.com/entry/2019/03/06/002800

https://light11.hatenadiary.com/entry/2021/05/27/203956

https://light11.hatenadiary.com/entry/2021/01/28/212625

https://qiita.com/hayu500ml/items/4d4483853b7885ad7ac9

https://light11.hatenadiary.com/entry/2019/09/25/220517

UniRxでUpdateをストリーム化

UniRxでUpdateをストリーム化, MonoBehaviourを継承していないクラスでUpdateを実行

https://kan-kikuchi.hatenablog.com/entry/UniRx_Update

DOTSを用いた空間分割アルゴリズムの比較

https://qiita.com/kawai125/items/99bb45165da4db34fb92

Discussion