【Unity】本当は知っておくべきUnityのホットリロードの仕組み

Unityのホットリロードの仕組みについてまとめます。

Unity2020.3.15f2

はじめに

Unityにはホットリロードと呼ばれる仕組みがあります。
この仕組みをうまく使うと、再生中にスクリプトを編集しても変数が保持されてそのまま再生を続けられます。

しかし対応の難易度が高いこともあり、ランタイムのスクリプトをホットリロードに対応させている事例はあまり見ません。
最近では以下の記事のようにスクリプト編集時に再生を自動的に止めるモードなどもできていて、ホットリロードの存在感はより薄れているように感じます。

blog.naichilab.com

一方で、エディタ拡張をする際にはこのホットリロードの仕組みを正しく理解しなければならないケースがあります。

そこで本記事では改めてこのホットリロードの仕組みと必要性をまとめます。

ホットリロードの仕組み

C#スクリプトコンパイルが走ると、Unityはシリアライズ要件を満たすデータを一度保存し、
スクリプトをリロードしてからそれらの値を格納しなおします。

ここでいう「シリアライズ要件を満たすデータ」の定義は以下の通りです。

  • SerializeFieldアトリビュートをつけたり、publicなフィールドにした場合にシリアライズされるもの
  • つまりSerializeFieldがついていないprivate変数も1.を満たせばホットリロードの対象となる
  • プロパティも↑を満たせば対象となる
  • インターフェースや抽象クラスについては明示的にSerializeReferenceがついていれば対象となる

GameObjectなどにシリアライズされる条件とは異なり、プロパティやprivate変数も対象となる点に注意が必要です。

ランタイムにおけるホットリロードの動作確認

さてそれではホットリロードの挙動を確認していきます。
まずPreferences > General > Script Changes While PlayingRecompile And Continue Playingになっていることを確認します。

f:id:halya_11:20211209231149p:plain
Recompile And Continue Playing

次に以下のようなスクリプトを適当なGameObjectにアタッチして再生します。

using System;
using UnityEngine;

public class HotReloading : MonoBehaviour
{
    // これがホットリロードされることを確認する
    private int Test { get; set; }

    private void Update()
    {
        // 10frameに一回だけ処理
        if (Time.frameCount % 10 != 0)
        {
            return;
        }

        Debug.Log(Test++);
    }
}

10フレームに一度、値がインクリメントされて出力されるはずです。
ここで再生中にスクリプトを編集してコンパイルを走らせると、正常にホットリロードされることが確認できます。

f:id:halya_11:20211209231530p:plain
ホットリロードされた

クラスもSerializableアトリビュートがついていればホットリロードの対象となります。

using System;
using UnityEngine;

public class HotReloading : MonoBehaviour
{
    // Serializable属性のついているオブジェクトはホットリロード対象に
    private TestClass Test { get; set; } = new TestClass();

    private void Update()
    {
        if (Time.frameCount % 10 != 0)
        {
            return;
        }

        Debug.Log(Test.Value++);
    }
}

[Serializable]
public class TestClass
{
    public int Value { get; set; }
}

インターフェースに関してはSerializeReferenceアトリビュートがついていればホットリロードの対象になります。

using UnityEngine;

public class HotReloading : MonoBehaviour
{
    // インターフェースはSerializeReferenceがついていればホットリロード対象に
    [SerializeReference] private ITest _test;

    private void Awake()
    {
        _test = new TestImpl();
    }

    private void Update()
    {
        if (Time.frameCount % 10 != 0)
        {
            return;
        }

        Debug.Log(_test.Value++);
    }
}

public interface ITest
{
    int Value { get; set; }
}

public class TestImpl : ITest
{
    public int Value { get; set; }
}

なおSerializeReferenceについて詳しくは以下の記事を参照してください。

light11.hatenadiary.com

これを再生してスクリプトコンパイルすれば、正常にホットリロードが行われることが確認できるはずです。

エディタとホットリロードの関係

さてこのホットリロードの仕組みは再生中だけのものではありません。
エディタコードでは、再生を停止している時にもホットリロードが走ります。
例えば以下のようなEditorWindowを作成します。

using UnityEditor;
using UnityEngine;

public class HotReloading : EditorWindow
{
    private int Test { get; set; }
    private int _updateCount;

    private void Update()
    {
        _updateCount++;
        if (_updateCount % 30 != 0)
        {
            return;
        }
        
        Debug.Log(Test++);
    }

    [MenuItem("Window/HotReloading")]
    private static void Open()
    {
        GetWindow<HotReloading>(ObjectNames.NicifyVariableName(nameof(HotReloading)));
    }
}

このウィンドウを開いた状態でスクリプトを更新すると当然のようにTestの値が保持されますが、
実はこれは裏でホットリロードが走っています。

試しに以下のように変数の型をクラスに変えてみると、値が保持されずホットリロードされないことが確認できます。

using UnityEditor;
using UnityEngine;

public class HotReloading : EditorWindow
{
    private TestClass Test { get; set; } = new TestClass();
    private int _updateCount;

    private void Update()
    {
        _updateCount++;
        if (_updateCount % 30 != 0)
        {
            return;
        }
        
        Debug.Log(Test.Value++);
    }

    [MenuItem("Window/HotReloading")]
    private static void Open()
    {
        GetWindow<HotReloading>(ObjectNames.NicifyVariableName(nameof(HotReloading)));
    }
}

public class TestClass
{
    public int Value { get; set; }
}

もちろんクラスにSerializableアトリビュートをつければホットリロードの対象となります。

また以下のようにNonSerializeアトリビュートをつければ、強制的にホットリロードの対象から外せます。

using System;
using UnityEditor;
using UnityEngine;

public class HotReloading : EditorWindow
{
    [field: NonSerialized] private int Test { get; set; }
    private int _updateCount;

    private void Update()
    {
        _updateCount++;
        if (_updateCount % 30 != 0)
        {
            return;
        }
        
        Debug.Log(Test++);
    }

    [MenuItem("Window/HotReloading")]
    private static void Open()
    {
        GetWindow<HotReloading>(ObjectNames.NicifyVariableName(nameof(HotReloading)));
    }
}

エディタ拡張におけるホットリロードに関する知識の重要性

さてこのように、UnityのホットリロードはEditorWindowなどのエディタ拡張にも影響することがわかりました。
この仕様を理解せずにエディタ拡張を進めると、スクリプトコンパイル時にある値は保持され、ある値は保持されないという状況に陥ります。

例えば入力している文字がコンパイル時に消えたり、折りたたみ状態がリセットされる、などといった非常に残念なツールが出来上がります。

冒頭にも書いたようにランタイムではホットリロードの仕様理解と意識が薄れている昨今ですが、
エディタ拡張を行う場合にはしっかりこの仕組みを理解して実装するようにしましょう。

関連

light11.hatenadiary.com

参考

docs.unity3d.com