在开发项目过程中,如果出现了Unity版本变化,有可能会导致一些预制体上的UI组件丢失,特别是大量UI脚本,明明一看就知道这个是Text组件,但是一个大大的missing出现在预制体上,让人产生了莫名的恐慌。
一、根据.prefab文件信息,分析引用的UGUI脚本信息
我们如果此时打开.prefab文件查看,大概可以看到如下信息(ForceText设置可以使得.prefab的显示内容以文本展示而非二进制格式)。

很多高手对.prefab文件内容并不陌生,但是为了接下来的展开还是解释一下内容,从每个大节点开始讲解(节选部分重要内容):
1.GameObject
--- !u!1 &165095463815504087
GameObject:m_ObjectHideFlags: 0m_CorrespondingSourceObject: {fileID: 0}m_PrefabInstance: {fileID: 0}m_PrefabAsset: {fileID: 0}serializedVersion: 6m_Component:- component: {fileID: 4433197073301798445}- component: {fileID: 5990753843237649034}- component: {fileID: 5075137301801018474}m_Layer: 5m_Name: Streakm_TagString: Untaggedm_Icon: {fileID: 0}m_NavMeshLayer: 0m_StaticEditorFlags: 0m_IsActive: 1
`--- !u!1 &165095463815504087`: 此行表示该配置块(往后一块内容都简称为配置块)在此文件中的私有fileID;
`GameObject`: 表示一个预制体中的GameObject节点,如果一个预制体由多个GameObject组成,这块仅表示其中一个;
`m_Component`: 表示该GameObject所引用的私有fileID
`m_Name`: 顾名思义,这个GameObject的名称,可以在Hierarchy面板中很轻松找到名字,同时也可以方便在后续其他配置块中找到自身所属的GameObject;
2.RectTransform、CanvasRenderer
内容略去,该部分一般不会丢失,因为他不隶属于UnityEngine.UI,同时名字可以直接看到
3.MonoBehaviour
--- !u!114 &5075137301801018474
MonoBehaviour:m_ObjectHideFlags: 0m_CorrespondingSourceObject: {fileID: 0}m_PrefabInstance: {fileID: 0}m_PrefabAsset: {fileID: 0}m_GameObject: {fileID: 165095463815504087}m_Enabled: 1m_EditorHideFlags: 0m_Script: {fileID: 11500000, guid: d6072c12dfea5c74897ce48533ec3f2a, type: 3}m_Name: m_EditorClassIdentifier: m_Material: {fileID: 0}m_Color: {r: 1, g: 1, b: 1, a: 1}m_RaycastTarget: 1m_OnCullStateChanged:m_PersistentCalls:m_Calls: []m_Sprite: {fileID: 0}m_Type: 0m_PreserveAspect: 0m_FillCenter: 1m_FillMethod: 4m_FillAmount: 1m_FillClockwise: 1m_FillOrigin: 0m_UseSpriteMesh: 0
敲黑板,该块表示引用了一个MonoBehavior脚本,有可能是自定义的,也有可能是其他继承自Component的脚本,这是我们解析出一个脚本是自定义脚本还是UI脚本的关键;
m_GameObject: {fileID: 165095463815504087} ,表示该脚本所属的GameObject,根据fileID向文件内索引,对应上文中的`--- !u!1 &165095463815504087`,所以是Streak上挂载的一个脚本
m_Script: {fileID: 11500000, guid: d6072c12dfea5c74897ce48533ec3f2a, type: 3},表示该脚本的引用信息,fileID表示所属的文件ID,如果是自定义脚本,通常都是11500000(但有例外,我所使用的Unity2019中,UnityEngine.UI中的也是11500000),如果是dll集,则表示dll中某个具体类的引用,guid则表示他在Unity中所归属的具体文件,type略去,暂时不影响复原操作
这两条配置项是所有配置块都会存在的内容,而我们需要根据接下来的配置项,来推测一个MonoBehavior大概率可能属于哪一类型的UGUI脚本
m_Material: {fileID: 0}m_Color: {r: 1, g: 1, b: 1, a: 1}m_RaycastTarget: 1m_OnCullStateChanged:m_PersistentCalls:m_Calls: []m_Sprite: {fileID: 0}m_Type: 0m_PreserveAspect: 0m_FillCenter: 1m_FillMethod: 4m_FillAmount: 1m_FillClockwise: 1m_FillOrigin: 0m_UseSpriteMesh: 0
其实很容易就发现了一个关键字`m_Sprite`,这大概率就是Image组件所使用的配置项,同时`m_FillCenter`等选项基本可以认定了这就是Image组件,因此,我们只需要根据本工程的Image的fileID,guid,type3个信息,修改该块中`m_Script`项即可复原丢失的引用。
二、索引出本项目中所有的UGUI信息
将所有集成自Component且出自程序集UnityEngine.UI的类型,添加到GameObject上,制成预制体,并根据上述分析获得fileID,guid,type,并记录到文件中。
private static void StatisticOrderedScriptGUIDs(string asmName, string fileName){var g = new GameObject();var asm = Assembly.Load(asmName);var types = asm.GetTypes();var guids = new List();long localId;string guid;foreach (var type in types){// 抽象类或不继承自Component的类过滤if (type.IsAbstract || !type.IsSubclassOf(typeof(Component))){continue;}g.name = type.Name;var component = g.AddComponent(type);if (component){// 此处设置一个临时路径var prefabPath = $"Assets/Editor/GUIDPrefab/{type.Name}.prefab";// 自定义方法,根据文件名判断文件夹是否存在,不存在则创建FileHelper.CreateDirectoryIfNotExistByFilename(prefabPath);var success = false;PrefabUtility.SaveAsPrefabAsset(g, prefabPath, out success);AssetDatabase.Refresh();var prefab = AssetDatabase.LoadAssetAtPath(prefabPath, type);if (!prefab){Debug.LogError($"type:{type} cannot load, path:{prefabPath}");continue;}if (AssetDatabase.TryGetGUIDAndLocalFileIdentifier(prefab, out guid, out localId)){var fileId = $"&{localId}";var metaLines = File.ReadAllLines(Path.GetFullPath(prefabPath));var foundLine = false;foreach (var line in metaLines){if (line.IndexOf(fileId) > 0){foundLine = true;continue;}if (!foundLine){continue;}if (line.IndexOf("m_Script:") > 0){AnalysisScriptLine(line, out long realFileId, out string realGuid, out int fileType);guids.Add(new FileTypeGUID(type, realFileId, realGuid, fileType));}}}GameObject.DestroyImmediate(component);}}GameObject.DestroyImmediate(g);var json = JsonConvert.Serialize(guids, true);Debug.Log(json);// 将收集的信息存储下来,为后续分析具体.prefab时供参考var path = Application.dataPath.Replace("Assets", $"{fileName}.json");File.WriteAllText(path, json);}private static void AnalysisScriptLine(string line, out long fileId, out string guid, out int type){fileId = 0;guid = string.Empty;type = 0;if (!line.Trim().StartsWith("m_Script:")){return;}var index = line.IndexOf("{");var endIndex = line.LastIndexOf("}");var inside = line.Substring(index + 1, endIndex - index - 1);var arr = inside.Split(',');foreach (var item in arr){var pair = item.Split(':');switch (pair[0].Trim()){case "fileID":long.TryParse(pair[1].Trim(), out fileId);break;case "guid":guid = pair[1].Trim();break;case "type":int.TryParse(pair[1].Trim(), out type);break;}}}
至于为什么参数asmName作为传入参数,当然是因为后续更新到了自定义开发脚本也可修复的原因。
通过上述步骤,我们调用 `StatisticOrderedScriptGUIDs("UnityEngine.UI", "uguiguids");` 获取了项目内所有UGUI组件的guids
三、动手尝试修复一个.prefab
这个先把一个预制体文件打开,随后随手修改其中MonoBehavior块中的fileID,guid后,该预制体在Unity中即呈现丢失引用状态。
然后我们需要编写一些代码来储存如下信息,CharacteristicInfo(UGUI类型的特征信息),FileTypeGUID(fileID, guid, type的相关信息),PrefabContent(整个.prefab文件读取后的可读内容),PrefabChunk(.prefab文件中的配置块),FileTypeGUIDCollection(FileTypeGUID的集合,不是简单用数组处理),
以下是代码信息,也可以跳过不看,自己尝试开发,因为思路已经都在上边了。
private struct CharacteristicInfo{public string Characteristic { get; set; }public Type Type { get; set; }public CharacteristicInfo(string c, Type t){Characteristic = c;Type = t;}}
private class FileTypeGUID{// 该信息组对应的实际类型public Type Type { get; set; }public long FileId { get; set; }public string Guid { get; set; }public int FileType { get; set; }public FileTypeGUID(Type type, long fileId, string guid, int fileType){Type = type;FileId = fileId;Guid = guid;FileType = fileType;}public override string ToString(){return $"Type:{Type.Name}, FileId:{FileId}, Guid:{Guid}, FileType:{FileType}";}}
private class PrefabContent{public Object Object { get; private set; }private readonly List _chunks = new List();private readonly List _head = new List();private readonly Dictionary _gameObjects = new Dictionary();private PrefabContent(Object belonging){Object = belonging;}public void Analysis(TryGetFileGUITypeDelegate tryGetHandler, IsConfusingTypeDelegate isConfusingTypeHandler){var changeCount = 0;FileTypeGUID[] fileTypeGUIDs;foreach (var chunk in _chunks){if (tryGetHandler.Invoke(chunk, out var item)){if (isConfusingTypeHandler.Invoke(chunk, out fileTypeGUIDs)){Debug.LogError("=========== confusing warning ===========");Debug.LogError($"chunk.Component is a confusing type, should confirm again, {chunk.PrefabContent.Object}", chunk.PrefabContent.Object);foreach (var guid in fileTypeGUIDs){Debug.LogError($"Type:{guid.Type}, FileID:{guid.FileId}, guid:{guid.Guid}, type:{guid.FileType}");}Debug.LogError("=========== end ===========");}if (chunk.ModifyM_Script(item.FileId, item.Guid, item.FileType)){changeCount++;}}}Debug.Log($"修改了 {changeCount} 个组件");}public string[] GetLines(){var list = new List();list.AddRange(_head);foreach (var chunk in _chunks){list.AddRange(chunk.Lines);}return list.ToArray();}public string GetName(long id){_gameObjects.TryGetValue(id, out var name);return name;}public static PrefabContent Parse(Object selected, string[] lines, CharacteristicInfo[] characteristics){var count = lines.Length;var listInChunk = new List();var content = new PrefabContent(selected);var id = 0L;var foundGameObjectTag = false;for (int i = 0; i < count; i++){var line = lines[i];if (line.StartsWith("%")){content._head.Add(line);continue;}if (line.StartsWith("---")){if (i + 1 < count && lines[i + 1].StartsWith("GameObject:")){var andIndex = line.IndexOf('&');var idString = line.Substring(andIndex + 1);long.TryParse(idString, out id);foundGameObjectTag = true;}if (listInChunk.Count != 0){var chunk = new PrefabChunk(content, listInChunk.ToArray(), characteristics);content._chunks.Add(chunk);listInChunk.Clear();}listInChunk.Add(line);continue;}if (foundGameObjectTag){var nameTag = "m_Name:";var nameIndex = line.IndexOf(nameTag);if (!string.IsNullOrEmpty(line) && nameIndex != -1){var name = line.Substring(nameIndex + nameTag.Length).Trim();content._gameObjects[id] = name;foundGameObjectTag = false;}}listInChunk.Add(line);}// 添加剩余的if (listInChunk.Count != 0){var chunk = new PrefabChunk(content, listInChunk.ToArray(), characteristics);content._chunks.Add(chunk);listInChunk.Clear();}return content;}}
private class PrefabChunk{private const string ScriptFormat = " m_Script: {fileID: #FILEID#, guid: #GUID#, type: #TYPE#}";public string Name { get; private set; }public PrefabContent PrefabContent { get; private set; }public Type ComponentType { get; private set; }public string[] Lines { get; private set; }public long FileID { get { return _fileID; } }private long _fileID;public string GUID { get { return _guid; } }private string _guid;public int Type { get { return _type; } }private int _type;private int _index;public PrefabChunk(PrefabContent content, string[] lines, CharacteristicInfo[] characteristics){PrefabContent = content;Lines = lines;FindName();FindType(characteristics);FindScriptLine();}private void FindName(){var line = FindLine("m_GameObject:", out var gameObjectIndex);if (string.IsNullOrEmpty(line) && gameObjectIndex == -1){return;}var gameObjectContent = line.Substring(gameObjectIndex);if (string.IsNullOrEmpty(gameObjectContent)){return;}gameObjectContent = gameObjectContent.Trim().Trim('{').Trim('}');var filedIDTag = "fileID:";var fileIDIndex = gameObjectContent.IndexOf(filedIDTag);if (fileIDIndex == -1){return;}var idString = gameObjectContent.Substring(fileIDIndex + filedIDTag.Length);long.TryParse(idString, out var id);if (id == 0){return;}Name = PrefabContent.GetName(id);}private void FindType(CharacteristicInfo[] characteristics){foreach (var pair in characteristics){if (FindCharacteristic(pair.Characteristic)){ComponentType = pair.Type;}}}private void FindScriptLine(){_index = -1;var count = Lines.Length;for (int i = 0; i < count; i++){if (Lines[i].Contains("m_Script")){_index = i;break;}}if (_index == -1){return;}AnalysisScriptLine(Lines[_index], out _fileID, out _guid, out _type);}public string FindLine(string tag, out int index){index = -1;foreach (var line in Lines){index = line.IndexOf(tag);if (index != -1){index += tag.Length;return line;}}return string.Empty;}private bool FindCharacteristic(string tags){var tagArr = tags.Split('&');var tagCount = tagArr.Length;var count = Lines.Length;var matchCount = 0;for (int tagIndex = 0; tagIndex < tagCount; tagIndex++){var tag = tagArr[tagIndex];for (int i = 0; i < count; i++){var line = Lines[i];if (line.Contains(tag)){matchCount++;}}}return matchCount == tagCount;}public bool ModifyM_Script(long fileId, string guid, int type){if (_index == -1){return false;}if (_fileID == fileId && _guid == guid && _type == type){Debug.Log($"{Name} is using correct reference, needn't to fix");return false;}var line = ScriptFormat.Replace("#FILEID#", fileId.ToString()).Replace("#GUID#", guid).Replace("#TYPE#", type.ToString());Debug.Log($"{Name}:{ComponentType} ---> fileID:{fileId}, guid:{guid}, type:{type}");Lines[_index] = line;return true;}}
private class FileTypeGUIDCollection{private bool _inited;protected readonly Dictionary _commonGUIDs = new Dictionary();protected readonly Dictionary _confusingGUIDs = new Dictionary();public void Init(string path){if (_inited){return;}if (!File.Exists(path)){StatisticAllUGUIGUIDs();}var json = File.ReadAllText(path);var guids = JsonConvert.Deserialize(json);foreach (var item in guids){_commonGUIDs[item.Type] = item;if (item.Type == typeof(VerticalLayoutGroup) || item.Type == typeof(HorizontalLayoutGroup)){_confusingGUIDs[item.Type] = item;}}_inited = true;}public virtual bool TryGetValue(PrefabChunk chunk, out FileTypeGUID item){if (chunk.ComponentType == null){item = null;return false;}return _commonGUIDs.TryGetValue(chunk.ComponentType, out item);}public virtual bool IsConfusingType(PrefabChunk chunk, out FileTypeGUID[] fileTypeGUIDs){fileTypeGUIDs = null;if (chunk.ComponentType == null){return false;}if (_confusingGUIDs.ContainsKey(chunk.ComponentType)){fileTypeGUIDs = _confusingGUIDs.Values.ToArray();return true;}return false;}}
此外还需要2个委托和一个特征信息集合
// 特征字典,根据配置块中特点返回组件类型private static readonly CharacteristicInfo[] UGUICharacteristics = new CharacteristicInfo[]{new CharacteristicInfo("m_Text&m_FontData", typeof(Text)),new CharacteristicInfo("m_InputType&m_OnEndEdit&m_OnValueChanged", typeof(InputField)),new CharacteristicInfo("m_Sprite&m_FillCenter", typeof(Image)),new CharacteristicInfo("m_OnCullStateChanged&m_Texture&m_UVRect", typeof(RawImage)),new CharacteristicInfo("m_OnClick&m_TargetGraphic&m_SpriteState&m_Interactable", typeof(Button)),new CharacteristicInfo("m_MovementType&m_Elasticity&m_Viewport&m_OnValueChanged", typeof(ScrollRect)),new CharacteristicInfo("m_AnimationTriggers&m_Interactable&m_TargetGraphic&m_HandleRectm_NumberOfSteps&m_OnValueChanged", typeof(Scrollbar)),new CharacteristicInfo("m_ShowMaskGraphic", typeof(Mask)),new CharacteristicInfo("m_Padding&m_ChildAlignment&m_CellSize&m_Spacing", typeof(GridLayoutGroup)),new CharacteristicInfo("m_Padding&m_ChildAlignment&m_Spacing&m_ChildForceExpandWidth&m_ChildForceExpandHeight&m_ChildControlWidth&m_ChildControlHeight&m_ChildScaleWidth&m_ChildScaleHeight", typeof(HorizontalLayoutGroup)),new CharacteristicInfo("m_Padding&m_ChildAlignment&m_Spacing&m_ChildForceExpandWidth&m_ChildForceExpandHeight&m_ChildControlWidth&m_ChildControlHeight", typeof(VerticalLayoutGroup)),new CharacteristicInfo("m_EffectColor&m_EffectDistance&m_UseGraphicAlpha", typeof(Outline)),new CharacteristicInfo("m_DynamicPixelsPerUnit&m_ReferenceResolution", typeof(CanvasScaler)),new CharacteristicInfo("m_IgnoreReversedGraphics&m_BlockingObjects&m_BlockingMask", typeof(GraphicRaycaster)),new CharacteristicInfo("m_FirstSelected&m_DragThreshold", typeof(EventSystem)),new CharacteristicInfo("m_HorizontalAxis&m_VerticalAxis&m_SubmitButton&m_CancelButton", typeof(StandaloneInputModule))};delegate bool TryGetFileGUITypeDelegate(PrefabChunk chunk, out FileTypeGUID fileTypeGUID);delegate bool IsConfusingTypeDelegate(PrefabChunk chunk, out FileTypeGUID[] confusingGUIDs);
随后,使用一段调用代码即可开始修复工作
// 尝试修复引用丢失[MenuItem("Assets/Try To Fix UGUIComponent Missing")]private static void TryFixUGUIComponentMissing(){var collection = new FileTypeGUIDCollection();collection.Init(Application.dataPath.Replace("Assets", "uguiguids.json"));FixComponentMissingBase(collection.TryGetValue, collection.IsConfusingType, UGUICharacteristics);}private static void FixComponentMissingBase(TryGetFileGUITypeDelegate tryGetHandler, IsConfusingTypeDelegate isConfusingTypeHandler, CharacteristicInfo[] characteristics){var selected = Selection.activeGameObject;// 对单个具体的prefab进行引用修复if (!selected){return;}var prefabPath = AssetDatabase.GetAssetPath(selected);var lines = File.ReadAllLines(prefabPath);var prefabContent = PrefabContent.Parse(selected, lines, characteristics);prefabContent.Analysis(tryGetHandler, isConfusingTypeHandler);lines = prefabContent.GetLines();File.WriteAllLines(prefabPath, lines);AssetDatabase.Refresh();Debug.Log("Done");}
上述class为什么为private,是因为都限定在static class PrefabMissingTool中,并不希望外部访问,读者也可以根据自己的需要修改访问范围。
目前实现了对单个选中的prefab进行修复,也可以自行扩展为对复选或文件夹范围的prefab进行修复。
=========================== !!重要!! ==========================
因为`VerticalLayoutGroup`和`HorizontalLayoutGroup`两个类型中的特征信息(配置项)在作者的.prefab配置块中完全一致,因此无法区分,如果后续有发现更新的办法,会更新到文章中,或者也请读者指出更好的做法
=========================== 扩展 ==========================
因为可以搜集`UnityEngine.UI`的类型信息,那么同样,也可以收集`Assembly-CSharp`中继承自`Component`的信息,从原版本中收集信息后,到新版本根据匹配结果进行修改,那么也就可以修复在新版本中产生的自定义脚本挂载丢失的情况了。
后续发现有更好的办法时会及时更新。
github上可查看源码,已解耦合,仅依赖Newtonsoft,
DoyoFish/PrefabMissingFixTool: Quick to fix missing component on unity prefab (github.com)
下一篇:【java】Lambda表达式