Unity弧形文本实现:从顶点变换到性能优化的完整指南

📅 2026/7/18 17:12:59
Unity弧形文本实现:从顶点变换到性能优化的完整指南
1. 项目概述为什么弧形文本值得投入在Unity UI开发里处理文字算是个高频且基础的需求。默认的Text或TextMeshPro组件文字都是规规矩矩地横平竖直排列。但当你接到一个需求比如要做一款赛博朋克风的HUD界面需要让状态信息沿着雷达屏幕的弧形边缘显示或者做一个RPG游戏的技能轮盘技能名称需要环绕在角色周围又或者是一个抽卡动画卡牌名称需要有一个优雅的弧线弹出效果——这时候标准的水平或垂直排列就完全不够用了。“弧形文本”这个需求本质上是对UI元素进行非线性的空间变换。它不是一个现成的开关点一下就能实现。网上能找到的现成插件或方案要么功能过于庞杂要么定制性不够无法完美融入你自己的项目美术风格和性能预算。更重要的是不理解其背后的原理一旦效果出了问题或者需要做一些动态调整比如弧度随着玩家等级变化排查和修改都会非常困难。所以自己动手实现一套弧形文本系统远不止是完成一个特效。它是一个深入理解Unity UI渲染流程、网格操作以及性能优化思想的绝佳切入点。从最基础的顶点变换开始到支持动态变化、适配不同排版需求再到最后针对移动端或复杂场景的性能调优整个过程会让你对“UI如何被画出来”这件事有全新的认识。接下来我会把我从零搭建、踩坑、再到优化的完整经验分享出来你可以把它看作一个可复用的框架也可以当作学习UI底层知识的实战教程。2. 核心原理与方案选型顶点变换是唯一真理在动手写代码之前我们必须搞清楚Unity是如何渲染一个UI文本的。无论是UGUI的Text还是更强大的TextMeshPro最终呈现在屏幕上的文字都是由一个个“四边形”两个三角形构成的网格组成的每个四边形对应一个字符。这些四边形默认按照我们输入的字符串顺序从左到右或根据排版规则水平排列。2.1 弧形变换的本质修改顶点位置要让文字变弯我们无法直接命令文字“弯曲”而是需要操纵组成每个字符的那个四边形的四个顶点的空间位置。这就是最核心的原理顶点变换。想象一下你有一串用线穿起来的方形小积木每个积木是一个字符它们原本平铺在桌面上。现在你想让它们沿着一个圆环内侧摆放。你需要做的就是计算每个积木应该处于圆环的哪个角度然后根据这个角度重新确定积木中心点的位置并可能还需要旋转积木让它始终“面向”圆心或切线方向。这个过程就是我们对UI文本网格要做的事情。因此所有弧形文本的实现方案无论包装得多么花哨底层都逃不开获取文本网格的顶点数据然后根据一个数学公式通常是圆形或贝塞尔曲线重新计算这些顶点的位置。2.2 方案选型继承与组件明确了原理接下来就是选择实现路径。主流有两种方式方案一继承并扩展TextMeshPro或Text组件这是功能最强大、最彻底的方式。你可以创建一个新的类例如ArcTextMeshPro继承自TMP_Text。然后重写其GenerateTextMesh或相关的方法在文本网格生成的最后阶段介入顶点数据并进行变换。这种方式的优点是深度集成你可以控制文本生成的完整流程包括字符间距、字距调整等实现最精细的控制。TextMeshPro本身性能优异功能丰富是商业项目的首选。但缺点是入门门槛稍高需要你对TMP的内部流程有一定了解。方案二编写独立的MonoBehaviour后处理组件这种方式更为轻量和灵活。你创建一个普通的脚本例如ArcTextEffect挂载到已有的TextMeshPro或Text游戏对象上。该脚本在OnEnable、Start或Update中通过GetComponent()获取文本组件然后访问其mesh属性来获取顶点数组修改后再赋值回去。这种方式原理直观与原有组件解耦方便附加或移除。特别适合用于实现动态变化的效果比如在协程中不断改变弧度。缺点是它属于“后处理”每帧或每次文本更新时都需要重新计算和上传网格数据如果处理不当可能带来额外的性能开销。对于大多数从学习到实战的场景我强烈推荐从方案二开始。它直观地将“效果”与“文本内容”分离更符合Unity的组件化思想也便于我们一步步添加功能。本文的后续实现也将基于此方案展开。当我们吃透了方案二理解了性能瓶颈所在再迁移到方案一以获得终极性能就是水到渠成的事了。注意无论选择哪种方案请务必使用TextMeshPro而非旧版Text。TMP在字体渲染、国际化支持、尤其是性能方面它使用了动态图集和更高效的网格合并具有压倒性优势。旧版Text在复杂效果下更容易出现性能问题和渲染瑕疵。3. 基础实现手把手打造静态弧形文本让我们从一个最简单的静态弧形文本开始。假设我们要让文字均匀地分布在一个给定半径的圆弧上。3.1 创建基础组件结构首先在Unity中创建一个空物体挂上TextMeshPro - Text (UI)组件输入一些测试文字比如“Hello, Arc World!”。然后我们创建一个C#脚本ArcTextEffect.cs。using TMPro; using UnityEngine; using UnityEngine.UI; [RequireComponent(typeof(TextMeshProUGUI))] public class ArcTextEffect : MonoBehaviour { private TextMeshProUGUI _tmpText; private Mesh _mesh; // 弧形参数 public float radius 100f; // 圆弧半径 public float arcAngle 90f; // 弧形角度度 public bool alignToRadius true; // 字符是否朝向圆心 void Start() { _tmpText GetComponentTextMeshProUGUI(); UpdateArc(); } void OnValidate() { // 当在Inspector中修改参数时立即更新效果仅在编辑器下 if (_tmpText ! null Application.isEditor !Application.isPlaying) { UpdateArc(); } } public void UpdateArc() { if (_tmpText null) return; // 强制生成文本网格并更新几何信息 _tmpText.ForceMeshUpdate(); // 获取当前文本的网格信息 Mesh mesh _tmpText.mesh; Vector3[] vertices mesh.vertices; // 计算文本的总宽度世界空间 float totalWidth _tmpText.GetPreferredValues().x; // 计算每个字符的角位移 float anglePerChar arcAngle / Mathf.Max(_tmpText.textInfo.characterCount - 1, 1); // 弧度的起始角度从左侧中间开始 float startAngle -arcAngle / 2f; // 遍历每一个字符信息 for (int i 0; i _tmpText.textInfo.characterCount; i) { TMP_CharacterInfo charInfo _tmpText.textInfo.characterInfo[i]; // 跳过空格等不可见字符 if (!charInfo.isVisible) continue; // 获取当前字符在原始水平布局下的“中心”X偏移相对于整个文本的起点 // 这里用一个简化的估算使用字符信息中的顶点位置 int vertexIndex charInfo.vertexIndex; float charCenterX (vertices[vertexIndex].x vertices[vertexIndex 2].x) / 2f; // 将水平偏移映射为角度偏移 // 注意这里假设字符是等宽分布的简化计算更精确的做法需要使用字符的实际排版信息 float t charCenterX / totalWidth; // 归一化位置 [0, 1] float currentAngle startAngle arcAngle * t; // 映射到角度范围 // 将角度转换为弧度 float rad currentAngle * Mathf.Deg2Rad; // 计算该角度在圆弧上的位置 Vector3 centerOffset new Vector3(Mathf.Sin(rad), -Mathf.Cos(rad), 0) * radius; // 计算字符是否需要旋转 Quaternion rotation Quaternion.identity; if (alignToRadius) { // 字符朝向圆心即旋转角度与当前位置的切线方向垂直 rotation Quaternion.Euler(0, 0, -currentAngle); } // 变换这个字符的4个顶点 for (int j 0; j 4; j) { int idx vertexIndex j; // 先获取顶点相对于字符中心的本地坐标 Vector3 localOffset vertices[idx] - new Vector3(charCenterX, 0, 0); // 应用旋转如果需要 localOffset rotation * localOffset; // 应用弧形偏移 vertices[idx] centerOffset localOffset; } } // 将修改后的顶点数组赋回网格 mesh.vertices vertices; _tmpText.canvasRenderer.SetMesh(mesh); } }将脚本挂载到之前创建的TMP文本对象上。运行游戏你应该能看到文字沿着一个圆弧排列了。在Inspector中调整Radius和Arc Angle参数可以实时改变弧形的半径和张开的角度。3.2 核心计算逻辑拆解上面的代码是核心我们来拆解几个关键点获取网格与顶点_tmpText.ForceMeshUpdate()确保文本布局计算完成。_tmpText.mesh获取到的是当前文本生成的网格其vertices数组包含了所有顶点的位置注意是相对于UI RectTransform的本地坐标。字符遍历与筛选通过_tmpText.textInfo.characterCount和_tmpText.textInfo.characterInfo数组遍历每个字符。charInfo.isVisible非常重要它能帮我们跳过空格、换行符等不渲染的字符避免计算错误。位置映射关键难点如何将字符在水平方向上的位置映射到圆弧上的角度代码中使用了简化方法float t charCenterX / totalWidth;。即用字符中心的X坐标除以文本总宽度得到一个[0, 1]的归一化比例再乘以总角度arcAngle。这种方法在字符等宽或近似等宽时效果不错但对于混合了中文、英文、标点的复杂文本每个字符的宽度差异很大这种线性映射会导致字符在弧上的分布疏密不均。更精确的做法是使用TMP提供的charInfo.xAdvance属性它表示该字符之后的下一个字符的起始X位置用这个值来累积计算每个字符的“排版前进量”作为映射的依据会更准确。顶点变换对于每个字符的4个顶点我们先计算它们相对于“字符原点”我们估算的charCenterX的本地偏移localOffset。然后根据alignToRadius决定是否施加旋转让字符“站”在圆弧上。最后将旋转后的本地偏移加上圆弧上的中心位置centerOffset得到最终顶点位置。应用修改修改完vertices数组后必须将其重新赋值给mesh.vertices并调用_tmpText.canvasRenderer.SetMesh(mesh)来更新渲染器。注意直接修改_tmpText.mesh.vertices并赋值回去在某些Unity版本下可能不会立即触发渲染更新使用canvasRenderer.SetMesh是更可靠的做法。实操心得在OnValidate方法中调用UpdateArc可以让在编辑器模式下不运行游戏就能预览效果这对于调整参数、匹配美术设计至关重要。但务必加上Application.isEditor !Application.isPlaying的判断否则可能会在运行时的参数修改中产生冲突。4. 动态效果优化让弧形“活”起来静态弧形只是开始。我们更常遇到的需求是动态效果比如弧度随着时间平滑变化、文字像波浪一样滚动、或者点击后文字有一个弧线展开的动画。这就需要我们的系统能够高效、流畅地更新。4.1 实现动态弧度与半径让radius和arcAngle可以随时间变化是最基本的需求。我们只需在Update函数中修改这些参数并调用UpdateArc()即可。但这里有一个巨大的性能陷阱每一帧都调用ForceMeshUpdate()和完整的顶点变换计算开销是非常大的。优化策略脏标记与延迟更新我们不应该每帧都更新。只有当文本内容改变或者我们的弧形参数改变时才需要重新计算。为此我们引入“脏标记”机制。public class ArcTextEffect : MonoBehaviour { // ... 原有变量 ... private float _lastRadius -1f; private float _lastArcAngle -1f; private string _lastText; void Update() { bool isDirty false; // 检查参数是否变化 if (!Mathf.Approximately(_lastRadius, radius) || !Mathf.Approximately(_lastArcAngle, arcAngle)) { _lastRadius radius; _lastArcAngle arcAngle; isDirty true; } // 检查文本内容是否变化 if (_tmpText ! null _lastText ! _tmpText.text) { _lastText _tmpText.text; isDirty true; // 文本变化必然需要更新 } if (isDirty) { UpdateArc(); } } // 修改UpdateArc方法移除ForceMeshUpdate改为由外部驱动或内部条件调用 public void UpdateArc(bool forceMeshUpdate true) { if (_tmpText null) return; if (forceMeshUpdate) { _tmpText.ForceMeshUpdate(); // 只有必要时才强制更新网格 } // ... 后续顶点变换代码不变 ... } }这样只有在参数或文本实际发生变化时才会触发重计算。如果弧形参数是由动画系统Animation或Animator驱动的由于动画每帧都在修改属性Update中的脏检查会每帧都标记为isDirty这仍然是合理的更新频率。但如果你的动态效果是连续的如弧度循环变化这依然是每帧计算。4.2 实现波浪式滚动效果一个更酷的效果是让文字像波浪一样沿着弧形路径运动。这需要为每个字符引入独立的相位偏移。我们为脚本增加新的参数public float waveSpeed 1.0f; public float waveAmplitude 10.0f; // 波浪幅度 public float waveFrequency 1.0f; // 波浪频率 private float _wavePhase 0f;在Update中更新相位void Update() { _wavePhase Time.deltaTime * waveSpeed; // 原有的脏检查逻辑... if (isDirty || waveSpeed Mathf.Epsilon) { // 如果波浪效果启用每帧都需要更新 UpdateArc(waveSpeed Mathf.Epsilon); // 波浪效果时可能需要强制更新网格 } }修改UpdateArc中的顶点变换部分在计算centerOffset时加入波浪偏移// 在计算centerOffset的循环内部 float waveOffset 0f; if (waveSpeed Mathf.Epsilon) { // 为每个字符计算一个基于其位置和时间的相位 float phase _wavePhase t * waveFrequency * Mathf.PI * 2f; waveOffset Mathf.Sin(phase) * waveAmplitude; } // 波浪方向可以是径向沿半径方向或法线沿圆弧切线方向 // 这里以径向为例 Vector3 waveVector new Vector3(Mathf.Sin(rad), -Mathf.Cos(rad), 0).normalized * waveOffset; Vector3 finalCenterOffset centerOffset waveVector;这样每个字符除了基础的弧形位置还会在半径方向上有一个正弦波动的偏移形成波浪效果。waveFrequency控制波浪的密度t * waveFrequency确保了字符间有相位差。4.3 使用协程实现序列动画对于一次性的展开、收起动画使用协程Coroutine比在Update中插值更清晰。public void PlayArcAnimation(float targetAngle, float duration) { StartCoroutine(ArcAnimationRoutine(targetAngle, duration)); } private IEnumerator ArcAnimationRoutine(float targetAngle, float duration) { float startAngle arcAngle; float elapsed 0f; while (elapsed duration) { elapsed Time.deltaTime; float t elapsed / duration; // 可以使用缓动函数如Mathf.SmoothStep arcAngle Mathf.Lerp(startAngle, targetAngle, Mathf.SmoothStep(0, 1, t)); // 脏标记会检测到arcAngle变化在Update中触发UpdateArc // 或者直接在这里调用 UpdateArc(false); 如果确定文本内容不变 yield return null; } arcAngle targetAngle; // 确保最终值准确 }注意事项在动画过程中如果文本内容很长每帧计算所有顶点变换仍可能有压力。对于复杂的动画一个进一步的优化是如果仅仅是弧度、半径等统一参数在变化而字符相对位置关系不变可以预先计算好每个字符的“本地偏移”和“初始角度比例”动画时只重新计算centerOffset并叠加避免重复的三角函数和归一化计算。这需要将计算拆分为“初始化”和“更新”两个阶段。5. 高级特性与兼容性处理基础弧形和动态效果都有了但要投入生产环境还需要处理一些边界情况和提升易用性。5.1 支持多种对齐方式我们的基础实现假设文字从左到右排列在弧形的底部中点。但实际需求可能多变文字可能需要分布在弧顶、弧左、弧右或者让文字的内侧或外侧对齐弧线。我们可以引入一个Alignment枚举参数public enum ArcAlignment { Bottom, // 弧形在文字下方默认 Top, // 弧形在文字上方 Outer, // 文字外缘对齐弧线 Inner // 文字内缘对齐弧线 } public ArcAlignment alignment ArcAlignment.Bottom;在计算centerOffset和rotation时需要根据alignment进行调整。例如对于ArcAlignment.Top我们需要将圆弧旋转180度让凸面朝下对于Outer和Inner则需要在计算顶点偏移时考虑字符的宽度或高度使字符的基线与弧线相切而不是中心点落在弧线上。这涉及到更精细的顶点变换可能需要根据字符的charInfo.ascender和charInfo.descender等信息进行偏移计算。5.2 与UI布局组件的兼容一个常见的问题是当弧形文本放在HorizontalLayoutGroup或ContentSizeFitter下面时布局系统会崩溃。因为布局系统依赖于RectTransform的rect大小来计算而我们的顶点变换是在网格层面并没有改变RectTransform的尺寸。这会导致布局重叠或留白。解决方案有两种欺骗布局系统在UpdateArc的最后根据弧形文本实际占据的空间可以估算为以半径为边长的正方形或者根据弧角计算包围盒手动设置_tmpText.rectTransform.sizeDelta。但这很 hacky且难以精确计算。推荐方案脱离自动布局对于需要复杂变形的UI元素最稳妥的做法是将其从自动布局体系中剥离。不要将弧形文本放在LayoutGroup下作为自动排列的元素。而是通过代码或手动调整其RectTransform的位置和大小。如果必须与其他元素对齐可以考虑使用Anchor或简单的脚本来同步位置。5.3 顶点色与材质动画弧形变换只修改了顶点位置。但有时我们还想连带做一些颜色渐变、UV动画等。好消息是在修改顶点数组时我们可以同时访问和修改mesh.colors32顶点色和mesh.uvUV坐标。例如实现一个从弧线起点到终点的颜色渐变Color32[] colors mesh.colors32; for (int i 0; i _tmpText.textInfo.characterCount; i) { // ... 获取字符信息和t值 ... Color32 lerpedColor Color32.Lerp(Color.red, Color.blue, t); for (int j 0; j 4; j) { int idx vertexIndex j; colors[idx] lerpedColor; } } mesh.colors32 colors;这可以创造出非常丰富的视觉效果如彩虹文字、根据弧度深浅变化的文字等。6. 性能优化深度剖析当屏幕上需要同时显示几十个动态弧形文本时比如一个充满弧形标签的策略游戏地图性能问题就会凸显。优化主要围绕两个核心CPU计算和Draw Call。6.1 CPU计算优化避免每帧ForceMeshUpdate如前所述这是最耗时的操作之一。它触发了TMP的完整文本布局、字形生成、网格构建流程。务必使用脏标记模式仅在文本内容改变时调用。缓存与预计算缓存TMP_TextInfo_tmpText.textInfo的获取在循环中多次调用是安全的但可以缓存到局部变量。预计算字符映射表如果弧形参数如半径、弧角不变只有文本内容变那么每个字符在弧上的“角度比例系数”其实只和字符索引有关。可以预先计算一个float[] positionRatios数组在UpdateArc中直接使用避免在循环内进行charCenterX / totalWidth的除法和映射计算。当文本变化时重新生成这个映射表。重用Mesh实例在UpdateArc中Mesh mesh _tmpText.mesh;会获取一个网格实例。频繁的网格操作可能会产生GC垃圾回收压力。可以考虑在Start时创建一个新的Mesh实例并缓存起来每次修改这个缓存网格然后通过_tmpText.canvasRenderer.SetMesh(cachedMesh)应用。但要注意如果TMP内部因为字体大小等改变而重新生成了网格你需要检测并更新你的缓存引用。简化计算减少三角函数调用Mathf.Sin和Mathf.Cos是相对昂贵的运算。如果动态效果只是让弧度匀速变化可以预先计算好每一帧的变换矩阵或偏移向量而不是为每个顶点每帧都计算一次Sin/Cos。对于波浪效果如果waveFrequency是整数倍关系可以利用正弦函数的周期性进行查表LUT。6.2 Draw Call优化Draw Call是CPU命令GPU绘制一次图元的过程。UI的Draw Call合并Batch能极大提升渲染效率。字体图集Font Atlas确保所有弧形文本使用的字体都来自同一个TMP_FontAsset。TMP会将字体字形打包到一个图集中。如果不同的弧形文本使用了不同的字体或者同一个字体但不同的大小/风格导致生成了新的图集就会打断合批增加Draw Call。材质与材质属性所有使用相同材质和相同材质属性值的UI元素可以被合批。我们的弧形文本组件如果只是修改顶点而没有修改材质属性如_FaceColor,_OutlineColor等Shader属性那么它们仍然可以和普通的TMP文本合批。但是如果你在脚本中通过_tmpText.fontMaterial或_tmpText.color修改了颜色或者启用了遮罩Mask都可能导致材质实例化MaterialPropertyBlock从而打断合批。重要提示直接修改_tmpText.color会为这个文本对象创建一个新的材质实例破坏合批如果需要对整个文本块调色建议通过修改顶点色mesh.colors32来实现这不会打断合批。如果必须修改材质属性考虑使用MaterialPropertyBlock但它对UI合批的支持需要具体测试。层级与顺序Unity UI的合批依赖于在Hierarchy中的顺序。尽可能将需要动态更新的弧形文本放在一起并减少它们与其他不同类型UI元素如Image、RawImage的穿插。静态的弧形文本可以和静态的普通文本放一起。考虑使用Canvas分区对于UI元素非常多且复杂的界面可以考虑使用多个Canvas。将频繁更新的动态弧形文本放在一个单独的、设置为Render Mode Screen Space - Camera或World Space的Canvas上并将其Canvas组件的Additional Shader Channels设置为包含TexCoord1,Normal,Tangent如果TMP材质需要。这可以将动态元素的更新与静态界面隔离开避免静态元素因为动态元素的重绘而被迫重绘即“Canvas重建”。但要注意多Canvas会增加Draw Call的基线数量需要权衡。6.3 性能监控与调试在Unity编辑器的Stats窗口和Frame Debugger中可以清晰地看到BatchesDraw Call数量。优化目标是让使用相同字体的弧形文本和普通文本的Batches数接近。SetPass calls渲染状态切换次数通常与Batches相关。Canvas.SendWillRenderCanvases这是UI系统在渲染前重建布局和网格的耗时。如果这个值很高说明有大量的UI元素每帧都在变化比如你的弧形文本在无条件地每帧调用UpdateArc。优化脏标记逻辑可以显著降低此项。7. 常见问题与排查技巧实录在实际开发中你肯定会遇到各种奇怪的现象。这里记录了一些典型问题及其解决方法。7.1 文字渲染错乱、重叠或消失现象文字变成乱码、方块或者字符相互重叠甚至完全不显示。排查检查字符遍历首先确认你的循环for (int i 0; i _tmpText.textInfo.characterCount; i)是否正确。确保跳过了不可见字符if (!charInfo.isVisible) continue;。一个常见的错误是误用了text.Length而不是characterCount前者包含空格等不可见字符会导致顶点索引错乱。检查顶点索引charInfo.vertexIndex是当前字符4个顶点中的第一个的索引。确保后续for (int j 0; j 4; j)循环中索引idx vertexIndex j没有越界。mesh.vertices数组的长度应该是_tmpText.textInfo.meshInfo[0].vertices.Length。检查坐标空间确保你计算的所有偏移量都在同一个坐标空间内。vertices是相对于UI矩形左下角的本地坐标。而radius等参数你定义的是像素值。在计算centerOffset时要确保其单位与顶点坐标匹配通常都是本地空间单位。如果弧形效果过于夸张或微小检查radius的值是否合理尝试100-500之间。检查文本溢出如果弧角太大或半径太小计算出的顶点位置可能会超出RectTransform的边界被裁剪掉。可以暂时关闭_tmpText.overflowMode或调整RectTransform的尺寸看看。7.2 动态更新时效果闪烁或卡顿现象在播放动态弧度变化时文字抖动、闪烁或者动画不流畅。排查帧率与更新时机确保你的动态更新逻辑在Update或LateUpdate中并且没有在多个地方重复调用UpdateArc。使用脏标记确保一帧只更新一次。三角函数精度在极小的半径或极大的弧角下浮点数计算精度可能导致顶点位置微幅抖动。可以考虑对最终顶点位置进行简单的四舍五入vertices[idx] new Vector3(Mathf.Round(vertices[idx].x), Mathf.Round(vertices[idx].y), vertices[idx].z);。但注意这可能会影响平滑动画。Canvas重建如前所述频繁的网格更新会触发Canvas重建。使用Frame Debugger查看Canvas.SendWillRenderCanvases的调用堆栈确认是否是弧形文本脚本引起的。如果是加强更新条件判断。协程与Time.deltaTime如果在协程中使用while循环进行插值动画确保使用了yield return null;并且插值因子t的计算基于Time.deltaTime累积而不是固定的帧数以适应不同的帧率。7.3 与阴影Outline、描边Underlay等效果冲突现象为TMP文本添加了Outline或Shadow效果后弧形变形失效或者阴影位置错乱。原因TMP的这些特效是通过复制多层网格并偏移实现的。我们的脚本默认只处理了主网格meshInfo[0]而特效网格meshInfo[1],meshInfo[2]...没有被处理导致主文字弯曲了但阴影还停留在原地。解决在UpdateArc的顶点变换循环外层再套一层对meshInfo数组的循环。TMP_MeshInfo[] meshInfos _tmpText.textInfo.meshInfo; for (int meshIndex 0; meshIndex meshInfos.Length; meshIndex) { Vector3[] vertices meshInfos[meshIndex].vertices; // ... 然后是你的字符遍历和顶点变换逻辑 ... // 注意变换逻辑需要应用到每一层网格上 // 处理完后将vertices赋值回 meshInfos[meshIndex].vertices } // 最后需要调用 _tmpText.UpdateVertexData(TMP_VertexDataUpdateFlags.All); 来更新所有网格数据这样无论是主体文字还是其阴影/描边都会同步进行弧形变换。7.4 在UI粒子或Mask下的渲染问题现象弧形文本被放在一个带有Mask或RectMask2D的父物体下或者与粒子系统叠加时显示不正常。排查Mask裁剪Mask组件依赖于RectTransform的矩形区域进行裁剪。我们的顶点变换可能使文字部分跑到这个矩形区域之外导致被错误裁剪。需要确保弧形文本的RectTransform尺寸足够大能够容纳变形后的整个文字区域。可以写一个辅助方法根据弧度、半径和文本长度估算一个包围盒并动态调整sizeDelta。渲染顺序UI的渲染顺序由Hierarchy中的顺序决定后渲染的会覆盖先渲染的。确保你的弧形文本在粒子渲染器的前面或后面以达到预期的叠加效果。对于复杂的UI混合可能需要调整Canvas的Sort Order或使用多个Canvas来分离渲染层。最后调试这类问题最强大的工具就是Frame Debugger。打开它一帧一帧地看UI的绘制命令你可以清晰地看到每个Draw Call绘制了什么网格数据是什么从而精准定位是顶点数据错了还是渲染状态不对。