You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 20:44:52