はじめまして、開発GでUnityクライアントエンジニアをしております
定期的にあるPrefabの編集作業、編集箇所は決まっていて作業量は少なくても
いざエディターで開いて編集すると意外と時間がかかっていることがありませんか?
インスペクターからSpriteを別のものに差し替えたり、テキストの修正だけなら
実際作業している時間よりエディターを立ち上げている時間の方が長いのでは・・・?
と思うこともしばしば(個人の感想です)
ということで、エディター外部からPrefabを編集してみることにしました
目次
Prefabファイルの内容
編集するにあたってまずPrefabファイルが何者なのかを確認しましょう
テキストエディタで開くと、中身はどうやらYAML記法のテキストのようです
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
%YAML 1.1 %TAG !u! tag:unity3d.com,2011: --- !u!1 &4745983572805821937 GameObject: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} serializedVersion: 6 m_Component: - component: {fileID: 8798888849235798586} - component: {fileID: 2115293271489869810} - component: {fileID: 5703168856476582847} - component: {fileID: 9008799669024064804} m_Layer: 5 m_Name: banner m_TagString: Untagged m_Icon: {fileID: 0} m_NavMeshLayer: 0 m_StaticEditorFlags: 0 m_IsActive: 1 --- !u!224 &8798888849235798586 RectTransform: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} |
詳しい書式の説明も公式に記載されています フォーマットに関する説明
さて、YAMLならYAMLのパーサーがあるから安全に編集できそう、と思ったのですが、
どうやらPythonで有名どころのPyYAMLではTag諸々の関係でUnityのYAMLは読めないようです
テキストとして編集するのはうっかり壊しそうですし、パーサーの自作も大変……
と調べていたら、すでにライブラリがありました
UnityPerser
https://github.com/socialpoint-labs/unity-yaml-parser
内部ではPyYAMLを利用しているようでした
Unity用のローダーなどを渡したり、Unity固有タグを足したり、といった部分を担ってくれているよう
使い方はサンプルにあるように、ファイルパスを渡してload/dumpするだけなので手軽に使えそうです
実際に使ってみた
このPrefabを編集してみます
1.自作クラスのフィールドを編集する
banner.prefabの_currentEventIdを書き換えます
1 2 3 4 5 6 7 8 9 10 11 12 13 |
--- !u!114 &9008799669024064804 MonoBehaviour: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 4745983572805821937} m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: 20e0aec2297b7464ea9d7828e5f9692d, type: 3} m_Name: m_EditorClassIdentifier: _currentEventId: 100020 |
実行するスクリプトはこちら
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
import pathlib from unityparser import UnityDocument BANNER_PATH = "Assets/Prefabs" def main(): # Prefabsフォルダからbanner.prefabを取得 try: banner = next(pathlib.Path(BANNER_PATH).glob("**/banner.prefab")) except StopIteration as e: print("[error] not found : banner.prefab") return False # 読み込み doc = UnityDocument.load_yaml(banner) # すべての_currentEventIdフィールドを取得,値の代入 components = doc.filter(class_names=("MonoBehaviour",), attributes=("_currentEventId",)) for component in components: print(component._currentEventId) component._currentEventId = 1000 # 保存 doc.dump_yaml() main() |
実行すると、以下のように書き換わっています
1 2 3 |
m_Name: m_EditorClassIdentifier: _currentEventId: 1000 |
エディターで確認してもこの通り、反映されています
特定のコンポーネントの_currentEventIdだけを変えたい場合はコンポーネントのguidで検索
1 2 3 4 |
guid = "20e0aec2297b7464ea9d7828e5f9692d" for component in components: if component.m_Script['guid'] == guid: component._currentEventId = 1000 |
2.Spriteの参照入れ替え
以下のようにprefabとtextureでまとめて別ディレクトリに入っている構成で
prefab以下のevent01を複製しevent02をつくります
event02以下のbanner.prefabのImageは
textures以下のevent01の画像を参照しています
これをevent02の画像に差し替える場合について考えます
画像の名前とguidをDictionaryか何かで紐づけて置換すれば良さそうですね
画像のguidはmetaファイルから取得できます
画像名もイベントの連番部分が変更されているのでそこの対応もいれると、以下のようなスクリプトになります
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
import pathlib import itertools from unityparser import UnityDocument def main(): prefab_path = pathlib.Path("Assets/event/prefabs/event02/banner.prefab") replace_path = [[pathlib.Path("Assets/event/textures/event01"), pathlib.Path("Assets/event/textures/event02")]] replace_names = [["event_01", "event_02"]] replace_sprite(prefab_path, replace_path, replace_names) def replace_sprite(prefab_path, replace_path, replace_names): """ 指定したprefabスプライトの参照(guid)を書き換える args: prefab_path: prefabが入っているディレクトリのpathObjectのList [path1, path2, ...] replace_path: texture新旧のpathObjectのペアのList [[original_path, new_path], ...] replace_names: 新旧素材名の一部を差し替えている場合 [['original', 'replaced'], ...] """ for path in replace_path: texture_path = path[0] # 旧スプライトのテクスチャファイル名とguidのDictionaryを生成 key: guid, val: name old_guids = {} for texture in itertools.chain(texture_path.glob("**/*.png.meta"), texture_path.glob("**/*.jpg.meta")): key = _get_guid_meta(texture) val = str(texture).replace(str(path[0]), "") # 画像名で比較できるよう新しい名前に置換 for replace in replace_names: val = val.replace(replace[0], replace[1]) old_guids[key] = val # 新スプライトのテクスチャファイル名とguidのDictionaryを生成 key: name, val: guid new_guids = {} texture_path = path[1] for texture in itertools.chain(texture_path.glob("**/*.png.meta"), texture_path.glob("**/*.jpg.meta")): key = str(texture).replace(str(path[1]), "") val = _get_guid_meta(texture) new_guids[key] = val # 差し替え _replace_prefab_sprite(prefab_path, old_guids, new_guids) def _replace_prefab_sprite(prefab, old_guids, new_guids): doc = UnityDocument.load_yaml(prefab) # Prefab内のSpriteを取得 完全一致ではとれない(m_SpriteAnim~なども返ってくる) sprites = doc.filter(class_names=("MonoBehaviour", "SpriteRenderer"), attributes=("m_Sprite",)) # Spriteのguidを差し替える for sprite in sprites: guid = sprite.m_Sprite.get("guid") # guidがないものも返ってきているのでgetで取得 name = old_guids.get(guid) new_guid = new_guids.get(name) if new_guid is not None: sprite.m_Sprite["guid"] = new_guid doc.dump_yaml() def _get_guid_meta(meta): with open(meta) as f: for line in f.read().splitlines(): label = "guid: " if line.startswith(label): guid = line.replace(label, "") return guid return None main() |
このスクリプトを実行すると
参照しているSpriteの差し変えができています
1 2 3 |
# Prefab内のSpriteを取得 完全一致ではとれない(m_SpriteAnim~なども返ってくる) sprites = doc.filter(class_names=("MonoBehaviour", "SpriteRenderer"), attributes=("m_Sprite",)) |
この部分ですが、公式のサンプルのように直接キーでアクセスすると、guidがNoneになってしまうものがありエラーがでるので調べたところ、attributesで指定した名前を「含む」attributeを持った要素が返されるようでした
使ってみての感想
思ったより手軽に書き換えができるようでした
fileIdを参照して特定のオブジェクトのコンポーネントを編集、という使い方もできるとよかったのですが
標準では対応していなさそうでした
しかし、数秒とかからない実行時間は魅力ですし、c#+エディタ拡張での対応よりもExcel等外部ファイルとの連携もしやすそうです
他にもJenkinsと組み合わせて自動化したり、対話型のスクリプトを書いて小回りの効くCLツール化もできそうです
ちょっとした入力値のチェックも挟みやすいので、一部では手作業よりもミスを減らす効果も期待できるのではないでしょうか
うまく使っていけると開発効率が上がりそうですね!
いくつか定常作業をまとめてツール化してみたいと思います
ぜひ皆さんもUnityParser使ってみてください!