前言

其实本文倒没有什么技术含量,纯纯的记录一下这个有趣的过程

环境:Unity 2020.3

正文

今天接到了要实现图文混排和超链接需求,第一反应是基于UGUI去做,然后我就去找各种开源库实现,一开始自然是去Github找了各种开源库实现,比如排行第一的:https://github.com/coding2233/TextInlineSprite

但遗憾的是自从Unity2019更改了UIText顶点构建策略之后,几乎所有网上现有的实现都失效了(反正我是基本上尝试了每一个看起来靠谱的仓库)

比如这个仓库的超链接功能

image-20220210184603520

其中绿色矩形为超链接点击区域的可视化Debug,可以发现,当UIText无法显示完全所有文本时,碰撞检测是正常的,但当UIText可以完全显示所有文本时碰撞检测又是错乱的

当然不仅如此,其图文混排也是有问题的

image-20220210184850164

第一个表情显示正常,第二个表情就直接乱码了,归根到底还是因为这个仓库的实现已经不适配新版本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 you are not in a Canvas using Screen Overlay, put your camera instead of null
if (linkIndex != -1)
{
// was a link clicked?
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)

核心思路与步骤如下:

  1. 使用工具合并多个TMP字体的纹理
  2. 剔除每个TMP字体的Material,新建一个公用的Material并引用新合并的那个纹理作为_MainTex,这些TMP字体都引用这个公用的Material
  3. 剔除每个TMP字体的AtlasTexture,都引用新合并的那个纹理
  4. 修改TMP Asset的atlasWidth和atlasHeight,数值为合并纹理的分辨率,修改此处的原因是这两个参数会被当成UV范围的缩放,所以需要同步更新
  5. 修改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富文本的时候意外发现了两个在线正则表达式编译和可视化网站,非常好用,凡是看不懂的正则往里一扔,立马明明白白