はなちるのマイノート

Unityをメインとした技術ブログ。自分らしくまったりやっていきたいと思いますー!

【C#, Unity】VYamlを用いてYAML(Unityで利用される特殊な形式を含む)をシリアライズ・デシリアライズする方法

はじめに

今回はVYamlを用いてYAML(Unityで利用される特殊な形式を含む)をシリアライズ・デシリアライズする方法を紹介したいと思います。

github.com

UnityとYAML

Unityでは.unity.prefab, .assetなどをYAMLにシリアライズして保存しています。以下はPrefabをシリアライズしたYAMLの例です。

%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &2636510911735150372
GameObject:
  m_ObjectHideFlags: 0
  m_CorrespondingSourceObject: {fileID: 0}
  m_PrefabInstance: {fileID: 0}
  m_PrefabAsset: {fileID: 0}
  serializedVersion: 6
  m_Component:
  - component: {fileID: 6157453622845355896}
  m_Layer: 0
  m_Name: SamplePrefab
  m_TagString: Untagged
  m_Icon: {fileID: 0}
  m_NavMeshLayer: 0
  m_StaticEditorFlags: 0
  m_IsActive: 1
--- !u!4 &6157453622845355896
Transform:
  m_ObjectHideFlags: 0
  m_CorrespondingSourceObject: {fileID: 0}
  m_PrefabInstance: {fileID: 0}
  m_PrefabAsset: {fileID: 0}
  m_GameObject: {fileID: 2636510911735150372}
  serializedVersion: 2
  m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
  m_LocalPosition: {x: 0, y: 0, z: 0}
  m_LocalScale: {x: 1, y: 1, z: 1}
  m_ConstrainProportionsScale: 0
  m_Children: []
  m_Father: {fileID: 0}
  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}

Unity のシリアライズ言語 YAML を理解する

VYamlとは

hadashiAさんが作成したパフォーマンス良くUnityフレンドリーなYAMLのパーサーです。

www.nuget.org

- 新世代の .NET のAPIを使ってスクラッチで実装した高速なYAMLパーサ。
- YAML 1.2仕様をほぼ完全に網羅。(ちなみに調べてみたところ、100%仕様に準拠しているYAMLパーサがこの世に存在するか不明)
- デファクト実装の YamlDotNet との比較では、
    - デシリアライズが6倍以上高速
    - パース処理中のGCゴミがゼロなため、ヒープアロケーションの量は数十倍の差がつく。
- Unity 2021.3 以降に対応。特にUnity上での性能を重視している。

VYaml - hadashiA

またMemoryPackのようにSourceGeneratorを利用してシリアライズ・デシリアライズの処理を自動生成していたりと、かなりナウい実装がされていて、開発者の視点としても非常に興味深いです。

インストール方法

NuGetから取得します。

Rider上でNuGetパッケージからインストールした様子
$ dotnet add package VYaml

Unityの場合

Unityの場合はUPM(Unity Package Manager)を通してインストールします。Package Managerを開き、add package from git URL...から以下を入力します。

https://github.com/hadashiA/VYaml.git?path=VYaml.Unity/Assets/VYaml#0.27.1
Add package from git URL

またUnity 2021.3 ~ Unity 2022.2のバージョンの場合はリリースページからVYaml.2022_1_or_lower.unitypackageをインストールしてインポートする必要があるそうです。注意してください。

補足するとVYamlSourceGeneratorを利用しているのですが、Unityが利用しているMicrosoft.CodeAnalysis.CSharpのバージョンがUnity2022.2からv4.1.0になってるのが原因かと思います。

基本的な使い方

まずはstructclassに対してシリアライズ・デシリアライズしてみましょう。

// YamlObjectをstruct or classに付与すると、SourceGeneratorによりコード生成が走る
// partialは必須
[YamlObject]
public partial class SampleStruct
{
    // publicなFieldはOK
    public string A;
    
    // publicなPropertyはOK
    public string B { get; set; }
    
    // publicなProperty (private・initなsetter)はOK
    public string C { get; private set; }
    public string D { get; init; }
    
    // setterがないPropertyにはYamlIgnoreを付与しないとSourceGeneratorが動作しないので注意
    [YamlIgnore]
    public string E { get; }
}
public class Program
{
    public static void Main(string[] args)
    {
        SampleStruct target = new SampleStruct()
        {
            A = "This is A",
            B = "This is B",
            D = "This is D"
        };

        // ----------------
        // -- シリアライズ --
        // ----------------
        // Utf16にする必要がなければUtf8で扱っていく方がパフォーマンスが良い
        ReadOnlyMemory<byte> yamlUtf8 = YamlSerializer.Serialize(target);

        // Utf16にする場合はSerializeToString
        string yamlUtf16 = YamlSerializer.SerializeToString(target);

        // a: This is A
        // b: This is B
        // c: null
        // d: This is D
        Console.WriteLine(yamlUtf16);
        
        // ------------------
        // -- デシリアライズ --
        // ------------------
        SampleStruct deserializedObj = YamlSerializer.Deserialize<SampleStruct>(yamlUtf8);
        
        // This is A
        // This is B
        // 
        // This is D
        Console.WriteLine(deserializedObj.A);
        Console.WriteLine(deserializedObj.B);
        Console.WriteLine(deserializedObj.C);
        Console.WriteLine(deserializedObj.D);
    }
}

命名規則

細かい仕様はReadMeを見て欲しいですが、命名規則周りは最低限知っておかないと厳しいので紹介しておきます。

まず前提として、SourceGeneratorにより生成されたコードを見ることでどのようなYAMLのキーになるかを調べられることができます。Riderであれば定義に移動かソリューションビューを見てください。

// 生成されたコードのSerialize処理だけ抜粋
// それぞれYAMLキーが "a", "b", "c", "d"であることが確認できる
[VYaml.Annotations.Preserve]
public void Serialize(ref Utf8YamlEmitter emitter, global::SampleStruct? value, YamlSerializationContext context)
{
    if (value is null)
    {
        emitter.WriteNull();
        return;
    }

    emitter.BeginMapping();
    emitter.WriteString("a", ScalarStyle.Plain);
    context.Serialize(ref emitter, value.A);
    emitter.WriteString("b", ScalarStyle.Plain);
    context.Serialize(ref emitter, value.B);
    emitter.WriteString("c", ScalarStyle.Plain);
    context.Serialize(ref emitter, value.C);
    emitter.WriteString("d", ScalarStyle.Plain);
    context.Serialize(ref emitter, value.D);
    emitter.EndMapping();
}

デフォルトではプロパティ名が小文字のキャメルケースになっています。これをカスタマイズしたい場合は[YamlObject(NamingConvention.SnakeCase)]のように引数を与えます。

namespace VYaml.Annotations
{
  public enum NamingConvention
  {
    LowerCamelCase,
    UpperCamelCase,
    SnakeCase,
    KebabCase,
  }
}

またメンバーのキー名を個別に変えたい時は、[YamlMember("custom-key")]のように指定します。

[YamlObject(NamingConvention.SnakeCase)]
public partial class SampleStruct
{
    public string SampleProperty { get; init; }
    
    [YamlMember("this-is-custom-property")]
    public string CustomProperty { get; init; }
}
// 生成されたコードのSerializeだけ抜粋
[VYaml.Annotations.Preserve]
public void Serialize(ref Utf8YamlEmitter emitter, global::SampleStruct? value, YamlSerializationContext context)
{
    if (value is null)
    {
        emitter.WriteNull();
        return;
    }

    emitter.BeginMapping();
    emitter.WriteString("sample_property", ScalarStyle.Plain);
    context.Serialize(ref emitter, value.SampleProperty);
    emitter.WriteString("this-is-custom-property");
    context.Serialize(ref emitter, value.CustomProperty);
    emitter.EndMapping();
}

あらかじめSchemaの決まっていないYAMLを読み取る

上記手法はあらかじめSchemaが決まっていれば有効ですが、VYamlはdynamicにデシリアライズすることも可能です。

public class Program
{
    public static void Main(string[] args)
    {
        SampleStruct target = new SampleStruct()
        {
            Array = ["A1", "A2", "A3"],
            List = ["B1", "B2", "B3"],
            Dic = new Dictionary<string, string>
            {
                { "Key1", "Value1" },
                { "Key2", "Value2" }
            }
        };

        // ----------------
        // -- シリアライズ --
        // ----------------
        
        // array: 
        // - A1
        // - A2
        // - A3
        // list: 
        // - B1
        // - B2
        // - B3
        // dic: 
        //   Key1: Value1
        //   Key2: Value2
        ReadOnlyMemory<byte> yamlUtf8 = YamlSerializer.Serialize(target);
        
        // ------------------
        // -- デシリアライズ --
        // ------------------
        dynamic deserializedObj = YamlSerializer.Deserialize<dynamic>(yamlUtf8);
        
        // A1
        Console.WriteLine(deserializedObj["array"][0]);
        
        // B1, B2, B3
        Console.WriteLine(string.Join(", ", deserializedObj["list"]));
        
        // Value1
        Console.WriteLine(deserializedObj["dic"]["Key1"]);
    }
}

[YamlObject]
public partial class SampleStruct
{
    public string[] Array { get; init; }
    
    public List<string> List { get; init; }
    
    public Dictionary<string, string> Dic { get; init; }
}

長々と書いてしまいましたが、型引数をdynamicにしてあげればOKです。

dynamic deserializedObj = YamlSerializer.Deserialize<dynamic>(yamlUtf8);
        
// A1
Console.WriteLine(deserializedObj["array"][0]);

// B1, B2, B3
Console.WriteLine(string.Join(", ", deserializedObj["list"]));

// Value1
Console.WriteLine(deserializedObj["dic"]["Key1"]);

Unityでprefabを読み取る

UnityからシリアライズされるYAMLは基本的にdynamicで読み込んでいくのが基本になるでしょう。

public class Sample
{
    [MenuItem("Sample/Execute")]
    public static async Task Execute()
    {
        // SamplePrefabという名前のPrefabを探し、パスを取得
        var prefabPath = AssetDatabase.GUIDToAssetPath(AssetDatabase.FindAssets("SamplePrefab").First());

        // dynamicにYAMLをデシリアライズ
        var prefabYaml = await YamlSerializer.DeserializeAsync<dynamic>(File.OpenRead(prefabPath));

        // Prefabの名前を取得する
        // Name : SamplePrefab
        Debug.Log($"Name : {prefabYaml["GameObject"]["m_Name"]}");
    }
}