Unityでのシリアライズ

この記事では、Unityのシリアライゼーション・システムの概要を紹介します。このシステムをよく理解することは、あなたの開発の効果や、あなたが作るもののパフォーマンスに大きな影響を与える。さあ、始めよう。
モノ」のシリアライゼーションはUnityの核心である。我々の機能の多くは、シリアライゼーション・システムの上に構築されている:
- スクリプトに保存されたデータを保存する。これはほとんどの人がある程度知っていることだろう。
- インスペクターのウィンドウ。インスペクター・ウィンドウは、C#のapiと会話して、検査対象のプロパティの値を把握することはない。オブジェクトにそれ自身のシリアライズを依頼し、シリアライズされたデータを表示する。
- プレハブ。内部的には、プレハブは1つ(または複数)のゲームオブジェクトとコンポーネントのシリアライズされたデータストリームです。プレハブインスタンスは、このインスタンスのシリアライズデータに対して行われるべき修正のリストです。プレハブという概念は、実際にはエディター時にしか存在しない。プレハブの修正はUnityがビルドするときに通常のシリアライズストリームにベイクされ、それがインスタンス化されるとき、インスタンス化されたゲームオブジェクトは、エディタに住んでいたときにプレハブであったことを知らない。
- インスタンス化。Instantiate()をプレハブやシーンに存在するゲームオブジェクト、またはその他のもの(UnityEngine.Objectから派生するものはすべてシリアライズ可能)に対して呼び出すと、オブジェクトをシリアライズしてから新しいオブジェクトを作成し、新しいオブジェクトにデータを「デシリアライズ」します。(その後、同じシリアライズコードを別のバリアントで再度実行し、どのUnityEngine.Objectが参照されているかをレポートするために使用します。次に、参照されるすべてのUnityEngine.ObjectがInstantiated()されるデータの一部であるかどうかをチェックします。リファレンスが「外部」のもの(テクスチャなど)を指している場合は、そのリファレンスをそのまま維持し、「内部」のもの(子ゲームオブジェクトなど)を指している場合は、対応するコピーにリファレンスをパッチします)。
- 節約だ。.unityシーンファイルをテキストエディタで開き、unityを "force text serialization "に設定すると、yamlバックエンドでシリアライザーを実行します。
- ローディング。意外に思われないかもしれないが、後方互換ローディングはシリアライゼーションの上に構築されたシステムでもある。 エディタ内でのyamlロードは、シーンやアセットのランタイム・ローディングと同様に、シリアライゼーション・システムを使用します。アセットバンドルもシリアライゼーション・システムを利用する。
- エディタコードのホットリロードエディタースクリプトを変更すると、すべてのエディターウィンドウをシリアライズし(UnityEngine.Objectから派生します!)、すべてのウィンドウを破棄し、古いC#コードをアンロードし、新しいC#コードをロードし、ウィンドウを再作成し、最後にウィンドウのデータストリームを新しいウィンドウにデシリアライズします。
- Resource.GarbageCollectSharedAssets().これはネイティブのガベージ・コレクターで、C#のガベージ・コレクターとは異なる。これは、シーンをロードした後に実行されるもので、前のシーンから参照されなくなったものを見つけ出し、それらをアンロードできるようにするものだ。ネイティブのガベージコレクタは、オブジェクトが外部のUnityEngine.Objects.Objectsへの参照をすべて報告するようにシリアライザを使用するモードで実行します。これは、scene1で使われていたテクスチャが、scene2をロードするときにアンロードされるようにするものだ。
シリアライゼーション・システムはC++で書かれており、すべての内部オブジェクト・タイプ(Textures、AnimationClip、Cameraなど)に使用しています。シリアライズはUnityEngine.Objectレベルで行われ、各UnityEngine.Objectは常に全体としてシリアライズされます。それらは他のUnityEngine.Objectsへの参照を含むことができ、それらの参照は適切にシリアライズされます。
今、あなたは、このようなことはあまり気にせず、ただ機能することに満足して、実際にコンテンツを作ることに取り掛かりたいと言うかもしれない。というのも、MonoBehaviourコンポーネントのシリアライズにも同じシリアライザーを使用しているからです。シリアライザーは非常に高いパフォーマンスを要求されるため、C#開発者がシリアライザーに期待するような動作をするわけではありません。ここでは、シリアライザーがどのように機能するのか、またシリアライザーを最大限に活用するためのベストプラクティスについて説明します。
私のスクリプトのフィールドがシリアライズされるためには何が必要ですか?
- public であるか、[SerializeField] 属性を持つ。
- 静的であってはならない
- 定数外
- 読み取り専用ではない
- フィールドタイプはシリアライズできる型でなければならない。
どのフィールドタイプをシリアライズできるか?
- Serializable]属性を持つカスタム非抽象クラス。
- Serializable]属性を持つカスタム構造体。(Unity4.5の新機能)
- UntiyEngine.Object から派生するオブジェクトへの参照
- プリミティブデータ型(int,float,double,bool,stringなど)
- 直列化できるフィールド型の配列
- 直列化できるフィールド型のリスト<T>。
ここまでは順調だ。では、シリアライザーが私の期待とは異なる振る舞いをする状況とは、どのようなものなのでしょうか?
カスタムクラスは構造体のように振る舞う
[シリアライズ可能]
クラス 動物
{
public string name;
}
class MyScript :モノ・ビヘイビア
{
public Animal[] animals;
}
配列 animals に1つの Animal オブジェクトへの参照を3つ入れると、シリアライズストリームには3つのオブジェクトが現れます。これがデシリアライズされると、3つの異なるオブジェクトが存在することになる。参照を含む複雑なオブジェクトグラフをシリアライズする必要がある場合、Unityのシリアライザーがすべて自動でやってくれるとは限りません。Unityが自分でシリアライズしないものをシリアライズする方法については、以下の例を参照してください。
カスタムクラスは "インライン "でシリアライズされるため、そのデータが使用されるMonoBehaviourの完全なシリアライズデータの一部になるからです。public Camera myCamera "のようにUnityEngine.Object派生クラスへの参照を持つフィールドがある場合、そのカメラのデータはインラインではシリアライズされず、UnityEngine.Objectへの実際の参照がシリアライズされます。
カスタムクラスのnullをサポートしない
ポップクイズ。このスクリプトを使用しているMonoBehaviourをデシリアライズするときに、何回アロケーションが行われるか:
クラスのテスト:モノ・ビヘイビア
{
公開トラブルT;
}
[シリアライズ可能]
クラス・トラブル
{
public Trouble t1;
public トラブル t2;
public トラブル t3;
}
テスト・オブジェクトのアロケーションを1つ期待してもおかしくはない。また、テスト・オブジェクト用とトラブル・オブジェクト用の2つのアロケーションがあってもおかしくない。正解は729である。シリアライザは null をサポートしていません。オブジェクトをシリアライズしてフィールドがNULLだったら、その型の新しいオブジェクトをインスタンス化してシリアライズするだけだ。もちろん、これは無限サイクルにつながる可能性があるので、7レベルという比較的魔法のような深さ制限を設けている。その時点で、カスタムクラス/構造体、リスト、配列の型を持つフィールドのシリアライズを止めるだけだ。[1]
多くのサブシステムがシリアライゼーション・システムの上に構築されているため、Testモノビヘイビア用のシリアライゼーション・ストリームが予想外に大きくなると、これらすべてのサブシステムの動作が必要以上に遅くなる。お客様のプロジェクトでパフォーマンスの問題を調査すると、ほとんどの場合この問題が見つかり、Unity 4.5でこの状況に対する警告を追加しました。実際、警告の実装をめちゃくちゃにしてしまったので、警告がたくさん出てしまい、すぐに修正する以外に選択肢がなくなってしまった。この警告はなくなりませんが、「プレイモードに入る」ごとに1回しか表示されませんので、スパムメールに悩まされることはありません。それでもコードを修正したいだろうが、自分の都合のいい時に修正できるはずだ。
ポリモーフィズムのサポートなし
もし
public 動物[] 動物
犬、猫、キリンのインスタンスを置くと、シリアライズ後にAnimalのインスタンスが3つできる。
この制限に対処する一つの方法は、インラインでシリアライズされる「カスタム・クラス」にのみ適用されることを理解することだ。他のUnityEngine.Objectへの参照は実際の参照としてシリアライズされ、それらのためにポリモーフィズムは実際に機能します。ScriptableObject派生クラスか別のMonoBehaviour派生クラスを作り、それを参照する。この方法の欠点は、モノビヘイビアやスクリプタブル・オブジェクトをどこかに保存する必要があり、インラインでうまくシリアライズできないことだ。
このような制限がある理由は、シリアライゼーション・システムの核となる基盤のひとつが、オブジェクトのデータストリームのレイアウトが前もってわかっていて、フィールドの中に何が格納されているかではなく、クラスのフィールドの型に依存するというものだからだ。
Unityのシリアライザーがサポートしていないものをシリアライズしたい。どうすればいい?
多くの場合、最善の方法はシリアライズ・コールバックを使うことである。これによって、シリアライザーがフィールドからデータを読み込む前と、フィールドへの書き込みが終わった後に通知を受けることができる。これを使えば、実行時に、シリアライズしにくいデータを、実際にシリアライズするときとは異なる表現にすることができる。 Unityがデータをシリアライズする直前に、Unityが理解できるようにデータを変換するために使用します。また、Unityがフィールドにデータを書き込んだ直後に、シリアライズされたフォームを、実行時にデータを格納したいフォームに戻すためにも使用します。
例えば、木のデータ構造を持ちたいとしよう。Unityにデータ構造を直接シリアライズさせると、"nullをサポートしない "という制限によってデータストリームが非常に大きくなり、多くのシステムでパフォーマンスの低下を招きます:
UnityEngineを使用しています;
using System.Collections.Generic;
を使用しています;
public class VerySlowBehaviourDoNotDoThis :モノ・ビヘイビア
{
[シリアライズ可能]
public class Node
{
public string interestingValue = "value";
//以下のフィールドは、シリアライズデータが巨大になる原因である。
//クラスサイクル」を導入する。
public List<Node> children = new List<Node>();
}
// これがシリアライズされる
public Node root = new Node();
void OnGUI()
{
ディスプレイ(ルート);
}
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)
ディスプレイ(子);
if (GUILayout.Button ("Add child"))
node.children.Add (new Node ());
GUILayout.EndVertical();
GUILayout.EndHorizontal();
}
}
その代わりに、Unityにツリーを直接シリアライズしないように指示し、Unityのシリアライザに適したシリアライズされた形式でツリーを格納する別のフィールドを作成します:
UnityEngineを使用しています;
using System.Collections.Generic;
を使用しています;
public class BehaviourWithTree :MonoBehaviour, ISerializationCallbackReceiver
{
// 実行時に使用されるノードクラス
public class Node
{
public string interestingValue = "value";
public List<Node> children = new List<Node>();
}
// 直列化に使用するノードクラス
[シリアライズ可能]
public struct SerializableNode
{
public string interestingValue;
public int childCount;
public int indexOfFirstChild;
}
//実行時に使用するもののルート。シリアライズされていない。
Node root = new Node();
//unityがシリアライズするフィールド。
public List<SerializableNode> serializedNodes;
public void OnBeforeSerialize()
{
//unityはserializedNodesフィールドの内容を読み込もうとしている。
//そのフィールドに正しいデータを "ジャスト・イン・タイム "で書き出す。
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はserializedNodesフィールドに新しいデータを書き込みました。
//実際のランタイムデータに新しい値を入力しよう。
if (serializedNodes.Count > 0)
root = ReadNodeFromSerializedNodes (0);
else
root = new 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));
new Node() { を返す。
interestingValue = serializedNode.interestingValue,
children = 子どもたち
};
}
void OnGUI()
{
ディスプレイ(ルート);
}
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)
ディスプレイ(子);
if (GUILayout.Button ("Add child"))
node.children.Add (new Node ());
GUILayout.EndVertical();
GUILayout.EndHorizontal();
}
}
シリアライザーは、シリアライザーから来るこれらのコールバックを含め、通常はメインスレッド上で実行されないので、Unity APIを呼び出すという点でできることが非常に制限されることに注意してください。(シーンのローディングの一部として行われるシリアライゼーションは、ローディング・スレッド上で行われる。スクリプトからInstantiate()を呼び出すと、メインスレッドでシリアライズが行われる。)しかし、必要なデータ変換を行い、データを非ユニティシリアライザフレンドリーなフォーマットからユニティシリアライザフレンドリーなフォーマットにすることができます。
あなたは最後までやり遂げた!
ここまで読んでくれてありがとう。この情報のいくつかを、あなたのプロジェクトに役立ててほしい。
さようなら、ルーカス。(@lucasmeijer)
追記:これらの情報もすべてドキュメントに追加する。
[1] 嘘をつきました。正解は729ではありません。というのも、7レベルの深さ制限ができる前の非常に古い時代には、今書いたTroubleのようなスクリプトを作成すると、Unityはただ無限ループを繰り返し、メモリ不足に陥っていたからだ。5年前、私たちが最初にこの問題を解決したのは、クラスそのものと同じ型のフィールドタイプをシリアライズしないようにすることだった。Trouble1->Trouble2->Trouble1->Trouble2クラスを使ってサイクルを作るのは簡単だからだ。そのため、その直後、私たちは実際に7段階の深度制限を導入し、そのようなケースにも対応できるようにした。しかし、私が言いたいのは、そんなことはどうでもいいということだ。重要なのは、もしサイクルがあるとすれば、それは問題だということを理解することだ。
