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

很多高手对.prefab文件内容并不陌生,但是为了接下来的展开还是解释一下内容,从每个大节点开始讲解(节选部分重要内容):
1.GameObject
- --- !u!1 &165095463815504087
- GameObject:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- serializedVersion: 6
- m_Component:
- - component: {fileID: 4433197073301798445}
- - component: {fileID: 5990753843237649034}
- - component: {fileID: 5075137301801018474}
- m_Layer: 5
- m_Name: Streak
- m_TagString: Untagged
- m_Icon: {fileID: 0}
- m_NavMeshLayer: 0
- m_StaticEditorFlags: 0
- m_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: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 165095463815504087}
- m_Enabled: 1
- m_EditorHideFlags: 0
- m_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: 1
- m_OnCullStateChanged:
- m_PersistentCalls:
- m_Calls: []
- m_Sprite: {fileID: 0}
- m_Type: 0
- m_PreserveAspect: 0
- m_FillCenter: 1
- m_FillMethod: 4
- m_FillAmount: 1
- m_FillClockwise: 1
- m_FillOrigin: 0
- m_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: 1
- m_OnCullStateChanged:
- m_PersistentCalls:
- m_Calls: []
- m_Sprite: {fileID: 0}
- m_Type: 0
- m_PreserveAspect: 0
- m_FillCenter: 1
- m_FillMethod: 4
- m_FillAmount: 1
- m_FillClockwise: 1
- m_FillOrigin: 0
- m_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<string> _head = new List<string>();
- private readonly Dictionary<long, string> _gameObjects = new Dictionary<long, string>();
-
- 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<string>();
- 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<string>();
- 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)