なんて同僚たちにマウント取られる前にUnityYAMLの勉強をしておこうと思った記事です。
一応調べましたが、やっぱりよくわかりません。
QualiArtsアドカレ 3日目の記事になります。
UnityYAMLとは
UnityYAMLとは、Unityのアセットのシリアライズ形式の一つで、yaml記法のサブセットです。
Asset SerializationにForce Textを指定するとこの形式でprefabなどのアセットが保存されるようになります。
とりあえずForce Textを選択しているものの、特にprefabは巨大なyamlになりがちで、読み方もよくわからないしで、ほとんど向き合ってきませんでした。
ところがしばらく前に、Unity公式から
Unity のシリアライズ言語 YAML を理解する
という記事が公開されました。ありがたいことに日本語翻訳されているのでまだ読んでない方は是非読んでみてください。
Unity のシリアライズ言語 YAML を理解する を読んでみて
上記記事の内容が素晴らしいので、一通り目を通してUnityYAMLを完全に理解した気持ちになれたので、手元のprefabを読んでみました。
CardBoxScreen.prefab(sample)
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &1865014687417162
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 224475894893193696}
- component: {fileID: 222259688764093502}
- component: {fileID: 114916577866252144}
m_Layer: 5
m_Name: Text
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &224475894893193696
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1865014687417162}
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_Children: []
m_Father: {fileID: 2527234515757153141}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 0, y: 215.39001}
m_SizeDelta: {x: 366.41266, y: 203.70001}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &222259688764093502
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1865014687417162}
m_CullTransparentMesh: 0
--- !u!114 &114916577866252144
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1865014687417162}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_FontData:
m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0}
m_FontSize: 50
m_FontStyle: 0
m_BestFit: 0
m_MinSize: 0
m_MaxSize: 69
m_Alignment: 4
m_AlignByGeometry: 0
m_RichText: 1
m_HorizontalOverflow: 1
m_VerticalOverflow: 1
m_LineSpacing: 1
m_Text: "\x04\u30AB\u30FC\u30C9\u4E00\u89A70"
--- !u!114 &2281633851196779435
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1794220058932668345}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 54274acba9bd3450992ce3a66d0e7af2, type: 3}
m_Name:
m_EditorClassIdentifier:
_root: {fileID: 3368266533159312908}
--- !u!114 &5223233202539212553
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1794220058932668345}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: c3ddc448688d94df2b8f9019109edfc6, type: 3}
m_Name:
m_EditorClassIdentifier:
_view: {fileID: 2281633851196779435}
_screenCommonRoot: {fileID: 6423277813639145140}
_cardListPresenter: {fileID: 4475072414863852767}
--- !u!1001 &1793172451492897065
PrefabInstance:
m_ObjectHideFlags: 0
serializedVersion: 2
m_Modification:
m_TransformParent: {fileID: 0}
m_Modifications:
- target: {fileID: 1400207620163216, guid: 094164b28ecc049328ef3063313e741b, type: 3}
propertyPath: m_Name
value: CardBoxScreen
objectReference: {fileID: 0}
- target: {fileID: 2148239045628858729, guid: 094164b28ecc049328ef3063313e741b,
type: 3}
propertyPath: m_RootOrder
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2148239045628858729, guid: 094164b28ecc049328ef3063313e741b,
type: 3}
propertyPath: m_LocalPosition.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2148239045628858729, guid: 094164b28ecc049328ef3063313e741b,
type: 3}
propertyPath: m_LocalPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2148239045628858729, guid: 094164b28ecc049328ef3063313e741b,
type: 3}
propertyPath: m_LocalPosition.z
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2148239045628858729, guid: 094164b28ecc049328ef3063313e741b,
type: 3}
propertyPath: m_LocalRotation.w
value: 1
objectReference: {fileID: 0}
- target: {fileID: 2148239045628858729, guid: 094164b28ecc049328ef3063313e741b,
type: 3}
propertyPath: m_LocalRotation.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2148239045628858729, guid: 094164b28ecc049328ef3063313e741b,
type: 3}
propertyPath: m_LocalRotation.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2148239045628858729, guid: 094164b28ecc049328ef3063313e741b,
type: 3}
propertyPath: m_LocalRotation.z
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2148239045628858729, guid: 094164b28ecc049328ef3063313e741b,
type: 3}
propertyPath: m_LocalEulerAnglesHint.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2148239045628858729, guid: 094164b28ecc049328ef3063313e741b,
type: 3}
propertyPath: m_LocalEulerAnglesHint.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2148239045628858729, guid: 094164b28ecc049328ef3063313e741b,
type: 3}
propertyPath: m_LocalEulerAnglesHint.z
value: 0
objectReference: {fileID: 0}
m_RemovedComponents: []
m_SourcePrefab: {fileID: 100100000, guid: 094164b28ecc049328ef3063313e741b, type: 3}
--- !u!1 &1794220058932668345 stripped
GameObject:
m_CorrespondingSourceObject: {fileID: 1400207620163216, guid: 094164b28ecc049328ef3063313e741b,
type: 3}
m_PrefabInstance: {fileID: 1793172451492897065}
m_PrefabAsset: {fileID: 0}
--- !u!114 &6423277813639145140 stripped
MonoBehaviour:
m_CorrespondingSourceObject: {fileID: 4739671352265131933, guid: 094164b28ecc049328ef3063313e741b,
type: 3}
m_PrefabInstance: {fileID: 1793172451492897065}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1794220058932668345}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: add964d6de6884f75a1f30247ae54694, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!224 &2527234515757153141 stripped
RectTransform:
m_CorrespondingSourceObject: {fileID: 4318999042065406556, guid: 094164b28ecc049328ef3063313e741b,
type: 3}
m_PrefabInstance: {fileID: 1793172451492897065}
m_PrefabAsset: {fileID: 0}
--- !u!225 &3368266533159312908 stripped
CanvasGroup:
m_CorrespondingSourceObject: {fileID: 3917247981887962917, guid: 094164b28ecc049328ef3063313e741b,
type: 3}
m_PrefabInstance: {fileID: 1793172451492897065}
m_PrefabAsset: {fileID: 0}
--- !u!1001 &2728508175332412409
PrefabInstance:
m_ObjectHideFlags: 0
serializedVersion: 2
m_Modification:
m_TransformParent: {fileID: 2527234515757153141}
m_Modifications:
- target: {fileID: 3127823570204878437, guid: 631e66137c7824aa79f18da746121ef2,
type: 3}
propertyPath: m_AnchorMax.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 3127823570204878437, guid: 631e66137c7824aa79f18da746121ef2,
type: 3}
propertyPath: m_AnchorMax.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 3127823570204878437, guid: 631e66137c7824aa79f18da746121ef2,
type: 3}
propertyPath: m_AnchorMin.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 4720499058283198052, guid: 631e66137c7824aa79f18da746121ef2,
type: 3}
propertyPath: m_Pivot.x
value: 0.5
objectReference: {fileID: 0}
- target: {fileID: 4720499058283198052, guid: 631e66137c7824aa79f18da746121ef2,
type: 3}
propertyPath: m_Pivot.y
value: 0.5
objectReference: {fileID: 0}
- target: {fileID: 4720499058283198052, guid: 631e66137c7824aa79f18da746121ef2,
type: 3}
propertyPath: m_RootOrder
value: 1
objectReference: {fileID: 0}
- target: {fileID: 4720499058283198052, guid: 631e66137c7824aa79f18da746121ef2,
type: 3}
propertyPath: m_AnchorMax.x
value: 0.5
objectReference: {fileID: 0}
- target: {fileID: 4720499058283198052, guid: 631e66137c7824aa79f18da746121ef2,
type: 3}
propertyPath: m_AnchorMax.y
value: 0.5
objectReference: {fileID: 0}
- target: {fileID: 4720499058283198052, guid: 631e66137c7824aa79f18da746121ef2,
type: 3}
propertyPath: m_AnchorMin.x
value: 0.5
objectReference: {fileID: 0}
- target: {fileID: 4720499058283198052, guid: 631e66137c7824aa79f18da746121ef2,
type: 3}
propertyPath: m_AnchorMin.y
value: 0.5
objectReference: {fileID: 0}
- target: {fileID: 4720499058283198052, guid: 631e66137c7824aa79f18da746121ef2,
type: 3}
propertyPath: m_SizeDelta.x
value: 100
objectReference: {fileID: 0}
- target: {fileID: 4720499058283198052, guid: 631e66137c7824aa79f18da746121ef2,
type: 3}
propertyPath: m_SizeDelta.y
value: 100
objectReference: {fileID: 0}
- target: {fileID: 4720499058283198052, guid: 631e66137c7824aa79f18da746121ef2,
type: 3}
propertyPath: m_LocalScale.x
value: 1
objectReference: {fileID: 0}
- target: {fileID: 4720499058283198052, guid: 631e66137c7824aa79f18da746121ef2,
type: 3}
propertyPath: m_LocalScale.y
value: 1
objectReference: {fileID: 0}
- target: {fileID: 4720499058283198052, guid: 631e66137c7824aa79f18da746121ef2,
type: 3}
propertyPath: m_LocalScale.z
value: 1
objectReference: {fileID: 0}
- target: {fileID: 4720499058283198052, guid: 631e66137c7824aa79f18da746121ef2,
type: 3}
propertyPath: m_LocalPosition.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 4720499058283198052, guid: 631e66137c7824aa79f18da746121ef2,
type: 3}
propertyPath: m_LocalPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 4720499058283198052, guid: 631e66137c7824aa79f18da746121ef2,
type: 3}
propertyPath: m_LocalPosition.z
value: 0
objectReference: {fileID: 0}
- target: {fileID: 4720499058283198052, guid: 631e66137c7824aa79f18da746121ef2,
type: 3}
propertyPath: m_LocalRotation.w
value: 1
objectReference: {fileID: 0}
- target: {fileID: 4720499058283198052, guid: 631e66137c7824aa79f18da746121ef2,
type: 3}
propertyPath: m_LocalRotation.x
value: -0
objectReference: {fileID: 0}
- target: {fileID: 4720499058283198052, guid: 631e66137c7824aa79f18da746121ef2,
type: 3}
propertyPath: m_LocalRotation.y
value: -0
objectReference: {fileID: 0}
- target: {fileID: 4720499058283198052, guid: 631e66137c7824aa79f18da746121ef2,
type: 3}
propertyPath: m_LocalRotation.z
value: -0
objectReference: {fileID: 0}
- target: {fileID: 4720499058283198052, guid: 631e66137c7824aa79f18da746121ef2,
type: 3}
propertyPath: m_AnchoredPosition.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 4720499058283198052, guid: 631e66137c7824aa79f18da746121ef2,
type: 3}
propertyPath: m_AnchoredPosition.y
value: -120
objectReference: {fileID: 0}
- target: {fileID: 4720499058283198052, guid: 631e66137c7824aa79f18da746121ef2,
type: 3}
propertyPath: m_LocalEulerAnglesHint.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 4720499058283198052, guid: 631e66137c7824aa79f18da746121ef2,
type: 3}
propertyPath: m_LocalEulerAnglesHint.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 4720499058283198052, guid: 631e66137c7824aa79f18da746121ef2,
type: 3}
propertyPath: m_LocalEulerAnglesHint.z
value: 0
objectReference: {fileID: 0}
- target: {fileID: 4732023361674043342, guid: 631e66137c7824aa79f18da746121ef2,
type: 3}
propertyPath: m_Size
value: 1
objectReference: {fileID: 0}
- target: {fileID: 4821767156134577128, guid: 631e66137c7824aa79f18da746121ef2,
type: 3}
propertyPath: m_AnchoredPosition.x
value: -0.0000038146973
objectReference: {fileID: 0}
- target: {fileID: 7042572429314006190, guid: 631e66137c7824aa79f18da746121ef2,
type: 3}
propertyPath: m_Name
value: CardList
objectReference: {fileID: 0}
m_RemovedComponents: []
m_SourcePrefab: {fileID: 100100000, guid: 631e66137c7824aa79f18da746121ef2, type: 3}
--- !u!114 &4475072414863852767 stripped
MonoBehaviour:
m_CorrespondingSourceObject: {fileID: 2001635751693651750, guid: 631e66137c7824aa79f18da746121ef2,
type: 3}
m_PrefabInstance: {fileID: 2728508175332412409}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 7cd2f7b60e5044a60b6a926eefc18d83, type: 3}
m_Name:
m_EditorClassIdentifier:
MonoBehaviourがいくつか付いている、ということ以外、僕の読解力では何もわかりませんでした・・・
UnityYAML読解補助ツールを作ってみる
わからなすぎたので次のような可視化ツールを作ってみました。
- UnityYAMLの要素をTreeViewとして表示
-
!u!1
のような記述をGameObject
などのようなクラス名に置き換える - 参照やスクリプトに応じたIconを表示
- GUIDをファイルパスに置き換える
これなら完全にUnityYAMLを理解したような気がしてきます。
・・・本当はヒエラルキー構造も表したいところですが。
以下、細かい実装やハマりどころを紹介して行きますが、もし、とりあえず試しに動かしてみたいよっていう方は、以下のrepoからインストールしてみてください。
https://github.com/satanabe1/asset-yaml-tree-view
注意点として、yamlのパーサにYamlDotNetを採用しているので、README.mdに書いてある通り、 yamldotnet
かYamlDotNetを取り込んでいる com.unity.visualscripting
(1.6.0以上)を一緒にインストールする必要があります。
UnityYAML読解補助ツールを作るまで
UnityYAMLの読み込み
yamlパーサの選定
UnityYAMLの読み込みにはYamlDotNetを採用しました。これはYamlDotNetが.Net界隈で最もポピュラーなyamlパーサの一つであり、また、 com.unity.visualscripting
にも取り込まれていて、Unity公式のUnity Registry内で完結することもできるためです。
ただし、com.unity.visualscripting
を使いたくないよってこともあり得るので、本家のyamldotnet
にも対応してみました。
Assembly DefinitionのVersion Definesを使えば両対応は難しくありません。
Version Definesで定義したDefineでusingを切り分けます。
#if AYTV_YAMLDOTNET_11_2_OR_NEWER
using YamlDotNet.RepresentationModel;
#elif AYTV_VISUALSCRIPTING_1_6_0_OR_NEWER
using Unity.VisualScripting.YamlDotNet.RepresentationModel;
#else
#error require '"yamldotnet": "11.2.1"' or '"com.unity.visualscripting": "1.6.0"'
#endif
読み込み方
UnityYAMLを標準的なyamlパーサに流し込むと、ファイル先頭のヘッダーライン(%TAG !u! ...
など)や、オブジェクトのヘッダー(--- !u!1 &1865014687417162
など)の存在により、うまく読み込めません。
そこでアセットファイルを --- !u!
の文字列で分割し、それぞれを独立したyamlとしてパーサに流し込むことで乗り越えます。 参考実装
--- !u!
の行にも重要な情報があるので、捨てずにYamlDocumentとセットで取得しておくのがよさそうです。
TreeView表示
IMGUIのTreeViewを利用してみました。(今風にするのであればUIToolKitを使うべきだったかもしれません・・・。)
基本的にはこの辺の処理で、YamlDocumentのNodeの要素と、ほぼ1体1でAssetYamlTreeElementからなる木構造に変換しました。guidなどに関しては若干のオリジナリティを加えましたが、ほとんど意味はありません。
安直にTreeViewでyamlを表示すると以下のようになります。
何もわかりやすくなってないですね。
ClassIdの解決
UnityYAMLのよくわからなさを助長している!u!XXXX
の部分をまともなクラス名に変えてみます。
!u!XXXX
の数字の部分XXXX
は解説ではクラスIDと呼ばれています。クラスIDとクラス名の関係は こちらのドキュメント で紹介されています。
とは言え、これを一つ一つハードコードしていくのも微妙なので、Unityの内部から拾ってきます。
Unity内では UnityType.FindTypeByPersistentTypeID(int id) というメソッドを通してクラスIDから型情報を取得できるようになっています。ただしUnityTypeはpublicクラスではないのでリフレクションが必要になります。
こんな感じでクラスIDからクラス名を取得するメソッドを実装しました。
private static string GetTypeNameByPersistentTypeID(int id) {
const BindingFlags flags = BindingFlags.Public
| BindingFlags.Static
| BindingFlags.Instance
| BindingFlags.InvokeMethod
| BindingFlags.GetProperty;
var assembly = Assembly.GetAssembly(typeof(MonoScript));
var unityType = assembly.GetType("UnityEditor.UnityType");
var findTypeByPersistentTypeID = unityType.GetMethod("FindTypeByPersistentTypeID", flags);
var nameProperty = unityType.GetProperty("name", flags);
var typeInstance = findTypeByPersistentTypeID?.Invoke(null, new object[] { id });
return typeInstance != null ? nameProperty?.GetValue(typeInstance) as string : null;
}
あとは取得したクラス名と!u!XXXX
の部分を置換してやれば、以下のように、人間にも優しい表示になります。
Iconの適用
文字列だけではやっぱり直感的ではないのでIconも表示してみます。イイカンジのIconの取得というのも結構厄介で、いろんなメソッドを組み合わせることになります。(もっとシンプルに一発で取得する方法、募集中です)
使用したAPIは以下の通りです。
-
AssetDatabase.GetCachedIcon(string path)
- public
- prefabやmaterialなどのファイルパスに応じたtextureが返ってくる
- 固有のファイルパスを持つアセットに使える
-
EditorGUIUtility.FindTexture(string name) & EditorResources
- public
- テクスチャ名(
Folder Icon
Assembly Icon
etc...)に応じたtextureが返ってくる - 名前を知っておく必要がある(よく使うものはEditorResourcesに定義されていたりする)
-
InternalEditorUtility.FindIconForFile(string fileName)
- public (でもInternalプレフィックス?)
- fileNameに含まれる拡張子に応じたtextureが返ってくる
-
AssetPreview.GetMiniTypeThumbnail(Type type)
- public
- System.Typeに応じたtextureが返ってくる
- 眺めているとちょっと楽しい
-
AssetPreview.GetAssetPreviewFromGUID(string guid)
- internal
- GUIDに応じたtextureが返ってくる
- GUIDは取得できるけどファイルパスが返ってこないパターンに使える?
-
AssetPreview.GetMiniTypeThumbnailFromClassID(int classId)
- internal
- クラスIDに応じたTransform等その他組み込みコンポーネントのtextureが返ってくる
どれも微妙にできることとできないことが違うので、UnityYAML中のクラスIDやGUIDに応じたIconをサポートしようとすると、こんなことに・・・
https://github.com/satanabe1/asset-yaml-tree-view/blob/c124aee327882202b7c93c87b98acf7b8b48962e/Editor/AssetYamlTreeUtil.cs#L117-L146
アドカレネタに突貫で書いたので不要なところもありそうですが、これで取得できたIconを表示してみると以下のようになります。
結構悪くない感じになってきました。
GUIDの解決
guidのところの情報が重要な割に、人間にはナニモワカラナイ所なので、可能な限りファイルパスに置き換えてみます。
これは何も難しい話はなくて、guidというYamlNodeの配下にあるYamlScalerNodeの値を取得して、AssetDatabaseに問い合わせるだけです。
ついでで、ダブルクリックでPrefabやScriptを開けるようにしてあげるとより親切でしょう。
// override TreeView.DoubleClickedItem
protected override void DoubleClickedItem(int id)
{
base.DoubleClickedItem(id);
var first = FindRows(new List<int> { id }).FirstOrDefault() as AssetYamlTreeViewItem;
if (first?.Data?.AssetPath == null) return;
AssetDatabase.OpenAsset(AssetDatabase.LoadMainAssetAtPath(first.Data.AssetPath));
}
FileIDの解決(未実装)
FileIDを使って、親子関係やComponentのアタッチ対象なども解決してあげると実用的なツールに近づきそう、という展望はありますが、今回は実装しませんでした。いつかモチベーションが上がったら・・・
終わりに
ツールを使えば僕でもUnityYAMLの雰囲気を感じ取ることができるようになりました。
ただ、Unity上でしか表示できないっていうのは、本来僕が求めているところじゃなかったりします。
Chrome Extension化してGithub PullRequestのところで同様の可視化ができたらいいのにな、とか夢は広がりますが、それは今後の課題ということで・・・
以上です。