UnityシーンのヒエラルキーをYAMLから復元する

#この記事の内容
Unityの環境: 2019.4.12
Unityのシーンファイル（.unity）はYAMLで記述されています。そこで、.unityファイルを直接読み込むことでヒエラルキーを復元します。
ただし、プレハブを含んだものはかなり大変なので、プレハブをヒエラルキーに含まないものとします。
（シーンのファイルだけでヒエラルキーが完結するもの。）
例えば、以下の簡単なシーン(SampleScene.unity)の場合、
![2020-10-19_20h59_03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/313474/6067412a-ec11-8131-d3e9-173e87d5c288.png)
![2020-10-19_20h59_30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/313474/c1bf1cba-5a7f-0c11-67fe-5620dbcee4a5.png)
↓こんな感じになります。
![2020-10-19_21h00_44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/313474/21d977be-5d87-34f1-3cf3-32ad859fec90.png)

Unityの内部の処理が分かった気になれると思います。
結果だけ欲しい人は、コード名が記載されているコードだけ見て他はスキップしてください。
（途中の説明で使うコード部分はコード名を記載してません。）
#UnityでのYAML
##準備
まずYAMLを読む準備をします。UnityでのYAMLはYamlDotNet for Unityアセットを用います。
ただし、自分が入れたときはアセットのnamespaceが認識されませんでした。
Plugins/YamlDotNet/YamlDotNet.asmdefのautoReferencedをtrueにして再読み込みするとうまくいきました。
（プラグインのレビューのDrewProTagさんのコメントのおかげで解決した）
![2020-10-19_21h10_51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/313474/c74ffecb-14b3-0e1e-9f5c-48aafca82905.png)
##YAMLの構文
Unityで使われているYAMLの構文を簡単におさらいします。（偉そうにいっているが今回初めて勉強した。）
まず、上で例に挙げたSampleScene.unityの最初の方を最初の方だけお見せします。

``` YAML
%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
RenderSettings:
//省略
--- !u!157 &3
LightmapSettings:
  m_ObjectHideFlags: 0
  serializedVersion: 11
  m_GIWorkflowMode: 1
  m_GISettings:
    serializedVersion: 2
    m_BounceScale: 1
    m_IndirectOutputScale: 1
    m_AlbedoBoost: 1
    m_EnvironmentLightingMode: 0
    m_EnableBakedLightmaps: 1
    m_EnableRealtimeLightmaps: 0
  m_LightmapEditorSettings:
    serializedVersion: 12
    m_Resolution: 2
    m_BakeResolution: 40
    m_AtlasSize: 2048
    m_AO: 0
    m_AOMaxDistance: 1
    m_CompAOExponent: 1
    m_CompAOExponentDirect: 0
    m_ExtractAmbientOcclusion: 0
    m_Padding: 2
    m_LightmapParameters: {fileID: 0}
    m_LightmapsBakeMode: 1
    m_TextureCompression: 1
    m_FinalGather: 0
    m_FinalGatherFiltering: 1
    m_FinalGatherRayCount: 256
    m_ReflectionCompression: 2
    m_MixedBakeMode: 2
    m_BakeBackend: 1
    m_PVRSampling: 1
    m_PVRDirectSampleCount: 32
    m_PVRSampleCount: 500
    m_PVRBounces: 2
    m_PVREnvironmentSampleCount: 500
    m_PVREnvironmentReferencePointCount: 2048
    m_PVRFilteringMode: 2
    m_PVRDenoiserTypeDirect: 0
    m_PVRDenoiserTypeIndirect: 0
    m_PVRDenoiserTypeAO: 0
    m_PVRFilterTypeDirect: 0
    m_PVRFilterTypeIndirect: 0
    m_PVRFilterTypeAO: 0
    m_PVREnvironmentMIS: 0
    m_PVRCulling: 1
    m_PVRFilteringGaussRadiusDirect: 1
    m_PVRFilteringGaussRadiusIndirect: 5
    m_PVRFilteringGaussRadiusAO: 2
    m_PVRFilteringAtrousPositionSigmaDirect: 0.5
    m_PVRFilteringAtrousPositionSigmaIndirect: 2
    m_PVRFilteringAtrousPositionSigmaAO: 1
    m_ExportTrainingData: 0
    m_TrainingDataDestination: TrainingData
    m_LightProbeSampleCountMultiplier: 4
  m_LightingDataAsset: {fileID: 112000000, guid: 4f5eb19331614e343b2ca2383492174c,
    type: 2}
  m_UseShadowmask: 1
--- !u!196 &4
NavMeshSettings:
//省略
--- !u!1 &495889487
GameObject:
  m_ObjectHideFlags: 0
  m_CorrespondingSourceObject: {fileID: 0}
  m_PrefabInstance: {fileID: 0}
  m_PrefabAsset: {fileID: 0}
  serializedVersion: 6
  m_Component:
  - component: {fileID: 495889491}
  - component: {fileID: 495889490}
  - component: {fileID: 495889489}
  - component: {fileID: 495889488}
  m_Layer: 0
  m_Name: Capsule
  m_TagString: Untagged
  m_Icon: {fileID: 0}
  m_NavMeshLayer: 0
  m_StaticEditorFlags: 0
  m_IsActive: 1
--- !u!136 &495889488
CapsuleCollider:
  m_ObjectHideFlags: 0
  m_CorrespondingSourceObject: {fileID: 0}
  m_PrefabInstance: {fileID: 0}
  m_PrefabAsset: {fileID: 0}
  m_GameObject: {fileID: 495889487}
  m_Material: {fileID: 0}
  m_IsTrigger: 0
  m_Enabled: 1
  m_Radius: 0.5
  m_Height: 2
  m_Direction: 1
  m_Center: {x: 0, y: 0, z: 0}
--- !u!23 &495889489
MeshRenderer:
//省略
--- !u!23 &495889489
MeshFilter:
//省略
--- !u!4 &495889491
Transform:
  m_ObjectHideFlags: 0
  m_CorrespondingSourceObject: {fileID: 0}
  m_PrefabInstance: {fileID: 0}
  m_PrefabAsset: {fileID: 0}
  m_GameObject: {fileID: 495889487}
  m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
  m_LocalPosition: {x: 1.05792, y: -0.8924775, z: 0.5581703}
  m_LocalScale: {x: 1, y: 1, z: 1}
  m_Children: []
  m_Father: {fileID: 0}
  m_RootOrder: 2
  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
//もっと続く
```

重要な部分を順番に説明します。

まず、一つのYAMLファイルは複数のdocumentを含むことができます。
ハイフン三つ "---" がドキュメントの始まりを表しており、documentごとに取り出して処理をしていきます。

documentを例を用いて説明します。上でお見せしたYAMLの最後のドキュメントは

``` YAML
--- !u!4 &495889491
```

と書かれています。
まず !u!4 の部分はタグを表します。結論を言えば、YAMLの最初のところで

``` YAML
%TAG !u! tag:unity3d.com,2011:
``` 
と書かれており、これは !u! で tag:unity3d.com,2011: を表す、という意味なので、
!u!4 は tag:unity3d.com,2011:4を表します。重要なのは !u! の後の数字で、これはUnityのクラスの種類を表します。
どの数字が何を表すかはUnityの公式ページ([YAML クラス ID リファレンス](https://docs.unity3d.com/ja/2018.4/Manual/ClassIDReference.html))に書かれています。
4はTransformを表しており。後で使うGameObjectは1で表されます。

次に !u!4 の後の &495889491 の数字はFileIDを表しています。
FileIDとは一つのアセット内での参照で使用されるドキュメントのIDのことです。
GUIDは聞いたことがあるかもしれませんが、GUIDはアセット自体の参照に使うIDです。
なので、一つのファイル内ではFileIDでdocumentを一意に決めることができますが、他のアセットのドキュメントを参照したいときは、アセットを表すGUIDとそのアセットないでのFileIDを指定する必要があります。
シーンにプレハブが含まれている場合には、（当たり前ですが）シーンの外にあるプレハブを使うので、プレハブGUIDを使って何を参照しているかたどる必要があります。
今回はGUIDを使わずFileIDだけで済むシーンを扱います。

さて、YAMLでは、連想配列つまりdictionaryをコロン:で表します。例えば、

``` YAML
A: 0
B: 1
```
でkey Aに対してvalueが0、key Bに対して value が1であることを表します。一行で連想配列を表すときは、波括弧{}で表します。
例えば、

``` YAML
{A: 0, B: 0, C: 0}
```
のように書きます。次にリスト（普通の配列）はハイフン-で表します。

``` YAML
- 0
- 1
``` 

一行で書く場合は角括弧[]を用いて、

``` YAML
[0, 1]
```
のようにかきます。

以上でUnityのYAMLを読む上で必要な知識はだいたい説明できました。
最後のdocumentの全体をもう一度見てみましょう。

``` YAML
--- !u!4 &495889491
Transform:
  m_ObjectHideFlags: 0
  m_CorrespondingSourceObject: {fileID: 0}
  m_PrefabInstance: {fileID: 0}
  m_PrefabAsset: {fileID: 0}
  m_GameObject: {fileID: 495889487}
  m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
  m_LocalPosition: {x: 1.05792, y: -0.8924775, z: 0.5581703}
  m_LocalScale: {x: 1, y: 1, z: 1}
  m_Children: []
  m_Father: {fileID: 0}
  m_RootOrder: 2
  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
```

まず、タグが4なのでこのdocumentはTransformを表していることが分かります。
documentの連想配列としてkeyがTransformだけなので、これを用いて中身のデータが取れます。（タグでTransformであることは分かるのですが冗長にできています。）

よって、Transform -> m_LocalPosition->x とkeyをたどることで、xの局所座標1.05792を得ることができます。
m_ChildrenはTransformの子Transformを表すのですが、角括弧なのでこれだけリストで格納されていることに注意してください。

# YamlDotNet for Unity でデータを取り出す

以下の記事を参考にしています。
[【Unity】YAMLファイルの読み込みと表示用メッセージの管理](https://gomafrontier.com/unity/670)

まず、シーンのパスScenePathを取得しておいて、それを使ってdocumentの配列を取得します。

``` C#
    StreamReader inputFile = new StreamReader(ScenePath, System.Text.Encoding.UTF8);
    var yamlStream = new YamlStream();
    yamlStream.Load(inputFile);
    IList<YamlDocument> documents = yamlStream.Documents;
``` 

documentのうち今回はGameObjectとTransformのみが興味あるので、以下の連想配列を用意しました。

``` C#
    static Dictionary<string, string> unityClassDic = new Dictionary<string, string>() {
        {"1", "GameObject"},
        {"4", "Transform"}
    };
```

documentからタグとFileIDを取得するために便利staticクラスを用意しました。

``` C#
public static class YamlSupport
{
    public static string ClassTag(YamlDocument document)
    {
        return document.RootNode.Tag.Split(':').Last();
    }

    public static string FileID(YamlDocument document)
    {
        return document.RootNode.Anchor;
    }
}
```

これを用いてすべてのdocumentを読んでいきます。

``` C#
string documentClass;
//ドキュメントごとにロードしていく
foreach (var document in documents)
{
//興味があるクラスでなければスキップ
     if (!unityClassDic.TryGetValue(YamlSupport.ClassTag(document), out documentClass))
         continue;

     var fileID = YamlSupport.FileID(document);

     if (documentClass == "GameObject")
     {
             //GameObjectに対する処理
     }
     else if (documentClass == "Transform")
     {
            //Transformに対する処理
     }
}
``` 

Unityは基本的に連想配列でデータが取れるので、keyとdocumentを渡してvalue（のstring）を返す関数を使いまくります。
ただし、Transformの子Transformを保存したm_Childrenだけリストを使っているので、そのための関数も用意します。

``` C#
    public static string GetValue(string key, YamlDocument document)
    {
        string[] keys = key.Split('.');

        int keyCount = keys.Length;

        YamlMappingNode mapping = (YamlMappingNode)document.RootNode;

        for (int i = 0; i < keyCount; i++)
        {
            YamlScalarNode currentNode = new YamlScalarNode(keys[i]);
            var outNode = mapping.Children[currentNode];

            if (i == keyCount - 1)
            {
                return (outNode as YamlScalarNode).ToString();
            }
            else
            {
                mapping = (YamlMappingNode)outNode;
            }
        }

        return "Error";
    }


    //リストにデータが一つ入っているものを取り出す
    //Transform:
    //    m_Children:
    //    - {fileID: 1689986398}
    //    - {fileID: 789851047}
    //の場合、GetList<"Transform.m_Children", "fileID", document)とする
    public static List<string> GetList(string key, string lastKey, YamlDocument document)
    {
        // キーをドットで分割
        string[] keys = key.Split('.');

        // キーの配列数(＝ネストレベル)取得
        int keyCount = keys.Length;

        YamlMappingNode mapping = (YamlMappingNode)document.RootNode;
        YamlNode node = null;

        List<string> rtnList = new List<string>();


        for (int i = 0; i < keyCount; i++)
        {
            YamlScalarNode currentNode = new YamlScalarNode(keys[i]);
            var outNode = mapping.Children[currentNode];

            if (i == keyCount - 1)
            {
                node = outNode;
            }
            else
            {
                mapping = (YamlMappingNode)outNode;
            }
        }

        var sequence = (YamlSequenceNode)node;

        var childrenList = sequence.Children;

        foreach (var child in childrenList)
        {
            mapping = (YamlMappingNode)child;

            YamlScalarNode currentNode = new YamlScalarNode(lastKey);
            var outNode = mapping.Children[currentNode];

            rtnList.Add(((YamlScalarNode)outNode).ToString());
        }


        return rtnList;
    }
```

以上をまとめて、YAMLからSceneのデータを読み込んで保存する以下のクラスを作成しました。

``` C#:YamlScene.cs
using System.Collections;
using System.Collections.Generic;
using System.Linq;
//using UnityEngine;
using System.IO;
using YamlDotNet.Core;
using YamlDotNet.RepresentationModel;

public class YamlScene
{
    public string SceneName { get; }
    public string ScenePath { get; }

    //今回は使わない
    string GUID { get; }

    //YamlDotNet for Unity を使用して初期化する
    public YamlScene(string scenePath)
    {
        ScenePath = scenePath;
        SceneName = scenePath.Split('/').Last();

        Load();
    }

    static Dictionary<string, string> unityClassDic = new Dictionary<string, string>() {
        {"1", "GameObject"},
        {"4", "Transform"}
    };


    //シーン中のデータ
    public List<YamlGameObject> GameObjects { get; } = new List<YamlGameObject>();

    public List<YamlTransform> Transforms { get; } = new List<YamlTransform>();

    //シーン直下のTransform。親を持たないもの。このシーンの子供と考える
    public List<YamlTransform> childTransforms = new List<YamlTransform>();

    private void Load()
    {
        StreamReader inputFile = new StreamReader(ScenePath, System.Text.Encoding.UTF8);
        var yamlStream = new YamlStream();
        yamlStream.Load(inputFile);

        IList<YamlDocument> documents = yamlStream.Documents;


        GameObjects.Clear();
        Transforms.Clear();


        string documentClass;
        //ドキュメントごとにロードしていく
        foreach (var document in documents)
        {
            //興味があるクラスでなければスキップ
            if (!unityClassDic.TryGetValue(YamlSupport.ClassTag(document), out documentClass))
                continue;

            var fileID = YamlSupport.FileID(document);

            if (documentClass == "GameObject")
            {
                string key = "GameObject.m_Name";
                string objName = YamlSupport.GetValue(key, document);
                GameObjects.Add(new YamlGameObject(objName, fileID));
            }
            else if (documentClass == "Transform")
            {
                string key = "Transform.m_GameObject.fileID";
                string objID = YamlSupport.GetValue(key, document);

                key = "Transform.m_Children";
                var children = YamlSupport.GetList(key, "fileID", document);


                key = "Transform.m_RootOrder";
                string rootOrder = YamlSupport.GetValue(key, document);

                key = "Transform.m_Father.fileID";
                string father = YamlSupport.GetValue(key, document);


                Transforms.Add(new YamlTransform(fileID, objID, rootOrder, children, father));
            }
        }

        //IDを使って参照をセット
        //Transformに参照を入れていく
        foreach (var transform in Transforms)
        {
            //IDが合うオブジェクトを探す
            foreach (var obj in GameObjects)
            {
                if (obj.FileID == transform.GameObjectID)
                {
                    transform.yamlGameObject = obj;
                    break;
                }
            }

            //Transformの子供をセットする
            foreach (var child in transform.ChildrenID)
            {
                foreach (var other in Transforms)
                {
                    if (child == other.FileID)
                    {
                        transform.children.Add(other);
                    }
                }
            }

            //親がないものはシーンの子供に入れる
            if (transform.FatherID == "0")
            {
                childTransforms.Add(transform);
            }
        }

        //並び替え
        childTransforms.Sort((a, b) => int.Parse(a.RootOrder) - int.Parse(b.RootOrder));
        foreach (var transform in Transforms)
        {
            transform.children.Sort((a, b) => int.Parse(a.RootOrder) - int.Parse(b.RootOrder));
        }
    }

}

public class YamlGameObject
{ 
    public string FileID { get; set; }
    public string ObjectName { get; set; }
    public YamlGameObject(string _name, string _id)
    {
        ObjectName = _name;
        FileID = _id;
    }

}

public class YamlTransform
{
    public string FileID { get; set; }

    public string RootOrder { get; set; }
    
    public string FatherID { get; set; }

    public string GameObjectID { get; set; }
    public List<string> ChildrenID { get; set; }

    public YamlTransform(string fileID, string gameObjectID, string rootOrder, List<string> childrenID, string fatherID)
    {
        FileID = fileID;

        GameObjectID = gameObjectID;

        RootOrder = rootOrder;

        ChildrenID = childrenID;

        FatherID = fatherID;
    }

    //ロードが終わってからクラスへの参照を計算する
    public YamlGameObject yamlGameObject = null;
    public List<YamlTransform> children = new List<YamlTransform>();

}


public static class YamlSupport
{
    public static string FileID(YamlDocument document)
    {
        return document.RootNode.Anchor;
    }

    public static string ClassTag(YamlDocument document)
    {
        return document.RootNode.Tag.Split(':').Last();
    }

    public static string GetValue(string key, YamlDocument document)
    {
        string[] keys = key.Split('.');

        int keyCount = keys.Length;

        YamlMappingNode mapping = (YamlMappingNode)document.RootNode;

        for (int i = 0; i < keyCount; i++)
        {
            YamlScalarNode currentNode = new YamlScalarNode(keys[i]);
            var outNode = mapping.Children[currentNode];

            if (i == keyCount - 1)
            {
                return (outNode as YamlScalarNode).ToString();
            }
            else
            {
                mapping = (YamlMappingNode)outNode;
            }
        }

        return "Error";
    }


    //リストにデータが一つ入っているものを取り出す
    //Transform:
    //    m_Children:
    //    - {fileID: 1689986398}
    //    - {fileID: 789851047}
    //の場合、GetList<"Transform.m_Children", "fileID", document)とする
    public static List<string> GetList(string key, string lastKey, YamlDocument document)
    {
        // キーをドットで分割
        string[] keys = key.Split('.');

        // キーの配列数(＝ネストレベル)取得
        int keyCount = keys.Length;

        YamlMappingNode mapping = (YamlMappingNode)document.RootNode;
        YamlNode node = null;

        List<string> rtnList = new List<string>();


        for (int i = 0; i < keyCount; i++)
        {
            YamlScalarNode currentNode = new YamlScalarNode(keys[i]);
            var outNode = mapping.Children[currentNode];

            if (i == keyCount - 1)
            {
                node = outNode;
            }
            else
            {
                mapping = (YamlMappingNode)outNode;
            }
        }

        var sequence = (YamlSequenceNode)node;

        var childrenList = sequence.Children;

        foreach (var child in childrenList)
        {
            mapping = (YamlMappingNode)child;

            YamlScalarNode currentNode = new YamlScalarNode(lastKey);
            var outNode = mapping.Children[currentNode];

            rtnList.Add(((YamlScalarNode)outNode).ToString());
        }


        return rtnList;
    }
}
```

#シーンのヒエラルキーを表示する
作ったクラスを使って、ヒエラルキーを構成し表示するEditorWindowを作りました。
簡単な確認用なので、初期化を適当にやってます。Windowを開いたままエディタを閉じたとき、開きなおすとエラーが出たりするので、実際に使う場合は作りなおしてください。

``` C#:SceneView.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using UnityEngine.SceneManagement;

public class SceneViewer : EditorWindow
{
    static bool isOpen = true;
    static Dictionary<YamlTransform, bool> openDictionary;

    static YamlScene yamlScene;

    [MenuItem("MyEditor/SceneView")]
    static void Open()
    {
        yamlScene = new YamlScene(SceneManager.GetActiveScene().path);

        openDictionary = new Dictionary<YamlTransform, bool>();
        foreach (var yTransform in yamlScene.Transforms)
        {
            openDictionary.Add(yTransform, true);
        }

        GetWindow<SceneViewer>();
    }

    private void OnGUI()
    {
        isOpen = EditorGUILayout.Foldout(isOpen, yamlScene.SceneName);
        if (isOpen)
        {
            using (new EditorGUI.IndentLevelScope())
            {
                foreach (var transform in yamlScene.childTransforms)
                {
                    RecursiveGUI(transform);
                }
            }

        }
    }

    static void RecursiveGUI(YamlTransform yTransform)
    {
        bool isOpen = openDictionary[yTransform];

        if (yTransform.children.Count >= 1)
        {
            using (new GUILayout.VerticalScope())
            {
                openDictionary[yTransform] = EditorGUILayout.Foldout(isOpen, yTransform.yamlGameObject.ObjectName);
                if (isOpen)
                {
                    using (new GUILayout.HorizontalScope())
                    {
                        GUILayout.Space(20f); // horizontal indent size of 20 (pixels)

                        using (new GUILayout.VerticalScope())
                        {
                            foreach (var child in yTransform.children)
                            {
                                RecursiveGUI(child);
                            }

                        }
                    }

                }
            }
        }
        else
        {
            using (new GUILayout.HorizontalScope())
            {
                GUILayout.Space(20f);
                using (new GUILayout.VerticalScope())
                {
                    GUILayout.Label(yTransform.yamlGameObject.ObjectName);
                }
            }
        }
    }
}
```

#あとがき
例外処理をやってなかったり、初期化をちゃんとやってないとはいえ、400行もいかずにヒエラルキーが見れるのは分かった気になれてうれしいです。
シーンの右クリックから開くようにすれば、アクティブなシーンを変更せずにヒエラルキーが確認できるようにできます。
ここまでいくと実用的だと思います。

以下に、時間があるときに気になった点を書く予定です。