n-yoda's blog

主にUnity3Dに関するあんまり技術的じゃないメモ的な何かを書いています。

UnityのシリアライズのルールとInstantiateと実行中のコンパイル

Serializeのルールを理解するとInstantiateや実行中のコンパイルに役に立ちますという話。

目次

シリアライズのルール

UnityEngine.Objectの派生クラスは、シリアライズして.prefabや.assetの形式で保存することができ、その際には以下の条件を満たすメンバ変数(フィールド)のみがシリアライズされます。派生クラスは、MonoBehaviour・UIBehaviour・ScriptableObject等を継承して作ることが出来ます。

  1. SerializedField属性が設定されているかpublicである。
  2. 型が以下のどれかである。
    1. int, string などの一部の組み込み型(5ではdecimalとobject以外全部?)
    2. enum
    3. UnityEngine.Objectの派生型
    4. Serializable属性が設定されたclassまたはstruct
      • ただしジェネリック型で無いこと。
      • 1, 2を満たすメンバ変数がシリアライズされる。
      • 実装は違うかもしれないが Vector3 などもこれに該当すると考える。
    5. a〜dの配列またはList<T>

共有データはScriptableObjectで

複数箇所から参照して共有したいオブジェクトは、Serializable属性付きのクラスではなくScriptableObjectを用いましょう。 データは冗長になってしまいますが、Hierarchyに配置出来るという利点のために、MonoBehaviourを用いることもあります。

例えば以下のコードを実行してみます。

using UnityEngine;
using System;
[ExecuteInEditMode]
public class HogeBehaviour : MonoBehaviour
{
    [Serializable] public class Hoge { public int piyo; }
    public Hoge hoge;
    public Hoge piyo;

    void Awake() {
        if (!Application.isPlaying) {
            hoge = new Hoge ();
            piyo = hoge;
        } else {
            if (piyo != hoge) {
                print("Omg! Piyo is not hoge!");
            }
        }
    }
}

piyo には hoge を代入しているので piyo == hoge となって欲しいところですが、print が実行されてしまいます。 これは、Unityが再生前に変数毎にシリアライズし、再生時にデシリアライズして新たなオブジェクトを生成するため、もはや hoge も piyo ももとの再生前のオブジェクトでは無くなってしまうからです。したがって、上のような等号が成り立って欲しい場合や、巨大なデータなど無駄なコピーを作って欲しくない場合は、ScriptableObjectを使いましょう。この扱いが悪いせいでシーンのサイズが数十MB大きくなるような有料アセット(AfterEffects関連だったかな?)を見たことがあります。

非Serializableな変数の対応

以下のものはシリアライズされません。

  • static変数
  • 標準ライブラリのクラスのほとんど(Dictionaryとか)
  • イベント

シリアライズされない変数は、実行開始時や、実行中のコンパイル後に初期化されてしまいます。 また、Instantiateで値がコピーされません。 結果として、NullReferenceExceptionが多発する場合があります。 従って、以下のような方法で対策する必要があります。

この辺りに気を遣うと、再取得とシリアライズのどちらが適切かで悩む場面が多いと思われます。インスペクタの表示が邪魔であれば HideInInspector で隠してしまうことは可能ですが、再取得のコストとシリアライズで増える容量のどちらを受け入れるか選ぶ必要があります。大抵どちらも微々たるものだと思いますが。

適切なシリアライズ設定は適切なInstantiateを可能にする

以上のように、必要なメンバ変数が全てシリアライズ可能で、それ以外は正しく再取得出来るように作って置くと、Instantiateでオブジェクトを複製する処理がシンプルになります。Instantiateは指定されたオブジェクトをシリアライズして、デシリアライズすることで複製するためです。その際、シリアライズされる参照であれば子オブジェクト同士の参照関係も正しくコピーされるという点も重要です。

適切なシリアライズ設定は実行中コンパイルを可能にする

実行中にスクリプトを編集すると、自動的にコンパイルして、シーンはそのままでプログラムのみリロードしてくれますが、ここでNullReferenceExceptionが多発することは非常に多いです。コンパイル後に、シーンをシリアライズし、新しいプログラムを読み込んだ後にそれをデシリアライズため、シリアライズ可能でない情報が失われてしまうためです。したがって、このNullReferenceExceptionは適切なシリアライズの設定と失われる情報の再取得によって防ぐことが出来ます。(2015/8/6追記:コンパイル時のリロードとその他のシリアライズでは仕様が少し違うかも?要確認。)

実行中コンパイルの問題:Coroutineが破棄される等

残念なことに、上記のようにSerializabilityに気を遣ってコードを書いても、恐らく実行中のコンパイルにはいくつか課題が残ると思われます。例えば、コンパイル後に実行中のCoroutineは破棄されてしまいます。他にも多分問題はあって、実行中コンパイルのためにそこまでこだわる必要は無いのかな…というかこだわっていないことが多いです…。