ジェネリック関数
UnityEvent

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

“もの(オブジェクト)”のシリアル化は,Unityのコアです。Unityの機能の多くは,シリアライゼーションシステムの上に構築されています:

  • インスペクタウインドウ インスペクタウインドウは,それがインスペクトされているプロパティの値が何であるかを把握するためにC#のAPIを使用しているのではありません。オブジェクトを自分自身でシリアル化するために要求し,その後,シリアライズされたデータを表示します。

  • プレハブ. 内部的には,プレハブは,1つ(または複数)のゲームオブジェクトとコンポーネントをシリアライズしたデータストリームです。プレハブインスタンスは,シリアライズされたデータになされるべき変更リストになります。プレハブの概念は,実際には,エディタにおいてのみ存在しています。プレハブの変更はUnityがビルドを行う際,通常のシリアライズの流れの中でベイクされ,それがインスタンス化された時,インスタンスされたゲームオブジェクトは,エディタ上では,プレハブだったのかは分かりません。

  • インスタンス化. エディタ上でのプレハブ,シーンの中のゲームオブジェクト,または他の何か(UnityEngine.Objectで派生したシリアライズ可能なオブジェクト)でInstantiate()を呼び出した時,オブジェクトをシリアライズし,新しいオブジェクトを作成します。その後,新しいオブジェクトのデータを“デシリアライズ”します。(その時,他の’UnityEngine.Objects’が参照される場合,異なった形として,再び同じシリアライズのコードを実行します。データがInstantiated()の一部であるならば,UnityEngine.Objects のすべての参照をチェックします。参照が“外部”の(テクスチャのように)何かを指している場合は,そのまま,その参照を保持し,“内部”の(子ゲームオブジェクトのように)何かを指している場合には,対応するコピーへの参照を適用します。

  • 保存. テキストエディタで’.unity’シーンファイルを開いたとき,Unityを“force text serialization” に設定している場合は,シリアライザをYAML形式でバックエンドで実行します。

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

  • エディタコードのホットリロード. エディタスクリプトを変更するとすべてのエディタウィンドウをシリアライズし(それらはUnityEngine.Object由来してます),その時,すべてのウィンドウは破棄されます。古いC#のコードをアンロードし,新しいコードをロードして,Windowsを再作成した後,この新しいウィンドウに戻って,ウィンドウのデータストリームをデシリアライズします。

  • ‘Resource.GarbageCollectSharedAssets()’. これはUnityのネイティブなガベージコレクタで,C#のガベージコレクタとは違うものです。それはシーンをロードした後,前のシーンのものが参照されていないかを把握して実行するもので,参照されてないものをアンロードできます。ネイティブガベージコレクタは外部のUnityEngine.Objects に参照した全てのオブジェクトレポートを得たときに,シリアライザモードで実行します。これはシーン2をロードするとき,シーン1で使用されたテクスチャについて(使用されてないと報告受けて),アンロードするものです。

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

今,実際にいくつかのコンテンツを作成し,それが動作することに満足している場合,あなたはあまり関係がないことだと思うかもしれません。

これがあなたに関係している所は,あなたのスクリプトがバックグランドで ‘MonoBehaviour’ コンポーネントをシリアライズするために,同じシリアライザを使用していることです。すべてのケースにおいて,C#デベロッパーがシリアライザに期待するようには正確に動作しません。なぜなら,非常に高いパフォーマンスをシリアライザが要求するためです。このドキュメントのパートでシリアライザがどのように機能するか,およびそれを最大限に活用する方法と,いくつかの最適な演習について説明します。

私のスクリプトにおいてシリアライズするには何が必要ですか?

  • ‘public, または ’[SerializeField]’属性を持つ
  • ’static’ではないこと
  • ’const’ではないこと
  • ’readonly’ではないこと
  • ’fieldtype’はシリアライズができるタイプである必要があります。

どのfieldtypeをシリアライズできますか?

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

シリアライザが期待した動作をしない状況とはどのようなものですか?

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

[Serializable]
class Animal
{
   public string name;
}

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

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

これはカスタムクラスにとって それらのデータは使用されているMonoBehaviourのための完全なserialization dataの一部となるので,“インライン”にシリアライズされたことに注意してください。’public Camera myCamera’のように,’UnityEngine.Object’派生クラスで何かへの参照を持つフィールドがある場合,そのcameraからのデータは,インラインでシリアライズさず, ‘UnityEngine.Object’ への実際の参照がシリアライズされます。

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

さてクイズです。このスクリプトを使用しているMonoBehaviourをデシリアライズするとき,どれだけ配分されるでしょうか?:

class Test : MonoBehaviour
{
    public Trouble t;
}

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

テストオブジェクトとして,1アロケーションを期待することはおかしいことではありません。また,テストオブジェクトとトラブルオブジェクトとして2アロケーションを期待するのもおかしくありません。正解は729です。シリアライザはnullをサポートしていません。オブジェクトをシリアライズした際,フィールドがnullのとき,その型の新しいオブジェクトをインスタンス化し,それをシリアライズします。明らかにこれは無限のサイクルにつながる可能性があるため,おまじないとして相対的に7層の深さまでと制限されています。その(7層になった)時点で,カスタムクラス/構造体及びリストと配列の型を持つフィールドのシリアライズを停止します。

私たちのサブシステムの多くは,シリアライゼーションシステム上に構築しているため,Test monobehaviourのためのこの予想外に大きなserializationstreamは,これらすべてのサブシステムの実行の速度低下を招きます。私たちがお客様のプロジェクトにおけるパフォーマンスの問題を調査すると,ほとんど常にこの問題がでてきました。このような状況のため私たちはUnity4.5で警告を追加しました。

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

このフィールドを持ち,犬,猫,キリンのインスタンスを収納して,シリアライズを行うと,3つのAnimalインスタンスを持つことになります。

public Animal[] animals この制限に対処する一つの方法は,そのシリアライズされたインラインを取得する“custom classes” にのみに適用することです。他の’UnityEngine.Objects’への参照は,実際の参照としてシリアライズされて,ポリモーフィズムは正しく動作します。あなたは’ScriptableObject’継承クラスまたは別の ‘MonoBehaviour’ 継承クラスを作り,それらを参照すると思います。それの欠点は,あなたがどこかにその monobehaviour または ScriptableObject オブジェクトを保存する必要があり,インラインにうまくそれをシリアライズできないことです。

これらの制限の理由は,シリアライゼーションシステムのコア部分の一つが事前に既知のオブジェクトのデータストリームのレイアウトであり,フィールド内の処理の内容ではなく,クラスのフィールドのタイプに依存することです。

Unityのシリアライザがサポートしていないものをシリアライズするためには何をすればいいですか?

多くの場合,最善のアプローチはシリアライズのコールバックを使用することです。シリアライザがあなたのフィールドからデータを読み込む前に,または書き込みが完了した後に通知させることができます。実際にシリアライズする時よりも,実行時にシリアライズするのが難しいデータに異なる表現を持たせるためにこれを使用することができます。 Unityがあなたのデータをシリアライズする前に,Unityが正しく理解するように変換させるために使用し,また,Unityはあなたのフィールドにデータを書き込んだ直後にも,実行時のあなたのデータを持ち,あなたが元のフォームに戻るためにシリアライズされたフォームから変換するためにそれを使用します。

例えば,あなたはデータをツリー構造にしたいとしましょう。Unityに直接データ構造をシリアライズさせた場合,“no support for 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 UnityEngine;
using System.Collections.Generic;
using System;

public class BehaviourWithTree : MonoBehaviour, ISerializationCallbackReceiver
{
    //node class that is used at runtime
    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 of what we use at runtime. not serialized.
    Node root = new Node(); 

    //the field we give unity to serialize.
    public List<SerializableNode> serializedNodes;

    public void OnBeforeSerialize()
    {
        //unity is about to read the serializedNodes field's contents. lets make sure
        //we write out the correct data into that field "just in time".
        serializedNodes.Clear();
        AddNodeToSerializedNodes(root);
    }

    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)
            root = ReadNodeFromSerializedNodes (0);
        else
            root = new Node ();
    }

    Node ReadNodeFromSerializedNodes(int index)
    {
        var serializedNode = serializedNodes [index];
        var children = new List<Node> ();
        for(int i=0; i!= serializedNode.childCount; i++)
            children.Add(ReadNodeFromSerializedNodes(serializedNode.indexOfFirstChild + i));
    
        return new Node() {
            interestingValue = serializedNode.interestingValue,
            children = children
        };
    }

    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 APIの呼び出しが大きく制限されています。ただし,non-unity-serializer-friendly formatからのunity-serializer-friendly-formatからデータを取得し,必要なデータ変換ができます。

ジェネリック関数
UnityEvent