Scene Reloadを有効にした通常のランタイムListenerの例。設定を比較し、所有者で値とイベントを初期化し、購読と解除を対にして再生を繰り返します。
図:Scene Reloadを有効にした通常のランタイムListenerの例。設定を比較し、所有者で値とイベントを初期化し、購読と解除を対にして再生を繰り返します。(クリックで拡大)

原因と仕組み

Domain Reloadを無効にすると、Playの終了後もstatic変数の値やstaticイベントの購読が残ります。2回目の再生だけカウンターが初期値に戻らない、同じ通知が複数回届くといった症状の候補です。staticを使うこと自体が原因とは限りません。

まず設定を比較する

  1. 調査用のシーンで、Project SettingsのEditorにあるEnter Play Mode設定を確認します。
  2. Scene Reloadは有効のまま、Domain Reloadの有効・無効で、同じ操作を複数回Playして比較します。
  3. どちらでも重複するなら、同じオブジェクトが複数ある、Inspectorとコードの両方で登録した、解除がないなど、通常の購読経路も確認します。

設定名はUnityの版で異なります。以下はUnity 6と2022.3のランタイムスクリプト向けの最小例です。

値とイベントを所有するクラスで初期化する

SessionCounter.csとして保存します。SubsystemRegistrationで値を0に戻し、このクラス自身が宣言したイベントを初期化します。他のクラスやUnityのイベントをまとめて消す例ではありません。

using System;
using UnityEngine;

public static class SessionCounter
{
    public static int Value { get; private set; }
    public static event Action<int> Changed;

    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
    private static void ResetState()
    {
        Value = 0;
        Changed = null;
    }

    public static void AddOne()
    {
        Value++;
        Changed?.Invoke(Value);
    }
}

購読と解除を対にする

SessionCounterListener.csとして保存し、シーン内の1つのGameObjectへ付けます。UI ButtonのOn Click()には、このComponentのSessionCounterListener.AddOneを1回だけ登録します。OnEnableの追加前に同じ購読を解除し、OnDisableでも解除します。

using UnityEngine;

public class SessionCounterListener : MonoBehaviour
{
    private void OnEnable()
    {
        SessionCounter.Changed -= OnChanged;
        SessionCounter.Changed += OnChanged;
    }

    private void OnDisable()
    {
        SessionCounter.Changed -= OnChanged;
    }

    private void OnChanged(int value)
    {
        Debug.Log($"Counter: {value}", this);
    }

    public void AddOne()
    {
        SessionCounter.AddOne();
    }
}

修正の確認と、この例の範囲

  • 1回目と2回目のPlayで、最初のクリックがCounter: 1になるか確認する。
  • 1クリックに対するログが1件か確認する。
  • GameObjectの無効・有効を繰り返しても購読が増えないか確認する。
  • 対象Playerビルドで、起動時とシーン再読み込み時の動作を別々に確認する。

この初期化は各シーン遷移で自動的に実行されるリセットではありません。Scene Reloadも無効にする場合は、オブジェクトの残存と初期化・購読のタイミングを別途設計します。実行途中でイベントを消すと必要な購読者まで失うため、所有者によるセッション開始時の初期化に限定します。Editor専用の静的状態にはInitializeOnEnterPlayModeなど、用途に合ったEditor APIを使います。

このListenerは通常のランタイムMonoBehaviourを想定しており、ExecuteAlwaysやExecuteInEditModeを付けていません。Edit Modeでも動くコードやScene Reloadを無効にした構成では、Play切替でOnEnable/OnDisableが呼ばれると決めつけず、EditorのPlay Mode状態通知など用途に合う経路で解除と再登録を設計してください。

参考資料