はじめに
今回はVYaml
を用いてYAML
(Unityで利用される特殊な形式を含む)をシリアライズ・デシリアライズする方法を紹介したいと思います。
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}
VYamlとは
hadashiA
さんが作成したパフォーマンス良くUnityフレンドリーなYAML
のパーサーです。
- 新世代の .NET のAPIを使ってスクラッチで実装した高速なYAMLパーサ。 - YAML 1.2仕様をほぼ完全に網羅。(ちなみに調べてみたところ、100%仕様に準拠しているYAMLパーサがこの世に存在するか不明) - デファクト実装の YamlDotNet との比較では、 - デシリアライズが6倍以上高速 - パース処理中のGCゴミがゼロなため、ヒープアロケーションの量は数十倍の差がつく。 - Unity 2021.3 以降に対応。特にUnity上での性能を重視している。
またMemoryPack
のようにSourceGenerator
を利用してシリアライズ・デシリアライズの処理を自動生成していたりと、かなりナウい実装がされていて、開発者の視点としても非常に興味深いです。
インストール方法
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
またUnity 2021.3 ~ Unity 2022.2のバージョンの場合はリリースページからVYaml.2022_1_or_lower.unitypackage
をインストールしてインポートする必要があるそうです。注意してください。
補足するとVYaml
はSourceGenerator
を利用しているのですが、Unityが利用しているMicrosoft.CodeAnalysis.CSharp
のバージョンがUnity2022.2
からv4.1.0
になってるのが原因かと思います。
基本的な使い方
まずはstruct
・class
に対してシリアライズ・デシリアライズしてみましょう。
// 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"]}"); } }