Unity视觉小说文本渲染GC过载、帧率降低及高内存占用问题求助
视觉小说文本渲染内存暴涨与性能下降问题排查
我正在开发一款视觉小说,随着游戏进程推进,文本渲染速度越来越慢,帧率不断降低。用Unity Profiler检测发现内存占用高达2GB,这对视觉小说来说过于夸张。
文本渲染函数代码
private IEnumerator Build_Fade() { int minRange = pre_text_len; int maxRange = minRange + 1; byte alphaThreshold = 15; TMP_TextInfo textInfo = tmpro.textInfo; Color32[] vertexColors = textInfo.meshInfo[textInfo.characterInfo[0].materialReferenceIndex].colors32; float[] alphas = new float[textInfo.characterCount]; while (true) { float fadeSpeed; if (hurryup) { fadeSpeed = character_per_cycle * 5 * speed * 4f; } else fadeSpeed = character_per_cycle * speed * 4f; for (int i = minRange; i < maxRange; i++ ) { TMP_CharacterInfo characterInfo = textInfo.characterInfo[i]; if (!characterInfo.isVisible) continue; int vertexIndex = textInfo.characterInfo[i].vertexIndex; if(i >= alphas.Length) { alphas = new float[textInfo.characterCount]; } alphas[i] = Mathf.MoveTowards(alphas[i], 255, fadeSpeed); for (int v = 0; v < 4; v++) { vertexColors[characterInfo.vertexIndex + v].a = (byte)alphas[i]; } if (alphas[i] >= 255) { minRange++; } } tmpro.UpdateVertexData(TMP_VertexDataUpdateFlags.Colors32); bool lastCharacterInvisible = !textInfo.characterInfo[maxRange - 1].isVisible; try { if (alphas[maxRange - 1] > alphaThreshold || lastCharacterInvisible) { if (maxRange < textInfo.characterCount) { maxRange++; } else if (alphas[maxRange - 1] >= 255 || lastCharacterInvisible) { DIALOGUE.ConversationManager.userPrompt = true; break; } } } catch { Clear(); } yield return new WaitForEndOfFrame(); } }
补充说明
- try-catch块仅用于处理快速点击时偶发的索引越界错误,核心性能问题在添加该块前就已存在。
- 本人对C#内存管理及GC的作用了解有限,特此求助解决该问题。
- Unity Profiler内存追踪显示内存占用异常(总内存达2GB,GC Alloc持续增长)
核心问题分析与优化方案
1. 频繁内存分配触发GC压力
- 问题点:循环内可能反复创建
float[] alphas数组(当i >= alphas.Length时),每次分配都会产生GC垃圾,积累后导致内存暴涨。 - 优化:初始化时直接创建足够大的数组,提前预判文本长度,避免中途重建。比如在协程启动前获取最终文本长度,一次性创建
alphas数组;或用List<float>提前扩容,减少数组拷贝开销。
2. 每帧全量更新顶点数据
- 问题点:每帧都调用
tmpro.UpdateVertexData(TMP_VertexDataUpdateFlags.Colors32),即使只有少数字符alpha变化,也会更新整个文本的顶点数据,造成不必要的性能浪费。 - 优化:仅更新当前处理的字符对应的顶点区域,或累积多帧变化后批量更新。TMP支持针对单个字符的顶点更新,可记录需要更新的字符范围,一次性提交修改。
3. 循环逻辑冗余与索引风险
- 问题点:循环内重复获取
textInfo.characterInfo[i],增加不必要的数组访问开销;maxRange的判断逻辑存在索引越界风险,导致try-catch频繁触发(额外性能开销)。 - 优化:
- 循环内仅获取一次
TMP_CharacterInfo characterInfo = textInfo.characterInfo[i],复用该对象。 - 提前校验
maxRange边界,比如判断alphas[maxRange - 1]前先检查maxRange - 1 < alphas.Length,避免进入异常处理流程。
- 循环内仅获取一次
4. 多协程运行导致内存泄漏
- 问题点:快速切换对话时,若旧的
Build_Fade协程未终止,多个协程会同时运行,每个协程持有vertexColors、alphas等引用,导致内存无法释放。 - 优化:启动新协程前,用
StopCoroutine停止旧的Build_Fade协程;在Clear()方法中确保释放相关引用或重置数组。
优化后的代码示例
private Coroutine currentFadeCoroutine; public void StartTextFade() { // 启动新协程前停止旧的,避免多协程并行 if(currentFadeCoroutine != null) StopCoroutine(currentFadeCoroutine); currentFadeCoroutine = StartCoroutine(Build_Fade()); } private IEnumerator Build_Fade() { int minRange = pre_text_len; int maxRange = minRange + 1; byte alphaThreshold = 15; TMP_TextInfo textInfo = tmpro.textInfo; int materialIndex = textInfo.characterInfo[0].materialReferenceIndex; Color32[] vertexColors = textInfo.meshInfo[materialIndex].colors32; // 一次性创建足够大的数组,避免中途重建 float[] alphas = new float[textInfo.characterCount]; // 预初始化所有alpha为0 for(int i=0; i<alphas.Length; i++) alphas[i] = 0f; while (true) { float fadeSpeed = hurryup ? character_per_cycle * 5 * speed * 4f : character_per_cycle * speed * 4f; bool needUpdate = false; for (int i = minRange; i < maxRange; i++ ) { if(i >= textInfo.characterCount) break; // 提前边界检查 TMP_CharacterInfo characterInfo = textInfo.characterInfo[i]; if (!characterInfo.isVisible) continue; float oldAlpha = alphas[i]; alphas[i] = Mathf.MoveTowards(oldAlpha, 255f, fadeSpeed); // 仅当alpha变化时更新顶点,减少无效操作 if(Mathf.Abs(alphas[i] - oldAlpha) > 0.1f) { byte newAlpha = (byte)alphas[i]; int vertexIndex = characterInfo.vertexIndex; for (int v = 0; v < 4; v++) { vertexColors[vertexIndex + v].a = newAlpha; } needUpdate = true; } if (alphas[i] >= 255f - 0.1f) // 浮点精度容错 { minRange++; } } // 仅在有修改时提交顶点数据 if(needUpdate) { tmpro.UpdateVertexData(TMP_VertexDataUpdateFlags.Colors32); } // 边界校验,避免索引越界 if(maxRange > textInfo.characterCount) { maxRange = textInfo.characterCount; } bool lastCharacterInvisible = maxRange > 0 && !textInfo.characterInfo[maxRange - 1].isVisible; bool lastAlphaReady = maxRange > 0 && alphas[maxRange - 1] > alphaThreshold; if (lastAlphaReady || lastCharacterInvisible) { if (maxRange < textInfo.characterCount) { maxRange++; } else { bool allDone = lastCharacterInvisible || (alphas[maxRange - 1] >= 255f - 0.1f); if(allDone) { DIALOGUE.ConversationManager.userPrompt = true; break; } } } yield return null; // 替换WaitForEndOfFrame,减少等待开销(可根据需求调整) } currentFadeCoroutine = null; }
额外建议
- 使用Unity Profiler的Memory面板,查看具体对象的内存占用情况,排查是否有未释放的TMP文本对象、协程引用等。
- 对话切换时及时清理旧的文本对象,避免持有TMP组件的长期引用。
- 考虑用对象池复用文本对象,而非每次对话都创建新的TMP组件。
内容的提问来源于stack exchange,提问作者mary jane
相关产品推荐
相关产品推荐

