☰
关于打字机逐字显示的性能优化问题
2026/10/30 2:44:21 网站建设 项目流程
private IEnumerator TypeText(TextMeshProUGUI text) { text.text = ""; foreach (char c in curDialogues.dialogues[curDialogueNum].dialogueContent) { text.text += c; yield return new WaitForSeconds(dialogueDelay); } continueButton.SetActive(true); }

打字机逐字显示对话内容,我使用了协程,在这里有几个问题:

1.每多显示一个字的时候,字符串都要更新一次,c#的string是不可变的,text+=c的时候不是简单追加,而是把旧的整个字符串拷贝过去再加上这个字符。

2.每次给text赋值的时候,tmp都要重新解析文本重新进行排版,计算换行和字的位置。

3.new了很多对象,每个字符都new了一个WaitForSeconds,全变成垃圾等GC回收。

三个开销,全都在主线程上,而 GC 在 Unity 里是主线程执行的——一旦触发,那一帧就要停下来回收。60 帧的预算是 16.7 毫秒,一次 GC 就可能吃掉几毫秒。

而对于这三个问题的解决:

1.改为一次性text.text=content,字符串只构造一次,不再产生新的string。

2.用maxVisibleCharacter控制显示进度,文本内容不变,tmp不需要重新解析和排版。

3.循环外new一次WaitForSeconds,循环内复用。

private IEnumerator TypeText(TextMeshProUGUI text) { string content = curDialogues.dialogues[curDialogueNum].dialogueContent; text.text = content; //问题1,字符串只构造一次 text.maxVisibleCharacters = 0; //问题2,字符全部不显示 text.ForceMeshUpdate(); //问题2,主动排一次版 int total = text.textInfo.characterCount; //问题2,按可见字符数算 var wait = new WaitForSeconds(dialogueDelay); //问题3,全程只分配一次 for (int i = 1; i <= total; i++) { text.maxVisibleCharacters = i; //问题2,每次增加一个字显示效果 yield return wait; //复用,不再new } continueButton.SetActive(true); }

关于string:string是不可变的

例:string s = "abc";
s += "d";

看似是追加,但实际上是s指向了一个全新的字符串对象,abc那个对象还在内存中,只是没人用了等待被回收。

所做的事:

1.分配一个长度为4的新char[]

2.把abc三个字符拷贝过去

3.把d放进第四位

原因:

1.线程安全,字符串可以被任意多线程共享

2.相同的字符串只存一份,节省内存

3. 哈希值可缓存——Dictionary 的 key 大量用 string,如果可变,哈希就失效了

4.避免别名问题

TMP的原理:

1.旧版text的原理从字体图集里取字,但那张图是普通位图,所以才会放大就很糊。

TMP 用的是 SDF 字体图集:每个字符烘焙到图集的一块矩形区域,存的不是颜色,而是每个像素到字形边缘的距离。渲染时由 shader 用这个距离算出边缘和透明度,所以任意字号都清晰,还能做描边和发光。

2.tmp拿到字符串后,会先解析,例如,<color=red>你好</color>,再按每个字去图集里找到对应的uv区域,再按字宽,字据,对齐规则排版,算出每个字的位置和换行。

3.将排版存进textInfo,

characterInfo[]——每个字的位置、大小、UV、可见性
lineInfo[]——每一行的起止和基线
meshInfo[]——顶点和三角形

然后每个字符生成一个四边形,uv指向图集里字符的矩形(4个顶点,2个三角形),最后交给Canvas渲染,材质用SDF shader使边缘平滑。

因此,修改 text 会触发重新解析、重新排版、重建 mesh 和 Canvas 重建,所以相对较贵;而修改 maxVisibleCharacters 只影响字符可见性,不触发重排,所以适合做打字机效果。

WaitForSeconds 为什么可以复用

new WaitForSeconds(t) 是在堆里分配一个对象,里面只存着要等待多少秒。

协程 yield return 它的时候,调度器读出这个值,算出什么时候恢复协程——这个对象本身不保存“这一次等待”的任何状态,它只是一张说明书。

所以它可以被反复 yield。

但前提是每次等待时长固定。如果时长会变,就不能共用。

这些问题都指向同一个:GC

分配 → 变成垃圾 → 堆增长到阈值 → 触发 GC → 主线程停下来回收 → 帧时间尖峰

GC 为什么会“卡”?
Unity 默认用的是 Boehm GC:非分代、stop-the-world。
意思是:GC 一开始,整个游戏逻辑都得停,等它标记、清除、压缩完内存才能继续。
一次 GC 可能几毫秒。而 60 帧的预算是 16.7 毫秒——一次 GC 就能吃掉小半帧。

所以性能优化需要减少分配。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询