本文参考了这片博客文章,在此基础上进行优化和改进:
https://blog.csdn.net/akof1314/article/details/80868869
先截张效果图:
TextMeshPro在之前的博客中有介绍:
https://www.cnblogs.com/koshio0219/p/11643268.html
思来想去,这东西还是有些使用不方便的地方,问题的根本还是在于中文字库太多,虽然缩减为7000简体字库或3500简体字库可以解决问题。
但无论怎么说,游戏中大量的字其实是没有用到的,这势必会造成资源浪费。
于是,接下来的想法也就应运而生——为什不能找到游戏中所有用到的字,只将这些字渲染进纹理图呢,有新增的字就更新下字库和纹理图就好了。
这也就是上面这个工具诞生的最主要原因,它主要为了实现:
1.批量查找游戏中Canvas或其他GameObject上的文字内容
2.扫描查找指定路径下的配置文件中文本内容
3.将这些文本去除重复字符后保存到一个固定的输出路径
4.根据输出的游戏文本内容按照想要生成的TMP字体类型批量一键生产和更新
5.随时批量修改Canvas上的字体资源
下面是一些相对具体的思路:
查找Canvas中的文字资源很简单,只需要遍历所有的对应组件上的内容就行了:
1 string newText = ""; 2 foreach (var targetCanvas in targetCanvasList) 3 { 4 TextMeshProUGUI[] textMeshProUGUIs = targetCanvas.GetComponentsInChildren(true); 5 foreach (var item in textMeshProUGUIs) 6 { 7 newText += item.text; 8 } 9}
对于配置文件,需要在指定的文件夹路径中查找:
1 private void FindTextAssets(string textAssetPath) 2 { 3 textAssetList.Clear(); 4 if (Directory.Exists(textAssetPath)) 5 { 6 DirectoryInfo info = new DirectoryInfo(textAssetPath); 7 FileInfo[] files = info.GetFiles("*", SearchOption.AllDirectories); 8 9 for (int i = 0; i < files.Length; i++) 10 { 11 //去除meat文件 12 if (!files[i].Name.EndsWith(".csv.meat")) 13 { 14 var str = File.ReadAllText(files[i].ToString()); 15 Debug.Log(str); 16 textAssetList.Add(str); 17 } 18 } 19 } 20 }
对于得到的文字,每次更新时进行字符去重,去空格,去换行:
1 private string StrCutRepeat(string oriStr) 2 { 3 return string.Join("", oriStr.ToArray().Distinct().ToArray()).Replace("\n", "").Replace(" ", ""); 4 }
上面的需要用到以下命名空间:
using System.IO; using System.Linq;
每次更新完文本内容需要刷新资源:
AssetDatabase.Refresh();
清理文本资源:
1 private void ClearTextAsset(string path) 2 { 3 if (File.Exists(path)) 4 { 5 File.WriteAllText(path, "X"); 6 AssetDatabase.Refresh(); 7 } 8 }
单个TMP字体纹理的生成功能在插件中已经有了,这里只需要实现选择控制和按顺序批量生产就可以了。
在Updata()中检测上一个字体资源的生成进度,按百分比显示,当生成完成时循环生成下一个即可:
1 private void MyUpdate() 2 { 3 if (m_IsRepaintNeeded) 4 { 5 m_IsRepaintNeeded = false; 6 Repaint(); 7 } 8 9 // 第一步创建字体渲染数组 10 if (m_IsProcessing) 11 { 12 m_AtlasGenerationProgress = FontEngine.generationProgress; 13 m_FontAssetInfos[m_CurGenerateIndex].genPercent = m_AtlasGenerationProgress * 100; 14 15 m_IsRepaintNeeded = true; 16 } 17 18 // 是否生成完 19 if (m_IsRenderingDone) 20 { 21 m_IsProcessing = false; 22 m_IsRenderingDone = false; 23 24 if (m_IsGenerationCancelled == false) 25 { 26 // 第二步输出渲染结果 27 UpdateRenderFeedbackWindow(); 28 // 第三步将渲染数组填充到纹理贴图(注意,贴图共享不删除) 29 CreateFontTexture(); 30 foreach (var asset in m_FontAssetInfos[m_CurGenerateIndex].assets) 31 { 32 //保存信息到字体资产 33 Save_SDF_FontAsset(asset); 34 } 35 // 最后置空 36 m_FontAtlasTexture = null; 37 } 38 Repaint(); 39 //开始循环生成下一个资源 40 GenerateNext(); 41 } 42 else if (m_LoadFontFaceInMainThread == 1) 43 { 44 FontEngineError errorCode = FontEngine.LoadFontFace(m_LoadFontFaceInMainThreadFontPath); 45 m_LoadFontFaceInMainThread = errorCode == FontEngineError.Success ? 2 : 3; 46 } 47 }
1 private void GenerateNext() 2 { 3 m_CurGenerateIndex++; 4 //判断是否所有资源序列已经执行完 5 if (m_CurGenerateIndex >= m_FontAssetInfos.Count) 6 { 7 EditorUtility.DisplayDialog("提示", "生成字库资产成功!", "OK"); 8 return; 9 } 10 11 //判断资源信息的生成开关是否已经开启 12 var info = m_FontAssetInfos[m_CurGenerateIndex]; 13 if (!info.toggle) 14 { 15 GenerateNext(); 16 return; 17 } 18 19 //得到资源路径下的文件 20 m_SourceFontFile = AssetDatabase.LoadAssetAtPath(info.fontPath); 21 //具体生成流程 22 GenerateFontAtlasButton(); 23 }
批量赋值字体资源:
1 private void AttachFontAsset() 2 { 3 foreach (var targetCanvas in attachCanvasList) 4 { 5 TextMeshProUGUI[] textMeshProUGUIs = targetCanvas.GetComponentsInChildren(true); 6 foreach (var item in textMeshProUGUIs) 7 { 8 item.font = tmpFontAsset; 9 } 10 } 11 }
Editor窗口显示部分:
大部分的Editor显示功能都是非常基础的,这里记录下几个不太常用的功能的实现:
1.显示需要序列化的属性,例如要序列化List:
先定义两个属性,一个是需要序列化的属性,另一个是该序列化属性对应的对象,如下:
[SerializeField] ListtargetCanvasList = new List (); SerializedProperty _canvasPropertyt;
在OnEnable()中进行序列化对象的初始化和对应属性查找赋值:
1 public void OnEnable() 2 { 3 _serializedObject = new SerializedObject(this); 4 _canvasPropertyt = _serializedObject.FindProperty("targetCanvasList"); 5 }
在绘制函数中对序列化对象进行修改更新的检测,并随时提交修改,例如:
1 public void OnGUI() 2 { 3 _serializedObject.Update(); 4 5 EditorGUI.BeginChangeCheck(); 6 7 EditorGUILayout.PropertyField(_canvasPropertyt, true); 8 9 if (EditorGUI.EndChangeCheck()) 10 _serializedObject.ApplyModifiedProperties(); 11 }
注意在函数EditorGUILayout.PropertyField中第二个参数一定要赋值为true,不然序列化属性的子对象无法显示。
2.点击控制标题——折叠式开关的开启与关闭:
1 public void OnGUI() 2 { 3 bShowFontWorld = EditorGUILayout.Foldout(bShowFontWorld, "Font Word", true, EditorStyles.foldoutPreDrop); 4 5 if (bShowFontWorld) 6 { 7 //折叠开关开启时需要显示的内容 8 } 9 }
函数EditorGUILayout.Foldout会自动在显示内容左侧创建小三角Icon,四个参数意义分别是:
1.控制点击时是否显示折叠的内容,为true时显示折叠内容
2.具体的显示内容标题
3.控制开关的检测区域是否包含标题内容区域本身,还是只包含开关Icon部分,为true时全部包含
4.风格设置,这里的风格最好选择带有foldout类型的,不然无法自动创建小三角Icon
函数返回当前用户的操作——是否开启折叠内容。
3.Object拾取器:
tmpFontAsset = (TMP_FontAsset)EditorGUILayout.ObjectField("Font Asset", tmpFontAsset, typeof(TMP_FontAsset),false);
最后一个参数为是否允许拾取场景中的物体,返回的是一个Object对象,需要进行强制类型转换。