【Unity】スクリプトがシリアライズされる仕組み

こじゃらこじゃら

次のようなインスペクターで設定できる値って、どのように保存されてるの?

このはこのは

これらの値は、バイナリデータテキストデータなどに変換されてファイルに保存されるわ。
この変換処理のことをシリアライズ(またはシリアル化)と言うの。

MonoBehaviourスクリプトで定義したフィールドをインスペクターから設定できるようにしたい場合、次のようなコードを書くことがあるでしょう。

class ExampleScript : MonoBehaviour
{
    // publicにしたフィールド
    public float _publicField;

    // SerializeField属性を指定したprivateフィールド
    [SerializeField] private float _privateField;

        ・
        ・
        ・

インスペクターから設定した内容は、シリアライズすることでアセットファイル等に保存されます。

シリアライズ対象のフィールドは、上記の書き方のように定義する必要がありますが、必ず設定できるようになる(=シリアライズできる)とは限らず、シリアライズできる条件が存在します。

本記事では、シーンやPrefabなどのインスペクターから設定した値がどのように保存されるかについて解説していきます。

スポンサーリンク

シリアライズとは

シリアライズとは直列化と訳され、複数の要素をある規則に従い一直線にデータを並べることを言います。
一直線に並べたデータはバイト列のバイナリデータだったりJSONのテキストデータだったりとまちまちです。

シリアライズの例

データをシリアライズすると、ファイルやデータベースなど、永続的なデータとして保存することができます。また、端末間のデータ通信なども可能になります。

シリアライズされたデータをプログラムから使用するためには、シリアライズとは逆の操作を行う必要があります。
これをデシリアライズと言います。

デシリアライズの例

Unityにおけるシリアライズ

UnityにおけるシーンファイルやPrefabなどのアセット情報は、すべてシリアライズされてファイルに保存されます。

普段私たちが触るUnityプロジェクトとビルドして生成される実行ファイルとでは、シリアライズされるデータ形式が異なります。

Unityプロジェクトでのシリアライズ形式は、メニューのEdit->Project Settings->Editorの順に選択して表示される画面のAsset Serializationの設定値によって決まります。

Unity2019.3.0では、Force Textが初期設定されます。
これは、アセットをYAML [1] と呼ばれるテキスト形式でシリアライズする設定です。

%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!29 &1
OcclusionCullingSettings:
  m_ObjectHideFlags: 0
  serializedVersion: 2
  m_OcclusionBakeSettings:
    smallestOccluder: 5
    smallestHole: 0.25
    backfaceThreshold: 100
  m_SceneGUID: 00000000000000000000000000000000
  m_OcclusionCullingData: {fileID: 0}
--- !u!104 &2
このはこのは

複数人で開発するときは、テキスト形式でシリアライズした方が色々と都合が良くておススメだわ!

なお、実行ファイルにビルドされたあとは、上記設定に関係なくバイナリ形式にシリアライズされるようです。
バイナリ形式はテキスト形式と比べてシリアライズ/デシリアライズの処理が高速という利点があります。

フィールドがシリアライズされる条件

スクリプトで定義したフィールドがシリアライズされる条件は、公式リファレンスに記載されています。

以下の条件を満たすフィールドがシリアライズされます。

シリアライズされる条件
  • publicまたは[SerializeField]属性が指定されている
  • staticフィールドではない
  • constフィールドではない
  • readonlyフィールドではない
  • シリアライズ可能な型である

具体的にスクリプトで確認してみましょう。

サンプル

シリアライズされる条件を確かめるためのサンプルスクリプトです。される/されないのどちらかはコメントに記載しています。

SerializationTest.cs
using System.Collections.Generic;
using UnityEngine;

public class SerializationTest : MonoBehaviour
{
    //-----------------------------------------------------
    // フィールドがシリアライズされる条件

    // publicなフィールドはOK
    public int publicField;

    // privateなだけのフィールドはNG
    private int privateField;

    // privateでもSerializeField属性があるとOK
    [SerializeField] private int serializeField;

    // constフィールドはNG
    public const int constField = 0;

    // readonlyフィールドもNG
    public readonly int readonlyField;

    // シリアライズ可能な型でなければ、条件を満たしてもNG
    [SerializeField] private NonSerializableClass nonSerializableType;

    //-----------------------------------------------------
    // シリアライズ可能な型の条件
    // Serializable属性の独自クラスはOK
    [System.Serializable] private class SerializableClass { public int field; }
    [SerializeField] private SerializableClass serializableClass;

    // Serializable属性の独自構造体もOK
    [System.Serializable] private struct SerializableStruct { public int field; }
    [SerializeField] private SerializableClass serializableStruct;

    // Serializable属性が無い独自クラスはNG
    private class NonSerializableClass { public int field; }
    [SerializeField] private NonSerializableClass nonSerializableClass;

    // Serializable属性が無い独自構造体もNG
    private class NonSerializableStruct { public int field; }
    [SerializeField] private NonSerializableStruct nonSerializableStruct;

    // UnityEngine.Object派生のオブジェクト参照はOK
    private class UnityObjectSubClass : UnityEngine.Object { public int field; }
    [SerializeField] private UnityObjectSubClass _objectType;

    // プリミティブ型はOK
    [SerializeField] private string primitiveType;

    // Enum型もOK
    private enum EnumType { None }
    [SerializeField] private EnumType enumType;

    // Unity特定のビルトイン型もOK
    [SerializeField] private Vector3 builtInType;

    // 抽象クラスはNG
    [System.Serializable] private abstract class AbstractClass { public int field; }
    [SerializeField] private AbstractClass abstractClass;

    // インタフェースクラスもNG
    private interface IInterface { }
    [SerializeField] private IInterface interfaceClass;

    // シリアライズ可能な型の配列はOK
    [SerializeField] private SerializableClass[] arrayType;

    // シリアライズ可能な型のリストもOK
    [SerializeField] private List<SerializableClass> listType;

    // その他のジェネリック型はNG
    [SerializeField] private Dictionary<int, string> dictionaryType;
}

実行結果

期待通りの結果になりました!

シリアライズ無効とインスペクター非表示の違い

ここで一つ落とし穴があります。

フィールドがインスペクターに表示されないからと言って、必ずしもそれがシリアライズされないという訳ではない点です。

HideInInspector属性をフィールドに指定すると、シリアライズするしないに関係なくインスペクターから非表示になります。

System.NonSerialized属性を指定すると、シリアライズそのものがされなくなります。

サンプル

publicフィールドにHideInInspector属性を指定した場合の挙動確認用サンプルです。

HideInInspectorTest.cs
using UnityEngine;

public class HideInInspectorTest : MonoBehaviour
{
    [HideInInspector] public int field = 123;
}

実行結果

フィールドが一切インスペクターに表示されなくなります。

このとき、シーンファイルを覗いてみると、フィールドデータがシリアライズされていることが分かります。

HideInInspector属性は、シリアライズしたいがインスペクターから直接値を編集して欲しくない場合などに使うと良いでしょう。

また、System.NonSerialized属性を指定すると、シリアライズそのものが無効になります。
この場合もインスペクターには表示されません。

独自のシリアライズ/デシリアライズ処理を実装する

Unityでは、シリアライズ/デシリアライズのタイミングをフックする手段が提供されています。
これを用いて独自のシリアライズ/デシリアライズ処理を行うことが可能です。

これはISerializationCallbackReceiverインタフェースを継承して実現します。

さっそく具体例を見ましょう。

サンプル

CustomSerializationTest.cs
using UnityEngine;

public class CustomSerializationTest : MonoBehaviour, ISerializationCallbackReceiver
{
    [SerializeField] private int intValue;

    [SerializeField] private string strValue;

    // シリアライズ直前
    public void OnBeforeSerialize()
    {
        // 文字列を整数値にしてシリアライズ
        int.TryParse(strValue, out intValue);
    }

    // デシリアライズ直後
    public void OnAfterDeserialize()
    {
        // 数値を文字列に展開
        strValue = intValue.ToString();
    }
}

実行結果

例では、シリアライズするときはint型整数値にしておき、デシリアライズするときに文字列型に展開しています。
試しにInt Valueに値を入力すると、Str Valueに数字の文字列が入ります。

さいごに

インスペクターから設定したスクリプト情報は特定の形式でシリアライズされて保存されることが分かりました。
Unityにはもう一つメタファイルが存在しており、この中にも別のシリアライズされた設定情報が格納されています。

メタファイルの仕組みについては、以下記事をご覧ください。

参考サイト

スポンサーリンク