Version: 5.4
スクリプトの制限
UnityEvent

スクリプトシリアライゼーション

シリアライズは Unity エディターのまさに中枢です。多くの機能は中枢となるシリアライゼ―ションシステムの上に構築されており、特に重要なことは、Unity エディターを使用しているときには、あなたのスクリプトによって制御されるMonoBehaviour コンポーネントをシリアライズしています。

シリアライズを利用するビルトイン機能

Unity でのシリアライズ作業を理解するために、シリアライズを使用する Unity の機能を以下に列挙します。

インスペクターウィンドウ

インスペクターウィンドウは、それが検査しているプロパティーの値を把握するために C# API と通信しているのではありません。オブジェクトに自分自身でシリアライズするように要求し、その後、シリアライズされたデータを表示します。

プレハブ

内部的には、プレハブは 1 つ以上のゲームオブジェクトとコンポーネントのシリアライズしたデータストリームです。プレハブインスタンスは、このインスタンスのためにシリアライズされたデータに関する変更リストです。プレハブの概念は、実際には、エディターでプロジェクトを編集している間のみ存在しています。プレハブの変更は Unity がビルドを行う際、通常のシリアライズのストリームの中に返され、オブジェクトがビルドでインスタンス化されるとき、これらのオブジェクトがプレハブになるための参照がありません。

インスタンス化

シーンに存在する何か (プレハブやゲームオブジェクトなど) のために Instantiate() を呼び出すとき、Unity エディターはゲームオブジェクトをシリアライズします (UnityEngine.Object から派生するものすべてはシリアライズされます)。

それから、エディターが新しいオブジェクトを作り、データをゲームオブジェクトにデシリアライズ (つまり読み込み) します。次に、エディターが同じシリアライズのコードを異なるバリアントで実行し、他にどの UnityEngine.Object が参照されているかをチェックします。すべての参照された UnityEngine.ObjectInstantiated() されたデータであるがどうかを検証します。参照が(テクスチャのように)「外部」の何かを指している場合は、その参照をそのまま保持し、子ゲームオブジェクトのように「内部」の何かを指している場合には、対応するコピーの参照を適用します。

保存

テキストエディターで .unity シーンファイルを開き、Unity を force text serialization に設定すると、シリアライザをは YAML 形式でバックエンドで実行します。(詳細は www.yaml.org を参照してください。)

読み込み

後方互換性を持たせた読み込みも同じようにシリアライゼーションの上に構築されたシステムです。エディターでは YAML 形式の読み込みについてシリアライゼーションシステムを使用しているだけでなく、シーンのランタイム読み込み、アセットとアセットバンドルもシリアライゼーションシステムを使用しています。

Unity エディターコードのホットリロード

エディタースクリプトを変更するとすべてのエディターウィンドウをシリアライズします (エディターウィンドウも UnityEngine.Object から派生してます)。そのとき Unity はすべてのウィンドウを破棄し、古い C# のコードを破棄し、新しいコードを読み込み、ウィンドウを再作成した後、この新しいウィンドウに戻って、データストリームをデシリアライズします。

Resource.GarbageCollectSharedAssets()

Resource.GarbageCollectSharedAssets() is the native Unity garbage collector. Note that it has a different function to the C# garbage collector. It runs after you load a Scene, to asertain GameObjects from the previous Scene are no longer referenced, and so can be unloade. The native Unity garbage collector runs the serializer in a variation in which GameObjects report all references to external UnityEngine.Objects. This is how textures that were used by, Scene1, are unloaded in Scene2.

シリアライゼーションシステムは C++で書かれており、すべての内部のオブジェクトタイプ (Texture, AudioClip, Camera など) で使用されます。シリアライゼーションは、UnityEngine.Object レベルで行われ、各 UnityEngine.Object は毎回、全部シリアライズされます。これらは、他の UnityEngine.Object への参照を含めることができ、参照も適切にシリアライズされます。

シリアライズと Monobehaviour

スクリプトと関連している MonoBehaviour コンポーネントもまた、スクリプトと関連しています。シリアライザに対する要求がとても高いため、シリアライザが常に C# デベロッパーの期待どおりに動作するというわけにはいきません。シリアライズを最も効果的に行う方法を以下に説明します。

スクリプトのフィールドがシリアライズされていることを確認する方法

以下を確認します。

  • public[SerializeField] 属性を持つ
  • static ではないこと
  • const ではないこと
  • readonly ではないこと
  • シリアライズができる フィールドタイプ であること (以下を参照)

シリアライズできるフィールドタイプ

  • [Serializable] 属性を持つカスタム非抽象クラス
  • [Serializable] 属性を持つカスタム構造体( Unity4.5 から追加)
  • UnityEngine.Object から派生したオブジェクトの参照
  • プリミティブ型(int, float, double, bool, string, etc.)
  • シリアライズできるフィールドタイプの配列
  • シリアライズできるフィールドタイプの List<T>

シリアライザが期待した動作をしない状況

カスタムのクラスは構造体のように動作します

[Serializable]
class Animal
{
   public string name;
}

class MyScript : MonoBehaviour
{
      public Animal[] animals;
}

ひとつの Animal オブジェクトに 3 つの参照を持つ animals 配列を作成する場合、シリアライズストリームに 3 つのオブジェクトがあります。デシリアライズすると、それが 3つの異なるオブジェクトになります。参照が必要な複雑なオブジェクトグラフをシリアライズする必要がある場合は、そのすべてを自動的に Unity のシリアライザにやってもらうことはできません。そのオブジェクトグラフを自分でシリアライズするためにいくつかの作業を行う必要があります。Unity が自動的にシリアライズできないものをシリアライズする方法については、以下の例を参照してください。

これはカスタムクラスのみの話であるということに注意してください。カスタムクラスのデータは、使用される MonoBehaviour の完全なシリアライズデータの一部となるので、カスタムクラスは「インライン」でシリアライズされます。 public Camera myCamera のような UnityEngine.Object の派生クラスの何かへの参照を持つフィールドがある場合、その Camera からのデータは、インラインでシリアライズされません。その代りに、Camera の UnityEngine.Object への実際の参照がシリアライズされます。

カスタムクラスでは null はサポートされません

以下のスクリプトを使用する MonoBehaviour をデシリアライズするとき、いくつのアロケーションが発生するか考えてみてください。

class Test : MonoBehaviour
{
    public Trouble t;
}

[Serializable]
class Trouble
{
   public Trouble t1;
   public Trouble t2;
   public Trouble t3;
}

Test ゲームオブジェクトに対する 1 回と考えてもおかしくはありません。Test ゲームオブジェクトと Trouble ゲームオブジェクトに対する 2回と考えることもできます。

しかし、正解は 729 です。シリアライザは null をサポートしません。オブジェクトをシリアライズした際、フィールドが null のとき、Unity はその型の新しいオブジェクトをインスタンス化し、それをシリアライズします。明らかにこれは無限のサイクルにつながる可能性があるため、デプス制限が 7 層までと制限されています。その時点で、Unity はカスタムクラス、構造体、リスト、配列の型を持つフィールドのシリアライズを停止します。

Unity のサブシステムの多くはシリアライゼーションシステム上でビルドするため、Test MonoBehaviour のこの予想外に大きなシリアライゼーションのストリームは、これらすべてのサブシステムの実行速度の低下を招きます。

注意 多くのプロジェクトで重大なパフォーマンスの問題の原因となるため、Unity 4.5 以降警告メッセージを発するようになりました。

ポリモーフィズムはサポートされません

もし、public Animal[] animals に犬、猫、キリンのインスタンスを収納して、シリアライズを行うと、3 つの Animal インスタンスを持つことになります。

この制限にうまく対処するには、この制限はカスタムクラスのみに発生するということを覚えておくことです。カスタムクラスはインラインでシリアライズされるからです。他の ‘UnityEngine.Object’ への参照は、実際の参照としてシリアライズされて、ポリモーフィズムは正しく動作します。‘ScriptableObject’ 派生クラスや別の ‘MonoBehaviour’ 派生クラスを作り、それらを参照することが可能ます。欠点としては、Monobehaviour またはスクリプト可能なゲームオブジェクトをどこかに保存する必要があり、それを効果的にインラインでシリアライズできないことです。

これらの制限の理由は、シリアライズシステムの中心となる根拠の 1つはオブジェクトのデータストリームのレイアウトは事前にわかるようになっており、フィールド内で実際に何が保存されたかというよりむしろ、クラスのフィールドタイプに依存するためです。

Unity のシリアライザがサポートしないものをシリアライズするためには

多くの場合、最善のアプローチはシリアライズのコールバックを使用することです。シリアライザがフィールドからデータを読み込む前や、書き込みが完了した後に通知可能です。シリアライズのコールバックを使用して、実行時にシリアライズするのが難しいデータを別タイプのデータとして処理させたいときに使用できます。

Unity がデータをシリアライズする前に、Unityがシリアライズできる形式に変換します。それから、Unity がフィールドにデータを書き込んだ直後に変換して、シリアライズしたものから実行時に使用したいタイプに戻します。

例えば、データをツリー構造にしたいとします。Unity に直接データ構造をシリアライズさせると、「null をサポートしない」制限によりデータストリームは、非常に大きくなり、多くのシステムにおいて性能の劣化につながります。

using UnityEngine;
using System.Collections.Generic;
using System;

public class VerySlowBehaviourDoNotDoThis : MonoBehaviour
{
    [Serializable]
    public class Node
    {
        public string interestingValue = "value";

        //The field below is what makes the serialization data become huge because
        //it introduces a 'class cycle'.
        public List<Node> children = new List<Node>();
    }
    
    //this gets serialized  
    public Node root = new Node();  

    void OnGUI()
    {
        Display (root);
    }

    void Display(Node node)
    {
        GUILayout.Label ("Value: ");
        node.interestingValue = GUILayout.TextField(node.interestingValue, GUILayout.Width(200));

        GUILayout.BeginHorizontal ();
        GUILayout.Space (20);
        GUILayout.BeginVertical ();

        foreach (var child in node.children)
            Display (child);
        if (GUILayout.Button ("Add child"))
            node.children.Add (new Node ());

        GUILayout.EndVertical ();
        GUILayout.EndHorizontal ();
    }
}

代わりに、Unity がツリーを直接シリアライズしないように指示し、別のフィールドを作成してツリーを Unity のシリアライザに適したシリアライズした形式で保存します。

using System.Collections.Generic;
using System;

public class BehaviourWithTree : MonoBehaviour, ISerializationCallbackReceiver
{
    // Node class that is used at runtime.
    // This is internal to the BehaviourWithTree class and is not serialized.
    public class Node
    {
        public string interestingValue = "value";
        public List<Node> children = new List<Node>();
    }

    // Node class that we will use for serialization.
    [Serializable]
    public struct SerializableNode
    {
        public string interestingValue;
        public int childCount;
        public int indexOfFirstChild;
    }

    // The root node used for runtime tree representation. Not serialized.
    Node root = new Node(); 

    // This is the field we give Unity to serialize.
    public List<SerializableNode> serializedNodes;

    public void OnBeforeSerialize()
    {
        // Unity is about to read the serializedNodes field's contents.
        // The correct data must now be written into that field "just in time".
        if (serializedNodes == null) serializedNodes = new List<SerializableNode>();
        if (root == null) root = new Node ();
        serializedNodes.Clear();
        AddNodeToSerializedNodes(root);
        // Now Unity is free to serialize this field, and we should get back the expected 
        // data when it is deserialized later.
    }

    void AddNodeToSerializedNodes(Node n)
    {
        var serializedNode = new SerializableNode () {
            interestingValue = n.interestingValue,
            childCount = n.children.Count,
            indexOfFirstChild = serializedNodes.Count+1
        };
        serializedNodes.Add (serializedNode);
        foreach (var child in n.children)
            AddNodeToSerializedNodes (child);
    }

    public void OnAfterDeserialize()
    {
        //Unity has just written new data into the serializedNodes field.
        //let's populate our actual runtime data with those new values.
        if (serializedNodes.Count > 0) {            
            ReadNodeFromSerializedNodes (0, out root);
        }
        else
            root = new Node ();
    }

    int ReadNodeFromSerializedNodes(int index, out Node node)
    {
        var serializedNode = serializedNodes [index];
        // Transfer the deserialized data into the internal Node class
        Node newNode = new Node() {
            interestingValue = serializedNode.interestingValue,
            children = new List<Node> ()
        };
        // The tree needs to be read in depth-first, since that's how we wrote it out.
        for (int i = 0; i != serializedNode.childCount; i++) {
            Node childNode;
            index = ReadNodeFromSerializedNodes (++index, out childNode);
            newNode.children.Add (childNode);
        }
        node = newNode;
        return index;
    }

    // This OnGUI draws out the node tree in the Game View, with buttons to add new nodes as children.
    void OnGUI()
    {
        if (root != null)
            Display (root);
    }

    void Display(Node node)
    {
        GUILayout.Label ("Value: ");
        // Allow modification of the node's "interesting value".
        node.interestingValue = GUILayout.TextField(node.interestingValue, GUILayout.Width(200));

        GUILayout.BeginHorizontal ();
        GUILayout.Space (20);
        GUILayout.BeginVertical ();

        foreach (var child in node.children)
            Display (child);
        if (GUILayout.Button ("Add child"))
            node.children.Add (new Node ());
            

        GUILayout.EndVertical ();
        GUILayout.EndHorizontal ();
    }
}

シリアライザから発生するこれらのコールバックも含め、シリアライザは通常、メインスレッド上にはありません。そのため、Unity API の呼び出しに関してできることはとても限られています。ただし、Unity がシリアライズできない形式から Unityがシリアライズできる形式への必要なデータ変換は行えます。

スクリプトのシリアライズエラー

スクリプトがコンストラクターかフィールドイニシアライザーから Unity API を呼び出すとき、または、デシリアライゼーション (読み込み) の間に、エラーがトリガーされます。ここでは、エラーの要因となる良くない例を紹介します。

Unity API のほとんどは、例えば、MonoBehaviour の StartUpdate のようなメインスレッドから呼び出します。

Unity API の一部だけが、Debug.LogMathf などのスクリプトコンストラクターやフィールドイニシアライザーから呼び出されるべきです。その理由は、デシリアライゼーションの間、クラスのインスタンスをコンストラクトするときにはコンストラクターが呼び出され、これはメインスレッド上でのみ実行されるべきなのに、最終的にはメインスレッド以外で実行されるからです。そのため、スクリプトコンストラクターやフィールドイニシアライザーから Unity API すべてを呼び出す場合、エラーが発生します。

Unity 5.4 ではたいていの場合、これらのエラーには例外が投げられず、スクリプトの処理を妨げることはありません。これにより、プロジェクトを Unity 5.4 にアップグレードするプロセスは簡単になります。ただし、これらのエラーは、後続の Unity リリースでも例外を発生させる要因になります。そのため、5.4 にアップグレードするときはすぐに、すべてのエラーを修正するべきです。

Unity API のコンストラクターかフィールドイニシアライザーからの呼び出し

Unity が MonoBehaviour または ScriptableObject の派生クラスのインスタンスを作成するとき、マネージドオブジェクトを作成するためにデフォルトコンストラクターを呼び出します。これは、メインループに入る前と画面が完全に読み込まれる前に発生します。フィールドイニシアライザーもマネージドオブジェクトのデフォルトコンストラクターから呼び出されています。一般的には、大抵の Unity API にとっては安全でないため、Unity API をコンストラクターからは呼び出さないでください。

悪い

//NOTE: THIS IS A DELIBERATE BAD EXAMPLE TO DEMONSTRATE POOR PRACTISE  - DO NOT REUSE

public class FieldAPICallBehaviour : MonoBehaviour
{
   public GameObject foo = GameObject.Find("foo");   // This line generates an error 
                        // message as it should not be called from within a constructor

}
//NOTE: THIS IS A BAD EXAMPLE TO DEMONSTRATE POOR PRACTISE - DO NOT REUSE

public class ConstructorAPICallBehaviour : MonoBehaviour
{
   ConstructorAPICallBehaviour()
   {
       GameObject.Find("foo");   // This line generates an error message
                                // as it should not be called from within a constructor
   }
}

これらの例は両方ともエラーメッセージ「Find is not allowed to be called from a MonoBehaviour constructor (or instance field initializer), call in in Awake or Start instead.(Find は MonoBehaviour コンストラクター、または、インスタンスフィールドイニシアライザーから呼び出すことはできません。代わりに、Awake か Start から呼び出してください。)」を発します。

MonoBehaviour.Start で Unity API への呼び出しを行うことにより修正できます。

シリアライズ中に呼び出されるメソッド

Unity がシーンを読み込む場合、保存されたシーンからマネージドオブジェクトを再作成し、保存した値に設定します (デシリアライゼーション)。マネージドオブジェクトを作成するためには、オブジェクトのデフォルトコンストラクターを呼び出します。オブジェクトを参照するフィールドが保存され (シリアライゼーション)、オブジェクトのデフォルトコンストラクターが Unity API を呼び出すと、シーンを読み込む際にエラーが発生します。以前のエラーで、まだメインループを開始しておらず、シーンが完全に読み込まれていません。この状態は、たいていの Unity API にとって安全ではないと考えられます。

悪い

//NOTE: THIS IS A BAD EXAMPLE TO DEMONSTRATE POOR PRACTISE  - DO NOT REUSE

public class SerializationAPICallBehaviour : MonoBehaviour
{
   [System.Serializable]
   public class CallAPI
   {
       public CallAPI()
       {
           GameObject.Find("foo"); // This line generates an error message 
                                                 // as it should not be called during serialization

       }
   }

   CallAPI callAPI;
}

この例はエラーメッセージ Find is not allowed to be called during serialization, call it from Awake or Start instead (Find はシリアライゼーションの間は呼び出しすることができません。代わりに、Awake か Start から呼び出してください) を発します。

これを修正するには、コードのリファクターリングを行い、シリアライズされたオブジェクトに対して、コンストラクターから Unity API の呼び出しを行わないようにします。オブジェクトに対し Unity API の呼び出しが必要な場合は、メインスレッド内で、StartAwakeUpdate などの MonoBehaviour コールバックの 1つから行うようにします。

スクリプトの制限
UnityEvent