Version: 2023.2
言語: 日本語
起動時エディタースクリプト実行
スクリプトのコンパイル

スクリプトのシリアル化

シリアル化 (シリアライゼーション) は、データ構造やゲームオブジェクトの状態を Unity が保存して後で再構築できる形式に変換する自動処理です。

Unity プロジェクトのデータを整理する方法は、Unity がそのデータをシリアライズする方法に影響し、プロジェクトのパフォーマンスに重大なインパクトを与える可能性があります。ここでは、Unity のシリアル化のための概要と、プロジェクトの最適化方法について説明します。

このセクションでは、以下のトピックについて説明します。

シリアライゼーションの規則

Unity のシリアライザーは、特にランタイムに効率的に動作するように設計されています。このため、Unity でのシリアル化は他のプログラミング環境でのシリアル化とは挙動が異なります。Unity のシリアライザーは、C# のクラスのプロパティではなく、フィールド に直接作用します。そのため、フィールドがシリアル化されるために準拠しなければならない規則があります。次のセクションでは、Unity でフィールドシリアライゼーションを使用するための概要を説明します。

フィールドシリアライゼーションを使用するには、フィールドが以下の状態であることを確認します。

  • public であるか、または SerializeField 属性をもっていること。
  • 静的でないこと。
  • const ではないこと。
  • 読み取り専用でないこと。
  • シリアラル化可能なフィールド型であること。
    • プリミティブなデータ型 (int、float、double、bool、string など) であること。
    • Enum types (32 bits or smaller)
    • 固定サイズバッファであること。
    • Unity の組み込み型 (例えば、Vector2、Vector3、Rect、Matrix4x4、Color、AnimationCurve) であること。
    • Serializable 属性をもつカスタム構造体であること。
    • UnityEngine.Object から派生するオブジェクトへの参照であること。
    • Serializable 属性を持つカスタムクラスであること。(カスタムクラスのシリアライズ を参照)。
    • 上記のフィールド型の配列であること。
    • 上記のフィールド型の List<T> であること。

ノート: Unity は多階層型 (多次元配列、ジャグ配列、辞書、ネストしたコンテナ型) のシリアライゼーションをサポートしていません。これらをシリアライズする場合、2 つのオプションがあります。

カスタムクラスのシリアライゼーション

Unity がカスタムクラスをシリアライズするためには、クラスが以下の状態であることが必要です。

  • Serializable 属性をもっていること。
  • 静的ではないこと。

UnityEngine.Object から派生したクラスのインスタンスをフィールドに割り当てそのフィールドを保存する場合、Unity はフィールドをシリアライズしてそのインスタンスへの参照にします。Unity はそのインスタンス自体を個々にシリアライズします。そのため、インスタンスに複数のフィールドが割り当てられても重複しません。しかし、UnityEngine.Object から派生しないカスタムクラスの場合、Unity はインスタンスの状態を、それらを参照する MonoBehaviour や ScriptableObject のシリアル化されたデータに直接加えます。inlineSerializeReference の 2 つの方法があります。

  • インラインシリアライゼーション: デフォルトでは、クラスを参照するフィールドに SerializeReference を指定しない場合、Unity はカスタムクラスを値によってインラインでシリアライズします。つまり、カスタムクラスのインスタンスへの参照を複数の異なるフィールドに保存する場合、これらはシリアライズされると別々のオブジェクトになります。それらはシリアライズされると別々のオブジェクトになります。そして、Unity がフィールドをデシリアライズすると、それらは同一のデータを持つ異なる別個のオブジェクトを含みます。
  • SerializeReference シリアライゼーション: SerializeReference を指定する場合、Unity はオブジェクトをマネージ参照として確立します。ホストオブジェクトは、オブジェクトをそのシリアル化されたデータに直接保存しますが、専用のレジストリセクションに格納します。

SerializeReference は若干のオーバーヘッドを加えますが、以下のケースをサポートします。

  • フィールドは NULL でもよい。インラインシリアライゼーションでは、NULL を表現することはできません。代わりに、NULL を未割り当てのフィールドを持つインラインオブジェクトに置き換えます。
  • 同じオブジェクトへの複数の参照。SerializeReference を使用せずに、カスタムクラスのインスタンスへの参照を複数の異なるフィールドに格納すると、シリアライズ時にそれらは別々のオブジェクトになります。
  • グラフや循環データ (例えば、それ自体に戻る参照を持つオブジェクトなど)。インラインクラスのシリアライゼーションは、NULL や参照の共有をサポートしません。そのため、データの循環は、インスペクターの動作の異常、コンソールエラー、無限ループなど、予期せぬ結果につながる可能性があります。
  • ポリモーフィズム。親クラスから派生したクラスを作成し、親のクラスを型として使用するフィールドに割り当てる場合、SerializeReference なしに、Unity は親クラスに属するフィールドのみをシリアライズします。Unity がクラスインスタンスをデシリアライズするとき、派生クラスではなく、親クラスをインスタンス化します。
  • When a data structure requires a stable identifier to point to a specific object without hardcoding the object’s array position or searching the entire array. See Serialization.ManagedReferenceUtility.SetManagedReferenceIdForObject.

ノート: インラインシリアライゼーションはより効率的なので、SerializeReference がサポートする機能の 1 つを特に必要としない限り、インラインシリアライゼーションを使用すべきです。SerializeReference の使用方法の詳細については、SerializeReference のドキュメントを参照してください。

プロパティのシリアル化

Unity では、以下のような場合を除き、通常プロパティをシリアル化することはありません。

  • プロパティが明示的なバッキングフィールドを持つ場合、Unity は通常のシリアライゼーション規則に従ってそれをシリアライズします。以下はその例です。
public int MyInt
{
get => m_backing;
private set => m_backing = value;
}
[SerializeField] private int m_backing;
  • Unity は、ホットリロード中のみ自動生成されたフィールドを持つプロパティをシリアル化します。

    public int MyInt { get; set; }

    自動生成されたフィールドを持つプロパティを Unity にシリアライズさせたくない場合は、[field: NonSerialized] 属性を使用します。

カスタムシリアライゼーション

Unity のシリアライザーがサポートしないもの (例えば、C# の Dictionary) をシリアル化したい場合があるかもしれません。最良の方法は、ISerializationCallbackReceiver インターフェースをクラスで実装することです。これにより、シリアライゼーションとデシリアライゼーション中の重要なポイントで呼び出されるコールバックを実装することができます。

  1. オブジェクトがシリアル化されようとするとき、Unity は OnBeforeSerialize() コールバックを呼び出します。このコールバックの内部で、データを Unity が理解できるものに変換することができます。例えば、C# の Dictionary をシリアライズする場合、データを Dictionary からキーの配列と値の配列にコピーします。
  2. OnBeforeSerialize() コールバックが完了した後、Unity は配列をシリアライズします。
  3. その後、オブジェクトがデシリアライズされると、Unity は OnAfterDeserialize() コールバックを呼び出します。このコールバックの内部で、データをメモリのオブジェクトに都合の良い形に変換して戻すことができます。例えば、キーと値の配列を使用して、C# Dictionary を再入力します。

Unity のシリアライゼーションの利用法

保存とロード

Unity はシリアル化を使って シーンアセットアセットバンドル をデバイスのメモリに (または、メモリから) ロードして保存します。これには、独自のスクリプティング API オブジェクトに格納される MonoBehaviour コンポーネントや ScriptableObject などのデータも含まれます。

Unity エディターの機能の多くは、基軸となるシリアル化システム上に構築されています。シリアル化で特に気を付けるべき 2 つの点は インスペクターウインドウ とホットリロードです。

Inspector ウィンドウ

Inspector ウィンドウには、検査されたオブジェクトのシリアライズされたフィールドの値が表示されます。Inspector 内で値を変更すると、Inspector はシリアライズされたデータを更新し、デシリアライゼーションを発生させて検査されたオブジェクトを更新します。

Unity のビルトインオブジェクトも、MonoBehaviour から派生したクラスなどのスクリプトオブジェクトも同様です。

Unity は、Inspector ウィンドウで値を表示したり変更したりする際に、C# のプロパティゲッターやセッターを一切呼び出しません。代わりにシリアライズされたバッキングフィールドに直接アクセスします。

ホットリロード

ホットリロードとは、エディターを開いている間にスクリプトを作成または編集し、そのスクリプトの動作をすぐに適用することです。変更を反映させるためにエディターを再起動する必要はありません。

スクリプトを変更して保存すると、Unity はその時点でロードされているすべてのスクリプトデータをホットリロードします。Unity はロードされたすべてのスクリプトにシリアライズ可能な変数を保存し、それらのスクリプトをリロードしてシリアライズ変数を復元します。ホットリロードでは、シリアライズ可能でないデータはすべて破棄されます。そのため、その後データにアクセスすることはできません。

これは、プロジェクト内のすべてのエディターウィンドウとすべての MonoBehaviour に影響します。シリアル化の他のケースとは異なり、private のフィールドは、 ‘SerializeField’ 属性を持たなくても、リロード時にデフォルトでシリアル化されます。

Unity は以下のようにスクリプトを再ロードします。

  1. Unity は、ロードされたすべてのスクリプトのすべての変数をシリアライズして保存します。
  2. Unity は、それらをシリアライズ前の元の値に復元します。
    • Unity は変数に [SerializeField] 属性がなくても、シリアライゼーションの要件を満たすすべての変数 (private 変数を含む) を復元します。例えば、スクリプトから再ロードした後に参照を NULL にしたい場合など、Unity が private 変数をリストアしないようにする必要がある場合があります。この場合は、[field: NonSerialized] 属性を使用します。
    • Unity は決して静的変数を復元しないので、スクリプトを再ロードした後に保持する必要のある状態には静的変数を使用しないでください。再ロードプロセスでそれらが破棄されるためです。

プレハブ

シリアル化では、プレハブ は 1 つまたは複数の ゲームオブジェクト または コンポーネント のシリアル化されたデータです。プレハブインスタンスには、プレハブソースとその変更のリストの両方への参照が含まれています。変更は、その特定のプレハブインスタンスを作成するために、Unity がプレハブソースに行う必要があるものです。

プレハブインスタンスは、Unity エディターでプロジェクトを編集している間だけ存在します。Unity エディターは、プレハブソースとプレハブインスタンスの変更の 2 つのシリアル化のデータからゲームオブジェクトをインスタンス化します。

インスタンス化

シーンに存在するもの (プレハブやゲームオブジェクトなど) 上でInstantiate を呼び出すと、以下が行われます。

  1. Unity はそれをシリアル化します。これはランタイムとエディターの両方で起こります。Unity は UnityEngine.Object から派生するものすべてをシリアル化することができます。
  2. Unity は新しいゲームオブジェクトを作成し、データを新しいゲームオブジェクトにデシリアライズします。
  3. Unity が同じシリアライゼーションコードを異なるバリアントで実行し、他にどの UnityEngine.Objects を参照しているかを連絡します。すべての参照された UnityEngine.Objects を確認し、それらが Unity がインスタンス化したデータの一部であるかどうかを確認します。参照がテクスチャのような外部のものを指している場合は、その参照をそのまま維持します。参照が子ゲームオブジェクトのような内部のものを指している場合には、Unity は、対応するコピーへの参照をパッチします。

使用しないアセットのアンロード

EditorUtility.UnloadUnusedAssetsImmediate は、ネイティブの Unity ガベージコレクターで、標準の C# ガベージコレクターとは異なる目的をもっています。シーンをロードした後に実行され、参照されなくなったオブジェクト (テクスチャなど) を確認して安全にアンロードします。ネイティブ Unity ガベージコレクターは、オブジェクトが外部の UnityEngine.Objects へのすべての参照を報告するバリエーションでシリアライザーを実行します。このようにして、1 つのシーンで使用されたテクスチャが、次のシーンでガベージコレクターによってアンロードされます。

エディターとランタイムのシリアライゼーションの違い

シリアライゼーションのほとんどはエディターで行われ、デシリアライズはランタイムに集中して行われます。Unity は一部の機能をエディターでのみシリアライズし、他の機能をエディターとランタイムの両方でシリアライズすることができます。

機能 エディター ランタイム
バイナリ形式のアセット 読み込み/書き込み対応 読み込み対応
YAML 形式のアセット 読み込み/書き込み対応 サポートなし
シーン、プレハブ、その他のアセットの保存 再生モード以外でサポート サポートなし
JsonUtility による個々のオブジェクトのシリアル化 JsonUtility による読み込み/書き込み対応。

EditorJsonUtility による追加のオブジェクト型へ対応
JsonUtility による読み込み/書き込み対応
SerializeReference サポートあり サポートあり
ISerializationCallbackReceiver サポートあり サポートあり
FormerlySerializedAs サポートあり サポートなし

オブジェクトは、UNITY_EDITOR スクリプティングシンボル 内でフィールドを宣言するときのように、エディターだけがシリアライズする追加フィールドを持つことができます。

public class SerializeRules : MonoBehaviour
{
# if UNITY_EDITOR
public int m_intEditorOnly;
# endif
}

上記の例では、m_intEditorOnly フィールドは、エディターでのみシリアライズされ、ビルドには含まれません。これにより、エディターだけで必要なデータをビルドから省くことで、メモリを節約することができます。そのフィールドを使用するコードもまた、条件付きで (例えば #if UNITY_EDITOR ブロックの中で) コンパイルする必要があります。そのようにすると、ビルド時にクラスをコンパイルできます。

エディターは、Unity がランタイムにのみシリアライズするフィールドを持つオブジェクトをサポートしていません (例えば、UNITY_STANDALONE ディレクティブ内でフィールドを宣言する場合など)。

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

スクリプトのシリアル化でエラーが発生することがあります。これらのうちのいくつかに対する修正を以下に示します。

“Find isn’t allowed to be called from a MonoBehaviour constructor (or instance field initializer), call in Awake or Start instead.” (Find は MonoBehaviour のコンストラクター、またはインスタンスフィールドのイニシャライザーから呼び出すことはできません。代わりに Awake または Start で呼び出してください。)

MonoBehaviour のコンストラクターやフィールドイニシャライザー内で GameObject.Find などのスクリプティング API を呼び出すと、このエラーが発生します。

これを修正するには、コンストラクターではなく MonoBehaviour.Start 内でスクリプティング API を呼び出します。

“Find isn’t allowed to be called during serialization, call it from Awake or Start instead.” (Find はシリアライゼーション中に呼び出すことはできません。代わりに Awake または Start から呼び出してください。)

System.Serializable でマークされたクラスのコンストラクター内から GameObject.Find などのスクリプティング API を呼び出すと、このエラーが発生します。

これを修正するには、シリアル化されたオブジェクトに対して、スクリプティング API の呼び出しが、コンストラクターで一切行われないようにコードを変更します。

スレッドセーフな Unity スクリプティング API

上の制限は、スクリプティング API の大部分に影響します。ただし、Unity のスクリプティング API の一部だけは例外で、どこからでも呼び出すことができます。

シリアライズ時のエラーのリスクを減らすため、代替手段がない場合を除き、Unity自体のデータを取得設定する必要がな中のエラー発生のリスクを減らすために、自己完結型で、Unity からデータを取得または設定する必要がない API メソッドのみを呼び出してください。他に選択肢がない場合にのみこれらを呼び出すようにしてください。い自己完結型のAPIメソッドのみを呼び出すようにしてください。

シリアライゼーションの効率的な使用法

Unity のシリアライゼーションを最適な方法で使用するために、データを整理します。

  • Unity に最小限のデータセットをシリアル化させます。 これの主な目的は、コンピューターのハードドライブ上の領域を節約するためではなく、以前のバージョンのプロジェクトとの下位互換性を確実に維持するためです。 シリアライズされたデータの大きなセットを扱う場合、下位互換性は開発の後になるほど難しくなります。
  • Unity に複製されたデータやキャッシュされたデータをシリアル化させないようにします。 これは、下位互換性にとって重大な問題が起きる原因になるからです。データが同期から抜け出せなくなるため、エラーが発生する可能性が高くなります。
  • 他のクラスを参照するネスト状の再帰的な構造を避けるようにします。シリアル化された構造のレイアウトは常に同じである必要があります。 つまり、データとは独立し、スクリプト内に公開されているものにのみ依存します。他のクラスを参照する唯一の方法は、UnityEngine.Object から派生するクラスを使用することです。これらのクラスは完全に分かれていて、互いを参照するだけで、コンテンツを埋め込むことはありません。

Serialization

起動時エディタースクリプト実行
スクリプトのコンパイル