Unity琐碎(3) UGUI 图文混排解决方案和优化
感觉使用Unity之后总能看到各种各样解决混排的方案,只能说明Unity不够体恤下情啊。这篇文章主要讲一下个人在使用过程中方案选择和优化过程,已做记录。顺便提下,开源很多意味着坑,还是要开实际需求。
1. 方案选择
1 TextMeshPro
Unity 最近公布收购了TextMeshPro并且免费开源给大家使用,估计还需要几个小版本才会完全融合到Unity中或者保持现在的状态。TextMeshPro支持效果丰富,兼容现在UI层级等,性能也可以满足移动端,但是很纠结的是:
- 现在的版本生成的字体库实在太大了,比较全的汉字字库生成TextMeshPro需要的字库之后已经接近17M,如果考虑到游戏中存在2种字体,估计会超过20M的常驻内存在移动端,这个至少现在还很难接受。
- 另外一个文字是序列化,20M的序列化数据,移动端受IO限制,读取时间会有点长。这个时间长度我没仔细测试过
基于上面亮点,最后我还是没有采用这种方案,如果Unity考虑融合进来,建议修改下字库的使用方式。
2 文字和图片独立渲染
- 文字和图片采用layout的方式控制渲染位置,最后会生成大量的Text和Sprite,实时计算位置信息,比如:RichText,这里面最大的问题可能会再CPU端造成不必要的浪费。
- 文字中留空间,图片再这个空间单独渲染。Text支持富文本的时候
会控制间隔,利用这个间隔提供图片位置信息,然后单独渲染图片位置。这种方案Text不需要实时更新,图片(带有动态)需要实时过更新。可以参考这里 - Shader中渲染图片:Uwa UGUI表情系统解决方案 直接再shader中渲染图片,这个方案对于outline、shadow时避免图片也被处理的问题没想到好的方案,就放弃了。
最后采用了文字富文本保留空间,图片根据位置单独渲染的方案,主要的原因在于性能可控以及现在代码还算比较完善(这里完全是个坑)。这个版本最初的源码:https://code.csdn.net/qq992817263/uguitextpro/tree/master
2. 基本原理
2.1 基本思想
- 利用Text富文本占位符为图片保留位置、图片名字、长宽等信息,通过字符解析获取图片相关信息
- 监听Text重绘以及位置更新等事件,并更新图片位置
参考文章
Unity Text 插入图片,这篇文章是基本的实现方式,后面CSDN“神码编程”也就在这基础上做了几处扩展和一些文章分享
2.2 代码实现思路
-
提前生成sprite区域信息,如果是一个系列的表情则根据sprite名字进行区分,当然后面也根据名字进行保留和查找。如angry_0\angry_1\angry_2\angry_3 , die_0/die_1/die_2/die_4/die_5/die_6
-
继承Text组件,重写OnPopulateMesh以及字符解析,维护里面图片位置、顶点等信息
-
表情管理器:记录所有Text中图片(有效的)位置、纹理、顶点信息的索引关系,由数据变化时生成需要的Mesh信息并提交
-
SpriteAsset 管理器:管理图片中所有Text中使用的图片资源加载以及sprite位置、名字信息。
3. 爬坑记录
最初的源码看似可用,但是在手机端ListView滚动情况下直接掉到20帧一下,即使在静态100个表情同时更新的境况下效率也很难令人满意。所以.................差不多用了一周时间爬各种坑,下面是一些主要的记录:
3.1 优化内容
(1) GC
代码中在解析字符中基本每次都在new数据,包括解析字符、计算图片位置、更新图片Mesh等都存在很严重的GC,看上图就可以看到滚动中如果频繁创建的问题。
优化思路:
- 对于每个Text,限制最大图片数量以及相关结构数量,只有在不够的时候再进行分配(不超过最大数量则),后续使用中不再进行分配,当然增加了数据有效性判断而不是是否为空。
- 对于图片管理Mesh,则管理器中图片总数量提前创建,只有再发生变化时才会重新进行内存分配。现在使用的策略还需优化。
(2) 图片信息查找
启动时读取配置信息,并简历sprite名字和信息的对应Dictionary,加快查找。当然也可以直接以Dictionary结构进行序列化,就可以节省这部分空间和时间,待优化。
(3) 有效图片更新方式
原始版本中有效Sprite 列表时通过List的形式进行管理,每次任一个Text的变化(enabled,posotion等)都会将这个列表清除并重新将有效Text中的有效Sprite添加到列表中来。这种方式如果在类似ListView等一直会变化的组件中就会产生不必要的CPU开销。
优化思路:
- 维护一个有效Text的Dictionary,保存Text中对应Sprite的Key值,在Text OnEnable/OnDisable中进行注册和注销操作
- 维护一个有效Sprite的Dictionary,保存Sprite string以及实际信息。
- 每次有Text改变时只修改Sprite 键值表中对应的部分,当然也考虑Text注销等情况。
这种方式避免在频繁更新中不必要的列表清除操作以及对SpriteManager lateUpdate的影响
(4) 图片Mesh数据更新过程时间
最初的版本采用对SpriteList遍历的形式逐个将triangles、uv、vertices 赋值到新创建的缓存中,再扔给iMesh去提交。在ListView快速移动时这部分的时间占用就很夸张了。
优化思路:
- 尽量减少无效sprite进入列表,限制每个Text中sprite的最大数量
- 采用Array.Copy的形式替代逐个赋值
(5) 占位符乱码清除方式
原始版本可能时作者计算错误了,清除乱码的UV位置其实只需要向后4个即可,但是也原始版本是按4 * Length(标签长度)来计算,这项的CPU占用率特别高。
(6) 动态表情更新方式
原始版本时在SpriteUpdate中每隔固定时间更新表情的索引(如果有)并重新更新Sprite Mesh内容。会产生一个问题:每种类型表情动画图片的数目不一样,那就很难保证每个动态表情都很自然的播放。提高更新的间隔意味着有些表情像发飙一样
优化思路:
每类型的表情中单独存放其时间间隔以及已经运行的时间,在Update中根据各自的情况进行更新。
(7)图片位置更新方式
原始代码中是在Text :SetVerticesDirty()中进行ParseText的操作并依赖SpriteManager中LaterUpdate更新图片的Mesh数据,产生的问题:
- SetVerticesDirty 是Text 任何变化都会调用接口,意味着ParseText的操作在ListView滚动过程中一直在进行。
- SpriteManager中LaterUpdate更新与Text位置变化不同步,滚动时很明显的可以看到sprite的位置偏移
优化思路:
- ParseText只在text文本内容变化时进行更新,可通过重载Text的text属性实现
- 在ListView滚动过程中 sprite变化的只有位置信息,所以只更新位置即可,并且直接更新MESH,不等待SpriteManager。
(8)其他
对应的还有编辑器、数据结构、贴图资源管理等的优化
3.2 新增功能
(1)支持简化标签
支持 "[xxxxx]"来替代
(2)图片层级管理
方便单个Canvas下多个层级,让Text 可以直接设置SpriteManager或者找最近的一个。
(3)增加文本与图片间隔设置
3.3 待优化内容
(1)下划线解析和超链接解析都是基于字符位置对应实际字符顶点位置
(2)字符串解析
(3)图片Mesh
(4)多张sprite Asset
3.4 优化效果
测试方式,屏幕中160个动画表情的情况,在ListView中快速滚动下进行测试的性能曲线(主要时CPU);
优化前
优化后
原生Text, 有占位符,无表情
4 小结
采用这种方案各种原因都有,有好处也有弊端,就像层级问题,解决起来会有点头痛。经过一段时间优化勉强可以在移动端满足需求,不过还有很多可以继续优化的空间。
GITHUB工程文件:https://github.com/carlosCn/Unity-EmojiText
百度网盘资源:http://pan.baidu.com/s/1geZuVNd
欢迎继续补充。