【Unity 6 対応】MonoBehaviour ライフサイクルの実行条件

<sup>更新履歴： 即時無効化した場合</sup><br/>



`destroyCancellationToken` がキャンセルされない、`Awake` が実行されないケースが存在する。



- [ライフサイクルの実行条件](#ライフサイクルの実行条件)
    - [`GameObject` が一度もアクティブにならなかった場合](#gameobject-が一度もアクティブにならなかった場合)
    - [`MonoBehaviour` が無効化されている場合](#monobehaviour-が無効化されている場合)
    - [`MonoBehaviour` が有効な状態で `GameObject` を非アクティブにした場合](#monobehaviour-が有効な状態で-gameobject-を非アクティブにした場合)
    - [フレームを跨がない場合](#フレームを跨がない場合)
    - [フレームを跨ぐ場合](#フレームを跨ぐ場合)
    - [`MonoBehaviour` が削除された場合](#monobehaviour-が削除された場合)
    - [追記：即時無効化した場合](#追記即時無効化した場合)
- [テスト用コード](#テスト用コード)
- [まとめとオマケ](#まとめとオマケ)
    - [`Awake` の実行漏れを検出する方法](#awake-の実行漏れを検出する方法)
    - [Unity 公式の `UnityEngine.Object` コンストラクターの使用例](#unity-公式の-unityengineobject-コンストラクターの使用例)





# ライフサイクルの実行条件

## `GameObject` が一度もアクティブにならなかった場合

`MonoBehaviour` のホスト `GameObject` が一度もアクティブにならなかった場合。

- `Awake`： 実行**されない**
- `destroyCancellationToken`： キャンセル**されない**
    - ※ トークン取得時にエラーは出ない。
    - 永遠にキャンセルされることのない `CancellationToken` をエラーなしで取得**出来てしまう**点に注意
- `OnDestroy`： 実行**されない**


```cs
void CreateObject()
{
    var go = new GameObject();

    // AddComponent より前に SetActive(false) する
    go.SetActive(false);
    go.AddComponent<X>();
}
```


## `MonoBehaviour` が無効化されている場合

`MonoBehaviour` が無効（`.enabled = false`）な状態で `GameObject` をアクティブにした場合の実行順。

- `Awake`
- --- オブジェクト削除 or シーンの破壊 ---
- `destroyCancellationToken`： GameObject, MonoBehaviour の状態に関わらず**キャンセルされる**
- `OnDestroy`： GameObject, MonoBehaviour の状態に関わらず**呼ばれる**

以下は実行**されない**。
- `OnEnable`
- `OnDisable`


## `MonoBehaviour` が有効な状態で `GameObject` を非アクティブにした場合

以下が実行される。
- `OnDisable`




## フレームを跨がない場合

```cs
void CreateObject()
{
    var go = new GameObject();

    go.AddComponent<X>();
    go.SetActive(false);   // AddComponent より後
}
```

以下の順序で実行される。
- `Awake`
- `OnEnable`
- ※ `Start`： 実行**されない**
- `OnDisable`： 同一フレーム内で OnEnable の後に呼ばれる
- --- オブジェクト削除 or シーンの破壊 ---
- `destroyCancellationToken`： GameObject が非アクティブでも**キャンセルされる**
- `OnDestroy`： GameObject が非アクティブでも**呼ばれる**



## フレームを跨ぐ場合

正確には `Update` `FixedUpdate` `LateUpdate` いずれかが実行される場合。
```cs
void CreateObject()
{
    var go = new GameObject();

    go.AddComponent<X>();
    StartCoroutine(DeactivateOnEndOfFrame(go));
}

IEnumerator DeactivateOnEndOfFrame(GameObject go)
{
    yield return new WaitForEndOfFrame();
    go.SetActive(false);
}
```

- `Awake`
- `OnEnable`
- `Start`： Update シリーズの直前に一度のみ
- `Update`
- `LateUpdate`
- (`FixedUpdate`)
- --- オブジェクト削除 or シーンの破壊 ---
- `OnDisable`
- `destroyCancellationToken`
- `OnDestroy`





## `MonoBehaviour` が削除された場合

`MonoBehaviour` そのもの、またはホスト `GameObject` やシーンに巻き込まれて削除された場合の実行順。

- `OnDisable`： **削除前に MonoBehaviour が有効だった場合**は呼ばれる
    - 👉 言い換えると `OnEnable` が呼ばれていて `OnDisable` がまだ呼ばれていない場合
- `destroyCancellationToken`
- `OnDestroy`





## 追記：即時無効化した場合

同一メソッド内でオンオフ操作を行った場合。

```cs
// １）非アクティブ状態から GameObject をオンオフする
go.SetActive(true);
go.SetActive(false);

// ２）無効化状態から MonoBehaviour をオンオフ
behaviour.enabled = true;
behaviour.enabled = false;

// テスト用の実装
void OnDisable() => Debug.Log("無効化");
void OnEnable()
{
    Debug.Log("有効化");
    StartCoroutine(LogOnEndOfFrame());  // 無効化のタイミングを確認する為
}

static IEnumerator LogOnEndOfFrame()
{
    yield return new WaitForEndOfFrame();
    Debug.Log("フレーム終了");
}
```


- `OnEnable`
- `OnDisable`： 同一フレーム内で即時実行され、WaitForEndOfFrame を待たない
- ※ `Update` `LateUpdate` `FixedUpdate`： 実行**されない**
- `WaitForEndOfFrame`
    - 「１」の場合は GameObject が無効化されているのでコルーチンは発火しない。後で有効化したタイミングで遅れて発火することもない。

> 前述の「MonoBehaviour が無効化されている場合」に記載の通り、`GameObject` をアクティブにしても `MonoBehaviour` が無効化されている場合は `OnEnable` `OnDisable` は実行**されない**。
> 
> `Awake` は `MonoBehaviour` の有効／無効の影響を受けないので実行**される**（ただし実行済みの場合は除く）



# テスト用コード

ゲームオブジェクトに貼り付けてインスペクターでチェックボックスをクリック！

<p><details lang="ja" --open><summary>見る</summary>

```cs
class TestRunner : MonoBehaviour
{
    [SerializeField] bool CreateGO_Deactivate_AddComponent = false;
    [SerializeField] bool CreateGO_AddComponent_DeactivateImmediately = false;
    [SerializeField] bool CreateGO_AddComponent_DeactivateOnEndOfFrame = false;
    [SerializeField] bool CreateGO_in_Update = false;

    int counter = 0;
    public void OnValidate()
    {
        if (CreateGO_Deactivate_AddComponent)
        {
            CreateGO_Deactivate_AddComponent = false;
            var go = new GameObject(nameof(CreateGO_Deactivate_AddComponent) + "\t\t" + counter++);

            go.SetActive(false);
            go.AddComponent<MyTestBehaviour>();
        }

        if (CreateGO_AddComponent_DeactivateImmediately)
        {
            CreateGO_AddComponent_DeactivateImmediately = false;
            var go = new GameObject(nameof(CreateGO_AddComponent_DeactivateImmediately) + "\t\t" + counter++);

            go.AddComponent<MyTestBehaviour>();
            go.SetActive(false);
        }

        if (CreateGO_AddComponent_DeactivateOnEndOfFrame)
        {
            CreateGO_AddComponent_DeactivateOnEndOfFrame = false;
            var go = new GameObject(nameof(CreateGO_AddComponent_DeactivateOnEndOfFrame) + "\t\t" + counter++);

            go.AddComponent<MyTestBehaviour>();
            StartCoroutine(DeactivateOnEndOfFrame(go));
        }
    }

    IEnumerator DeactivateOnEndOfFrame(GameObject go)
    {
        yield return new WaitForEndOfFrame();
        go.SetActive(false);
    }

    void Update()
    {
        if (CreateGO_in_Update)
        {
            CreateGO_in_Update = false;
            var go = new GameObject(nameof(CreateGO_in_Update) + "\t\t" + counter++);

            go.AddComponent<MyTestBehaviour>();
            go.SetActive(false);
        }
    }
}

class MyTestBehaviour : MonoBehaviour
{
    void Awake()
    {
        Debug.Log(nameof(Awake) + "\t\t" + this.name);

        this.destroyCancellationToken.Register(() =>
        {
            Debug.Log(nameof(this.destroyCancellationToken) + "\t\t" + this.name);
        });
    }

    public void OnDestroy() => Debug.Log(nameof(OnDestroy) + "\t\t" + this.name);

    public void OnEnable() => Debug.Log(nameof(OnEnable) + "\t\t" + this.name);
    public void OnDisable() => Debug.Log(nameof(OnDisable) + "\t\t" + this.name);

    public void Start() => Debug.Log(nameof(Start) + "\t\t" + this.name);
    public void Update() => Debug.Log(nameof(Update) + "\t\t" + this.name);
    public void LateUpdate() => Debug.Log(nameof(LateUpdate) + "\t\t" + this.name);
    public void FixedUpdate() => Debug.Log(nameof(FixedUpdate) + "\t\t" + this.name);
}
```

<!------- End of Details JA Tag -------></details></p>





# まとめとオマケ

- `Awake` は呼ばれない可能性がある
    - 無駄な処理を省くために `GameObject` を無効化するなら `AddComponent` の後または `Awake` の最後に
- `Awake` が呼ばれたら `OnDestroy` `destroyCancellationToken` が保証される
- `OnDisable` はオブジェクト削除時にも `.enabled` が真の場合は呼ばれる
    - つまり `OnEnable` が呼ばれたら `OnDisable` が保証される



例： 脱 Unity イマジナリーコード

```cs
class DatsuUnity : MonoBehaviour
{
    // Unity エディター経由でリストに設定できるように公式がんばって欲しい
    [SerializeReference] List<IAwakable> _onAwakeList;
    [SerializeReference] List<IUpdatable> _onUpdateList;
    [SerializeReference] List<ILateUpdatable> _onLateUpdateList;

    void Awake()
    {
        for (int i = 0, count = _onAwakeList.Count; i < count; i++)
            _onAwakeList[i].OnAwake(this.destroyCancellationToken);  // 非アクティブでもキャンセルされる

        // 無効化は最後に
        this.SetActive(false);  // アップデートマネージャーも兼ねてる場合はやっちゃダメ
    }

    void OnDestroy()  // 非アクティブでも呼ばれる
    {
        for (int i = 0, count = _onAwakeList.Count; i < count; i++)
            if (_onAwakeList[i] is IDisposable d)
                d.Dispose();
    }

    void Update()
    {
        for (int i = 0, count = _onUpdateList.Count; i < count; i++)
            _onUpdateList[i].OnUpdate(this);

        // インターフェイスで実行タイミングを制御せず、どのマネージャーに追加するかで実行タイミングを制御する方式
        for (int i = 0, count = _onFrameList.Count; i < count; i++)
            _onFrameList[i].OnFrame(this);
    }

    void LateUpdate()
    {
        for (int i = 0, count = _onLateUpdateList.Count; i < count; i++)
            _onLateUpdateList[i].OnLateUpdate(this);

        // MonoBehaviour 側が OnFrame をどのサイクルで実行するかを制御する方式の場合、
        // コンポーネント側は追加するマネージャーを切り替えるだけで LateUpdate 呼び出しに変更できる
        for (int i = 0, count = _onFrameList.Count; i < count; i++)
            _onFrameList[i].OnFrame(this);
    }
}
```



## `Awake` の実行漏れを検出する方法

👇 は一応合法。アクティブだとオッケーで非アクティブの GameObject に追加したときだけエラーが出る。

```cs
class AwakeDetectorBehaviour : MonoBehaviour
{
    static readonly List<object> _failsafe = new();

    AwakeDetectorBehaviour()  // 👈 条件が厳しいだけでコンストラクターは普通に使える
    {
        _failsafe.Add(this);
        Debug.Log(this.GetHashCode());  // Debug は使える。.ToString (.name) はアクセス不能
    }

    void Awake()
    {
        _failsafe.Remove(this);
    }

    // デストラクタは思ってるのと違うタイミングで呼ばれるので能動的にリストを見に行ってレポートする
    ~AwakeDetectorBehaviour()
    {
    }

    void OnValidate()
    {
        if (_failsafe.Count != 0)
        {
            var msg = "Something went wrong!! " + string.Join(", ", _failsafe.Select(x => x.GetHashCode()));
            Debug.LogError(msg);
        }
    }
}
```


## Unity 公式の `UnityEngine.Object` コンストラクターの使用例

https://github.com/Unity-Technologies/UnityCsReference/blob/master/Editor/Mono/ScriptableSingleton.cs#L83



--

以上です。お疲れ様でした。