Unity上でゲームを再生する時間を短くする「Configurable Enter Play Mode」とは


Configurable Enter Play Modeとは?

Unity2019.3から「Configurable Enter Play Mode」という機能を使うことで、Unity上でゲームを起動する時間が早くなる機能(素早い再生モード)が追加されました。
詳細はこちらのUnity公式ブログをご覧ください
技術的な詳細はこちらの公式フォーラムをご覧ください
起動時間が1/10近くなるなど効果は大きいのですが、既存のプログラムの書き方が通用しなくなる「破壊的な変更」のため、自作のプログラムやAssetStoreのアセットも全てが対応していないと正常に動作しないです。
公式ブログでは「Unity では、アセットストアで人気のパッケージを確実に Domain Reload・Scene Reload の無効化に対応させたいと考えています」と書かれてるのですが、今のところはアセット開発者に丸投げっぽいです。
アセット開発者として対応はしてみたのですが、それがけっこうややこしかったので、知る限りの対応方法をまとめておこうと思います。

Configurable Enter Play Modeの使い方

Edit > Project Settings > Editor から「Enter Play Mode Options」をオンにすると、 「Reload Domain」と「Reload Scene」のオプションが利用可能になります。

通常、Unityのプレイモード起動時は「Reload Domain」「Reload Scene」という重い処理を行っているため、それを簡略化して処理することで起動時間を早くしようというアプローチなようです。
「Reload Domain」と「Reload Scene」の両方をオフにすれば、「Reload Domain」「Reload Scene」両方が簡略化処理になるようです。

Reload Domainをオフにする場合の対応方法

「Reload Domain」は「スクリプトの状態のリセット」を行っているものらしいです。
オフにすると、staticなフィールドやイベントのリセットされなくなるため、前回起動時の値やイベントが登録されたままになってしまいます。
対応方法はすでに公式ドキュメントに載っています。
https://docs.unity3d.com/ja/2019.3/Manual/DomainReloading.html

staticフィールドを初期化

staticなフィールドの初期化を手動で行う必要があります。
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]を使うことで、指定のメソッドをプレイモード起動時に呼ぶことができるのでこれを使います。

using UnityEngine;

public class StaticCounterExampleFixed : MonoBehaviour
{
    static int counter = 0;

    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
    static void Init()
    {
            Debug.Log("Counter reset.");
            counter = 0;   
    }

    // Update is called once per frame
    void Update()
    {
            if (Input.GetButtonDown("Jump"))
            {
                counter++;
                Debug.Log("Counter: " + counter);
            }
    }
}

staticなイベントを初期化

staticなイベントも初期化する必要があります。
イベントには+=でメソッドを登録するため、前回起動時のものが削除されていないと重複して登録されて、同じメソッドが複数回呼ばれることになってしまうからです。
対応するには、以下のように登録(+=)の前にメソッドを削除(-=)するという形になるため、パッと見とても気持ち悪いコードになります。
「前回起動時に登録されたかもしれないメソッドを削除する」ということではあるのですが、初回起動では未登録なのに削除することになるので大丈夫なのか不安になります。(特に問題なく動きはするのですが)
また、ラムダ式では削除(-=)ができないので、メソッドに書き直す必要があります。

using UnityEngine;
public class StaticEventExampleFixed : MonoBehaviour
{
    [RuntimeInitializeOnLoadMethod]
    static void RunOnStart()
    {
            Debug.Log("Unregistering quit function");
            Application.quitting -= Quit;
    }

    void Start()
    {
            Debug.Log("Registering quit function");
            Application.quitting += Quit;
    }

    static void Quit()
    {
        Debug.Log("Quitting the Player");
    }
}

Reload Sceneをオフにする場合の対応方法

こちらは「Reload Domain」の場合と違って具体的な対応方法がまだドキュメントにありません。
https://docs.unity3d.com/ja/2019.3/Manual/SceneReloading.html
現在(2020/1/20)のところ、Unityフォーラムにしか情報がないようです。
まだ実験機能ということもあり、今後対応方法が変わっていくとは思うのですが、これだけだとちょっとわかりづらいので個人的に対応したときの情報をまとめておきます。

UnityEngine.Objectの初期値にNULLを使おうとする場合

通常はあまり気にすることではないのですが、UnityEngine.Objectを継承しているクラス・・・つまりMonobehaviour(コンポーネント)やScriptableObjectやTextureなどのアセットへの参照の初期値は、C#本来のNULLが保証されなくなるようです。
Unityのオブジェクトは「==演算子をoverrideしているためNULLじゃなくても==NULLが成り立つ」という裏仕様があります。
そのため、ヌル合体演算子などを使ってヌルチェックをしていた場合は、意図しない動作になることがありました。
これが問題になるのは主に「Destroyされた場合」だったのですが、どうも Reload Sceneをオフにした場合は、プレイモードの起動時にもこれが起きるようになるらしいです。
そのため「初期値はNULLのはずなのでヌル合体演算子を使う」といったコードがあった場合は全て、==演算子(または三項演算子)を使うように書き直す必要があります。
また、これはpublicであったり[SerializeField]をつけている場合だけに限らず、privateなフィールドでも同様となります。

ヌル合体演算子を使った古いコード

using UnityEngine;
using UnityEngine.UI;
public class Sample : MonoBehaviour
{
    RectTransform CachedRectTransform
    {
        get
        {
            return cachedRectTransform ?? (cachedRectTransform = this.GetComponent<RectTransform>());
        }
    }
    RectTransform cachedRectTransform;
}

ヌル合体演算子を使わない新しいコード

using UnityEngine;
using UnityEngine.UI;
public class Sample : MonoBehaviour
{

    RectTransform CachedRectTransform
    {
        get
        {
            if (cachedRectTransform == null)
            {
                cachedRectTransform  = this.GetComponent<RectTransform>();
            }
            return cachedRectTransform;
        }
    }
    RectTransform cachedRectTransform;
}

Listや配列にNULLを使おうとする場合

Listや配列は参照なのでデフォルト値はNULLです。
ですが、Reload Sceneをオフにした場合は、privateに明示的に=nullとしてあった場合でも、サイズ0のインスタンスが作成されていて、NULLになりません。
明示的にNULLを使いたい場合は、privateではなく「NonSerialized」とする必要があるようです。

従来のリストの初期値がNULLなコード

        List<int> list;

NonSerializedを使って、リストの初期値に明示的にNULLを使ったコード

        [NonSerialized]
        List<int> list = null;

ExecuteInEditMode/ExecuteAlwaysを使ったコンポーネント

どうも、Awakeが呼ばれなくなってしまうようです。
ちょっとこの辺フォーラムでも混乱があるようなので、対応方法が今一つはっきりしないです。
ExecuteInEditModeは、エディタ中でもコンポーネントのUpdateなどを呼ばれるようにするための機能です。
他のオブジェクトの変更やScreenSizeなどの環境の変更に合わせて表示を更新したいコンポーネントなどで使うことがあるかと思います。
ただ、Awakeが呼ばれなくなってしまうと、自作コードならまだしも、Awakeを使用したコンポーネントを継承している場合は、解決策がなくなってしまいます。
この点はフォーラムでも指摘されているようで、なんらかの形で今後変更があるかもしれません。

今のところのこうするするのがよさそう?

「Reload Scene」に関しては、これまでにまとめたように影響の甚大な破壊的な変更が多いです。
対応方法がまだはっきりしないのと、今後さらに変更もありそうなので、なかなかに対応が辛いです。
なので、今のところは「Reload Domain」のほうを優先して対応するのが良いかと思います。
設定は「Reload Domain」をオフにして「Reload Scene」をオン。staticな部分のコードのみをまずは対応する。

個人的にはこの形で様子見するのがよいかなと思います。
「Reload Domain」だけをオフにしても「素早い再生モード」の効果は十分に得られるようです。

画像の出典:Unity公式ブログ