前言
其实本文倒没有什么技术含量,纯纯的记录一下这个有趣的过程
环境:Unity 2020.3
正文
今天接到了要实现图文混排和超链接需求,第一反应是基于UGUI去做,然后我就去找各种开源库实现,一开始自然是去Github找了各种开源库实现,比如排行第一的:https://github.com/coding2233/TextInlineSprite
但遗憾的是自从Unity2019更改了UIText顶点构建策略之后,几乎所有网上现有的实现都失效了(反正我是基本上尝试了每一个看起来靠谱的仓库)
比如这个仓库的超链接功能
其中绿色矩形为超链接点击区域的可视化Debug,可以发现,当UIText无法显示完全所有文本时,碰撞检测是正常的,但当UIText可以完全显示所有文本时碰撞检测又是错乱的
当然不仅如此,其图文混排也是有问题的
第一个表情显示正常,第二个表情就直接乱码了,归根到底还是因为这个仓库的实现已经不适配新版本Unity的顶点构建策略了
怎么办呢?我都想硬着头皮读作者的源码然后一点点修复了,想想都觉得头疼,涉及到DSL(自定义的富文本语言)解析,顶点重计算,Sprite占位符计算,文本换行支持。。。
好在一位群友提醒我TextMeshPro做这些功能十分方便,让我先去了解了解再做决定也不迟,事实正如他所说,简直不要太方便
基于TMP的图文混排和超链接实现
首先先了解下什么时TextMeshPro:https://www.raywenderlich.com/22175776-introduction-to-textmesh-pro-in-unity#toc-anchor-001 (推荐大家收藏这个网站,上面的教程含金量很高,并且十分适合初学者!)
制作自定义TMP字体
跟着上面的教程就能制作出自己的TMP字体了,关于支持中文的TMP操作方法可参见:https://zhuanlan.zhihu.com/p/375889482 (需要注意的是,在创建自定义TMP字体时,需要将原字体改成英文名,否则会有类似:Font Asset Creator - Error Code [Invalid_File_Path] has occurred trying to load the [DPCOMIC] font file. This typically results from the use of an incompatible or corrupted font file.
的报错)
当然了很多字体库支持的字符数量可能不全,所以需要制作自定义的TMP字体,制作方法同样参照上面的两个链接,字符的来源Github上也有:
超链接
非常的简单,只需要输入形如:
1 <link="http://www.lfzxb.top">个人网站</link >
的内容就已经相当于输入了一个超链接了,但是我们发现点击这个超链接并没有任何反应,这是因为我们还没有针对它进行事件监听操作,直接新建如下Mono脚本挂载到TMP_Text归属的GameObject上即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 using TMPro;using UnityEngine;using UnityEngine.EventSystems;[RequireComponent(typeof(TMP_Text)) ] public class LinkOpener : MonoBehaviour , IPointerClickHandler { public void OnPointerClick (PointerEventData eventData ) { TMP_Text pTextMeshPro = GetComponent<TMP_Text>(); int linkIndex = TMP_TextUtilities.FindIntersectingLink(pTextMeshPro, eventData.position, null ); if (linkIndex != -1 ) { TMP_LinkInfo linkInfo = pTextMeshPro.textInfo.linkInfo[linkIndex]; Application.OpenURL(linkInfo.GetLinkID()); } } }
图文混排
我去摆度了一下,都说TMP图集的制作需要用到TexturePacker来辅助,就像这个:https://blog.csdn.net/qq_37057633/article/details/81120583
嗯,有理有据,我甚至都准备学习静兄的自动化工具了:https://www.jingfengji.tech/2019/08/09/unity-bian-ji-qi-tuo-zhan-zhi-er-shi-qi-textmeshpro-de-tmp-spriteasset-tu-wen-hun-pai-tu-ji-kuai-jie-geng-xin-gong-ju/
但“懒狗”之心促使我再去谷歌了一下,果然有更加简单的方法:https://forum.unity.com/threads/new-textmesh-pro-sprite-asset-importer-data-source.571123/ 即直接通过Assets->Create->TextMeshPRO->Sprite Asset创建即可
TMP性能优化
字体纹理压缩
由于TMP导出的字体图集格式是未压缩的,所以对于4096 * 4096的纯Alpha通道来说就是16MB的内存占用,所以考虑做一个工具,对图集进行压缩
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 public class TMPPostProcessorWindow : OdinEditorWindow { public class TMPPostProcessorSetting { [LabelText("TMPAsset图集资产" ) ] public TMP_FontAsset TMPFontAsset; [LabelText("最大分辨率" ) ] public int MaxSize = 4096 ; [LabelText("压缩格式" ) ] public TextureImporterFormat TextureImporterFormat = TextureImporterFormat.ASTC_6x6; [LabelText("压缩质量" ) ] public TextureCompressionQuality TextureCompressionQuality = TextureCompressionQuality.Best; } [LabelText("要后处理的TMP字体文件" ) ] public List <TMPPostProcessorSetting > TMPPostProcessorSettings = new List<TMPPostProcessorSetting>(); [ToolStorehouse("TMP字体图集优化工具" , ToolStorehouseAttribute.Category.UI, false) ] private static void ShowTMPPostProcessorWindow ( ) { var tmpWindow = GetWindow<TMPPostProcessorWindow>(); tmpWindow.Show(); } [Button("一键优化" , ButtonSizes.Medium) ] public void Execute ( ) { foreach (var tmpFontAsset in TMPPostProcessorSettings) { string fontPath = AssetDatabase.GetAssetPath(tmpFontAsset.TMPFontAsset); string texturePath = fontPath.Replace(".asset" , ".png" ); TMP_FontAsset targeFontAsset = tmpFontAsset.TMPFontAsset; Texture2D texture2D = new Texture2D(targeFontAsset.atlasTexture.width, targeFontAsset.atlasTexture.height, TextureFormat.Alpha8, false ); Graphics.CopyTexture(targeFontAsset.atlasTexture, texture2D); byte [] dataBytes = texture2D.EncodeToPNG(); FileStream fs = File.Open(texturePath, FileMode.OpenOrCreate); fs.Write(dataBytes, 0 , dataBytes.Length); fs.Flush(); fs.Close(); AssetDatabase.Refresh(); texture2D = AssetDatabase.LoadAssetAtPath<Texture2D>(texturePath); TextureImporter textureImporter = AssetImporter.GetAtPath(texturePath) as TextureImporter; textureImporter.textureType = TextureImporterType.Default; TextureImporterPlatformSettings androidSetting = textureImporter.GetPlatformTextureSettings("Android" ); androidSetting.overridden = true ; androidSetting.format = tmpFontAsset.TextureImporterFormat; androidSetting.maxTextureSize = tmpFontAsset.MaxSize; androidSetting.compressionQuality = (int ) tmpFontAsset.TextureCompressionQuality; TextureImporterPlatformSettings iosSetting = textureImporter.GetPlatformTextureSettings("iPhone" ); iosSetting.overridden = true ; iosSetting.format =tmpFontAsset.TextureImporterFormat; iosSetting.maxTextureSize = tmpFontAsset.MaxSize; iosSetting.compressionQuality = (int ) tmpFontAsset.TextureCompressionQuality; textureImporter.SetPlatformTextureSettings(androidSetting); textureImporter.SetPlatformTextureSettings(iosSetting); textureImporter.mipmapEnabled = false ; textureImporter.textureType = TextureImporterType.Default; textureImporter.SaveAndReimport(); AssetDatabase.RemoveObjectFromAsset(targeFontAsset.atlasTexture); targeFontAsset.atlasTextures[0 ] = texture2D; targeFontAsset.material.mainTexture = texture2D; } AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); } }
目前实践下来ASTC_6X6
压缩格式为兼顾效果和内存较好的一个压缩格式,将原本16MB的贴图压缩到7MB
字体纹理合并
如果有一堆生僻字被多个TMP字体所使用,就会生成多个字体纹理
如果可以将这些字体纹理合并成一个图集,然后每个TMP字体修改对unicode索引的UV与范围,就能达到常规UI图片图集的优化效果(合批,减少DrawCall)
核心思路与步骤如下:
使用工具合并多个TMP字体的纹理
剔除每个TMP字体的Material,新建一个公用的Material并引用新合并的那个纹理作为_MainTex
,这些TMP字体都引用这个公用的Material
剔除每个TMP字体的AtlasTexture,都引用新合并的那个纹理
修改TMP Asset的atlasWidth和atlasHeight,数值为合并纹理的分辨率,修改此处的原因是这两个参数会被当成UV范围的缩放,所以需要同步更新
修改TMP Asset的glyphTable中的每个元素的glyphRect的x和y属性,数值为其原本纹理在新的合并纹理中的位置(偏移),这个偏移值可以通过开启Sprite的Multiple模式,获取其中的子Sprite的textureRect属性来获取
这些步骤完成后,即可达到一次DrawCall渲染多个TMP字体的目的,需要注意的是,当这些经过图集合并TMP字体被用于其他TMP的FallBack字体时,将无法进行合批,这是因为不同的主字体所使用的材质球不一样,所以无法合批,而Fallback字体又位于主字体渲染后,所以Fallback字体就算是经过合并工具处理过,也无法进行合批
总结
可以看到,通篇没有什么技术含量,但是我希望大家能看到更深层次的东西——技术调研与选型
如果我一开始直接就认死UGUI,后果可想而知,没有一星期基本上是做不完这些需求的,如果我选择了TMP然后按照百度上普遍的做法去处理,那估计配环境,自动化集成也要花个一两天才行
但是后面通过多方搜集资料发现整个工作流都可以被简化,变相节约了相当多的时间,比如技术调研A花了半小时,技术调研B花了3小时,看上去是A更加高效,但是由于A信息搜集的不够完全,其做法完全可能是落后的,耗时的,而B调研的比较到位,选择了更加先进简单的方案,那么A就要为这眼前领先的两个半小时付出十倍,数十倍的代价。
所以技术调研做全面真的很重要,他节约的是未来的时间。
其他
我在看 https://github.com/coding2233/TextInlineSprite 中解析DSL富文本的时候意外发现了两个在线正则表达式编译和可视化网站,非常好用,凡是看不懂的正则往里一扔,立马明明白白