ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Unity界面适配与形状自定义:从Canvas Scaler到Shader的完整指南

2026/10/6 13:08:52 拓冰建站 浏览量
Unity界面适配与形状自定义:从Canvas Scaler到Shader的完整指南 做Unity游戏界面最难的不是把按钮摆上去、把图标塞进列表而是你做得挺完美的东西换个手机型号就变得七零八落。竖屏变横屏、刘海屏多了一条黑边、平板上的按钮大得离谱、模拟器上一套分辨率到了真机又是另一套这些问题我在项目里反复踩过而且每次都不是改一个参数就能收场的。你要真正搞定得同时理解两个层面一个是界面形状另一个是分辨率适配。这两个东西看起来分开其实是一件事——屏幕的物理形状和逻辑分辨率决定了UI能怎么排、怎么裁、怎么调。这篇文章我打算把这套东西完整展开讲一遍Canvas Scaler到底要怎么配、异形屏安全区怎么处理、自定义形状的UI有哪些方案Mask、Sprite Mask、UI Shader、动态Mesh、以及一个从零到一的小案例怎么落地。适合正在做Unity客户端、或者做UI框架的开发者尤其是那种不想每次加一个界面都被分辨率折腾一遍的人。我会尽量把原理讲清楚因为只有懂了原理你才敢自己动手扩展而不是永远在复制别人写的适配组件。1. 界面适配和形状自定义本质是在解决什么问题1.1 分辨率问题的本质是屏幕比例变了坐标系没变很多新手一上来就问分辨率怎么设置其实这个说法本身就带着误解。你在Unity编辑器里把Game视图切个分辨率它做的事只是改一下窗口尺寸但你的Canvas到底怎么缩放、怎么定位完全由Canvas Scaler说了算。也就是说分辨率不是一个设置项而是你的UI系统需要应对的输入变量——不同设备传来的Screen.width和Screen.height完全不一样你要做的是让UI布局对这个变量不那么敏感。这里有个关键点Unity UI的坐标单位是逻辑像素不是物理像素。在默认情况下1个UI单位等于1个屏幕像素但这是建立在你的Canvas Scaler选择Constant Pixel Size恒定像素大小的前提下的。一旦你把UIScale设置为Scale With Screen Size逻辑坐标系就会根据参考分辨率缩放所以分辨率变了不再直接等于UI变大了而是变成了UI的缩放比例变了。这个缩放比例的算法就是Canvas Scaler里matchWidthOrHeight做的事情后面我会专门讲。真正让你头疼的往往是宽高比的变化而不是分辨率数值的变化。同样是16:9的1920x1080和1280x720UI可以等比缩放问题不大。但一旦从16:9换到20:9很多挖孔屏、从竖屏换到横屏、或者从手机换到平板UI就必须重新排列而不是单纯缩放。这也是为什么我习惯把适配拆成两部分看数值不同用缩放比例不同用布局。1.2 形状问题不只是圆形头像还有安全区和不规则点击自定义游戏界面形状这个需求往小了说是美术想做几个圆形头像、异形卡片、或者那种带尖角的对话框气泡往大了说是整个UI被限制在某个不规则范围内比如环形屏幕、带刘海的屏幕、甚至车载中控那种长条屏幕。这两类需求的技术路径完全不同千万别混为一谈。圆形的头像框其实就是一个Image配上Mask组件把方形图片裁成圆形这部分很简单。但这种裁切是一个视觉表现它不改变UI元素的矩形位置和大小。也就是说如果你做一个圆形的按钮点击区域默认还是矩形的这个坑很经典——看起来是圆形点四个角也能点到你要是没有处理碰撞区域玩家就会觉得很怪。不规则屏幕的安全区就比较麻烦了。Android的挖孔屏、iPhone的刘海屏都有自己的一套安全区域APIUnity里对应的是Screen.safeArea。如果你不做处理竖屏时UI会被刘海和底部Home条挡住横屏时左右两边会留下奇怪的黑色或者被遮挡。这不是缩放能解决的必须通过代码动态调整UI的根节点边距。1.3 我常用的技术栈总览这里先给一个总览后面每一块都会细讲。我的技术栈大致是Canvas Scaler负责全局缩放RectTransform Anchor控制布局位置SafeArea组件处理异形屏Image的九宫格处理拉伸不变形Mask和RectMask2D做简单裁切自定义Shader处理圆角、边框、多边形等复杂形状如果美术需求极其特殊比如要做一个不规则路径的地图窗口那就直接用VertexHelper动态生成UI网格。这套组合基本能覆盖我能想到的所有界面形状和分辨率适配需求。2. 分辨率适配的底层原理与实操配置2.1 Canvas Scaler三种模式到底怎么选Canvas Scaler是Unity中决定UI缩放的核心组件它挂在Canvas根节点上有几种模式但实际项目中常用的也就三种我一个个说清楚。Constant Pixel Size恒定像素大小。这种模式下UI不随屏幕大小缩放1个UI单位始终等于1个物理像素。优点是逻辑简单缺点是一旦屏幕分辨率低UI就挤成一团分辨率高UI就变得很小看不清。这种模式只适合不做跨设备适配的场合比如纯编辑器工具窗口、或者游戏内的固定分辨率渲染目标正常项目我不会拿它来做主UI。Scale With Screen Size按屏幕大小缩放。这是主流选择。你需要设定一个参考分辨率比如1920x1080Unity会拿当前实际分辨率与你设定的参考分辨率做对比计算出缩放系数。关键参数是matchWidthOrHeight匹配宽度或高度它决定缩放时以哪个维度为准。设为0表示完全匹配宽度设为1表示完全匹配高度0到1之间表示混合。简单理解竖屏游戏我一般把参考分辨率设成竖屏然后把matchWidthOrHeight设为1这样高度是硬参考宽度变化靠布局拉伸横屏游戏反过来或者取一个折中值。Constant Physical Size恒定物理大小。这种模式让UI元素在物理上保持一样大也就是在不同PPI的设备上UI的物理尺寸不变比如一个13毫米高的按钮在任何手机上都是13毫米。它的问题在于会引入DPI动态变化性能更差而且不同设备的响应可能非常奇怪我基本只在做纸质说明书类的工具项目时才考虑。模式适用场景核心问题我的选择Constant Pixel Size编辑器工具、固定分辨率跨设备必崩不选Scale With Screen Size手机、PC游戏主UI需要控制宽高比匹配主流方案Constant Physical Size特殊硬件、印刷类模拟性能差、行为不稳定很少用2.2 参考分辨率怎么定matchWidthOrHeight的取舍选定Scale With Screen Size后第一件事是设定Reference Resolution。我见过很多团队直接填1920x1080然后做了一个月发现竖屏手机上UI完全没法看——因为美术给的图是横屏的参考分辨率也是横屏的。你要做竖屏游戏参考分辨率就应该定竖屏做横屏游戏就定横屏这是常识但真的很多人会搞错。接下来是matchWidthOrHeight的具体理解。这个值的含义是缩放系数 根据当前屏幕宽高比在以宽为基准的缩放因子和以高为基准的缩放因子之间做插值。注意它不是简单地平均两个因子而是对两个因子做指数插值再取对数实际效果是设为0时UI的宽度方向始终占满屏幕设为1时UI的高度方向始终占满屏幕在0.5时UI的缩放会让它在两种极端之间取折中这时候屏幕两侧可能会有超出部分需要通过布局去处理。我的实操建议是不要把一个matchWidthOrHeight值用到底。竖屏游戏大多设为1横屏游戏大多设为0如果游戏要同时支持竖屏和横屏那你必须动态切换这个值——在脚本里根据Screen.width和Screen.height的比值决定当前该用0还是1。我实测过很多方案最终做法是写一个简单的脚本OnRectTransformDimensionsChange里监听Canvas的尺寸变化然后动态设置matchWidthOrHeight。这样至少能保证UI在横竖屏切换时不会出现大面积乱飞。2.3 SafeArea组件异形屏最后的保命手段异形屏的处理不是靠Canvas Scaler完成的因为刘海和挖孔是屏幕物理形状造成的遮挡逻辑分辨率并没有变但可视区域变了。Unity在Screen.safeArea里给你提供了安全区域的矩形单位是屏幕像素。你要做的就是把这个矩形映射到UI根节点上。我写过一个通用的SafeArea脚本挂在UI根节点或者某个需要避让的父节点上核心逻辑就几行读取Screen.safeArea然后根据Canvas Scaler的缩放比把安全区的像素值转换成UI坐标下的offset最后设置RectTransform的offsetMin和offsetMax。要注意的是安全区不是永远不变的手机横竖屏切换、折叠屏展开、Android的导航栏显示/隐藏都会触发安全区变化所以这个脚本必须监听屏幕变化事件最好是每帧检测一次或者用Screen相关的事件回调。这里有一个我在项目里反复踩的坑如果把SafeArea组件直接挂在Canvas根节点上它会改变整个Canvas的尺寸这会反过来影响Canvas Scaler的适配基准导致UI整体位置偏移。我的做法是Canvas根节点保持全屏不动Canvas下加一个安全的子节点把SafeArea放在这个子节点上后续所有UI都放在这个安全区容器里面。这样既有适配的参照系又能避开异形区域。2.4 UI相机和正交投影别忽略分辨率对相机的影响如果你的UI不是用Canvas渲染模式自带的屏幕空间相机而是把Canvas放在World Space里那分辨率适配就多了一个维度正交相机的orthographicSize设置。这个size决定了相机的半高也就是世界坐标里你看到的范围高度。屏幕宽度则由高度和宽高比共同决定。举例来说如果游戏的世界UI参考高度是10个单位那么你把正交相机的size设为5一半高度屏幕宽度会根据当前屏占比自动变化。在16:9的屏幕上看到的宽度是8.89个单位在20:9的屏幕上宽度就变成11.11个单位。如果你需要左右两端不露馅就得把背景图拉得足够宽或者做一个动态扩展的背景层。我习惯用脚本在Start或者分辨率变化时根据Screen.width/Screen.height动态调整背景层的宽度让世界UI不管在什么屏幕上都能铺满。3. 自定义各种界面形状的四种主流方案3.1 Mask和RectMask2D简单形状的首选Unity最基础的形状限制就是Mask组件它放在一个带Image的UI节点上然后用这张图作为模板把子节点里超出模板的部分裁掉。比如你有一个方形图片给它的父节点挂一个MaskMask的Image是圆形时子节点就只显示圆形区域。这是实现圆形头像、三角形边框、星星形状卡片最直接的方式。但Mask有个性能问题它在内部需要为被遮罩的孩子生成一个额外的Stencil buffer操作如果Mask的数量很多或者Mask下面挂的UI元素特别多DrawCall和填充率都会上升。更麻烦的是Mask不支持一些特殊操作比如子节点里有粒子系统或者非UI物体处理起来会比较别扭。所以我的建议是简单形状少量用Mask大量的话选择RectMask2D或者自定义Shader。RectMask2D是另一个方案它的特点是只用矩形范围做裁剪没有形状概念但性能比Mask好因为它的裁剪逻辑更简单不依赖模板缓冲。适合那种I只需要把超出某个矩形区域的都裁掉的场景比如列表滚动的时候隐藏超出列表框的内容。要注意的是RectMask2D只能处理矩形不能处理圆形或三角形。3.2 Sprite Mask另一种遮罩但用在UI上要慎重Sprite Mask组件平时更多是配合2D Sprite渲染用的它也可以用来遮罩UI但效果和性能都不太理想。原因很简单Sprite Mask默认是作用于整个场景的SpriteRenderer渲染管线的你在UI里用它会牵扯到UI渲染和Sprite渲染互相交叉先不说渲染顺序乱不乱单说它没法很好地和Masking Stencil机制配合就足够让你头疼了。我的建议是常规UI形状需求不要碰Sprite Mask除非你是做2D场景里的技能指示器、地图视野遮罩那它反而很合适。3.3 自定义UI Shader最优雅的形状方案如果你要大量圆形、圆角、带边框的异形卡片用Mask做不仅浪费性能而且会限制很多效果。更优雅的方案是直接写一个UI Shader让Image在绘制时根据UV坐标计算形状。比如给一个普通的UIImage挂一个带圆角片元函数的Shader你在Inspector里设一个圆角半径它就在四个角上画圆角矩形设一个内半径值它就能变成圆环。这个思路的核心是Fragment shader里通过判断像素坐标和形状边界的距离决定这个像素是保留原颜色、变成透明、还是变成边框色。我用这种方式做过血条外框、技能冷却的弧形遮罩、六边形按钮效果比Mask干净很多而且不增加DrawCall。缺点是Shader的写法对团队有一定门槛而且Unity的UI材质有合批要求——如果你给不同的UI元素挂不同的材质合批就会被打断所以要学会用MaterialPropertyBlock或者尽量共享材质避免DrawCall爆炸。下面是一个最基础的圆角矩形UI Shader的代码示例Shader Custom/UIRoundRect { Properties { [PerRendererData] _MainTex (Sprite Texture, 2D) white {} _Color (Tint, Color) (1,1,1,1) _Radius (Corner Radius, Range(0,0.5)) 0.1 _Stroke (Stroke Width, Range(0,0.1)) 0 _StrokeColor (Stroke Color, Color) (0,0,0,1) [HideInInspector] _StencilComp (Stencil Comparison, Float) 8 // 其他Stencil相关属性按Unity默认UI Shader补全 } SubShader { Tags { QueueTransparent RenderTypeTransparent IgnoreProjectorTrue PreviewTypePlane CanUseSpriteAtlasTrue } Cull Off Lighting Off ZWrite Off Blend One OneMinusSrcAlpha Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float4 vertex : SV_POSITION; float2 uv : TEXCOORD0; }; sampler2D _MainTex; float4 _MainTex_ST; fixed4 _Color; float _Radius; float _Stroke; fixed4 _StrokeColor; v2f vert (appdata v) { v2f o; o.vertex UnityObjectToClipPos(v.vertex); o.uv TRANSFORM_TEX(v.uv, _MainTex); return o; } fixed4 frag (v2f i) : SV_Target { // 把uv从[0,1]映射到[-0.5,0.5]方便距离计算 float2 p i.uv - 0.5; float2 d abs(p); // 圆角半径归一化 float r _Radius; // 计算圆角距离场 float q length(max(d - (0.5 - r), 0.0)) - r; // 圆角矩形内为-1外为正数用平滑步长做抗锯齿 float alpha 1.0 - smoothstep(-0.001, 0.001, q); // 边框 float strokeAlpha 0.0; if (_Stroke 0.0) { float borderAlpha 1.0 - smoothstep(-0.001, 0.001, abs(q) - _Stroke); strokeAlpha borderAlpha; } fixed4 color tex2D(_MainTex, i.uv) * _Color; color.a * alpha; // 边框颜色叠加 color.rgb lerp(color.rgb, _StrokeColor.rgb, strokeAlpha * color.a); // UI Shader需要预乘Alpha color.rgb * color.a; return color; } ENDCG } } }这里我用了最简单的距离场思想先算出当前像素离圆角矩形边界的距离距离小于0表示在内部再用smoothstep做一下抗锯齿边缘看起来会平滑很多。边框就是算出边界距离后再判断一次绝对距离是否小于描边宽度。这种方式最大的优势是不需要Mask不需要额外的模板缓冲只要Image的Sprite不透明它就能画出一个完美的圆角矩形。我在项目里把这套Shader扩展成了一个可调圆角、内圆角、描边、渐变、弧形进度条的工具库用起来非常舒服。3.4 VertexHelper动态生成UI Mesh终极自由如果需要表现的形状完全不受限制比如不规则的路径、环形血条、动态改变的多边形那么Shader可能也不好做这时候就该用VertexHelper直接改UI网格了。Unity的Image组件本身是可以用一个自定义的Mesh Geometry替换的方法就是写一个继承自MaskableGraphic的自定义组件在OnPopulateMesh函数里自己生成顶点和三角形。这种做法最常见的使用场景是动态绘制多边形按钮、雷达图、弧形进度条、或者带偏移的图文混排。因为你有完整的顶点生成逻辑你想要多少边形、想让顶点位置随时间变化都能控制。代价是需要对三角剖分和UV映射有一定的几何基础否则很容易生成错乱的面片。举一个简单例子动态生成一个六边形按钮的轮廓using UnityEngine; using UnityEngine.UI; public class HexGraphic : MaskableGraphic { [SerializeField] private float radius 50f; [SerializeField] private int sideCount 6; protected override void OnPopulateMesh(VertexHelper vh) { vh.Clear(); // 中心顶点 UIVertex center UIVertex.simpleVert; center.position Vector2.zero; center.color color; vh.AddVert(center); // 外围顶点 for (int i 0; i sideCount; i) { float angle Mathf.PI * 2f * i / sideCount; Vector2 pos new Vector2(Mathf.Cos(angle), Mathf.Sin(angle)) * radius; UIVertex vertex UIVertex.simpleVert; vertex.position pos; vertex.color color; vertex.uv0 new Vector2((pos.x / radius 1f) * 0.5f, (pos.y / radius 1f) * 0.5f); vh.AddVert(vertex); } // 中心三角形扇 for (int i 0; i sideCount; i) { vh.AddTriangle(0, i 1, i 2); } } }这样生成出来的组件你把它挂在UI下它就能绘制一个六边形。配合代码动态修改sideCount和radius就能让按钮在点击时变成八边形或者缩小视觉效果很灵活。不过要提醒一点动态Mesh组件不能和标准Image共享批次所以在大量UI元素中这种动态组件要控制数量否则合批会被破坏。3.5 扩展图文混排和自定义校验里的形状问题顺带提一个热词里出现的图文混排。Unity原生Text不支持文中插入图片最多就是RichText标签改颜色。如果你要做对话系统里带表情、名称、物品图标的富文本那就要自己扩展或者用第三方插件。图文混排最核心的难点就是图片不是一个独立的个体它要参与Text的排版——行高、换行、对齐都要处理。本质上这和你处理不规则UI图形的思路是相通的都是要精确控制渲染区域和布局区域的关系。我见过很多团队的做法是在Text下面挂一个独立的图片节点然后手动对齐它的位置遇到过行数变化就乱七八糟。真正稳妥的方案是写一个自定义文本网格生成器在生成每个字符的顶点时同时为图片生成一个占位矩形再把这个矩形区域的图片绘制出来。这个方案其实也是基于VertexHelper的和动态Mesh那节是一套逻辑。4. 完整实操做一个多边形状态卡片 自动适配分辨率的小案例4.1 需求描述和目标拆解下面我用一个实际案例把前面讲的东西串起来。假设游戏里有一个背包里的装备卡片卡片不是圆角矩形而是六边形边框卡片要适配不同分辨率和异形屏卡片内部要显示图标、名称和属性数值而且这些内容不能跑到六边形外。这种需求看起来简单但结合形状分辨率很多团队会做成图片渲染错位或者内容溢出。我先把目标拆解成三点第一卡片的轮廓是六边形图片不能被裁出矩形边第二不管屏幕分辨率怎么变卡片在屏幕上的相对位置和整体大小不能失控第三卡片内部文本区域要始终处于六边形内不溢出。按照这个目标我一个一个做。4.2 第一步Canvas和Canvas Scaler配置我新建一个Canvas渲染模式用Screen Space - Overlay或者Screen Space - Camera配合专门的UICamera。在Canvas上挂Canvas Scaler参考分辨率设置为设计稿的分辨率——这个项目美术提供的是竖屏1280x720以宽为基准所以我直接填Width: 720Height: 1280matchWidthOrHeight填0。为什么填0因为这张设计稿的UI是按宽度来对齐的左右两边是固定边界上下可以留空。如果画面转横屏我会在代码里动态把它切换成1但第一版先按竖屏处理。这里我额外说一句很多人的设计稿可能是1920x1080横向但你拿到手机上第一时间要做的就是确认游戏是竖屏还是横屏。竖屏游戏如果把参考分辨率设成横屏那Canvas的宽度参考就没有意义所有布局都会被压缩。4.3 第二步用自定义Shader实现六边形卡片我在项目里没有选择Mask因为这张卡片的轮廓不是UI图片的透明区域而是需要动态加边框、加渐变、加高光的。Mask只能裁不能画边框。所以我写了一个六边形UI Shader核心思想是在Fragment阶段算出当前UV点在不在六边形内再根据到边的距离画描边。六边形的距离场比圆角矩形稍微复杂一点。最简单的方式是把六边形拆成六个三角形区域分别判断点是否在三角形内。你可以用一把很简单的公式把点转换到六边形的局部坐标系然后依次判断六个边的半平面。我不想把代码拉太长重点告诉大家思路——核心是判断点是否在多边形内计算点到多边形边缘的距离前者负责显示后者负责画边框和抗锯齿。4.4 第三步编写适配脚本和内容布局因为卡片要自动适配分辨率我把卡片的根节点放在安全区容器内然后给卡片定义了一个Anchor。竖屏时卡片固定在中部偏左的位置用Anchor把左下角拉到屏幕的一半处。这样做的好处是我在代码里只需要设置RectTransform.anchoredPosition不用关心屏幕尺寸。实际的布局代码我会这样写using UnityEngine; using UnityEngine.UI; public class EquipmentCard : MonoBehaviour { public RectTransform cardRoot; public Text nameText; public Text attrText; public Image iconImage; public void Setup(string cardName, string attrs, Sprite icon) { // 直接设置文本 nameText.text cardName; attrText.text attrs; iconImage.sprite icon; // 因为卡片是多边形内容区域需要进行收缩避免文字溢出六边形边界 float safePadding 20f; // 这个值根据卡片半径算 iconImage.rectTransform.offsetMax new Vector2(-safePadding, -safePadding); iconImage.rectTransform.offsetMin new Vector2(safePadding, safePadding); } }这段代码里的safePadding是个关键经验值。六边形和矩形不一样越靠近边缘越窄所以你不光要留出常规边距还得让图标中心往中心靠。如果你不做这个收缩内容看起来会像被挤到边缘外一样。这里的设计思路和SafeArea是一样的形状限制会吃掉一部分可用布局空间所以内容布局必须为形状让路。4.5 第四步真机多分辨率预览和检查清单配置完成后我在编辑器里先切了几个预设分辨率比如1920x1080、2400x1080、2560x1440、1080x2400大致看一遍布局有没有失控。然后上真机重点检查三个地方刘海屏上下是否遮挡、卡片边缘是否贴边、文字是否溢出。我自己的检查顺序是先看Canvas Scaler的匹配值再看安全区容器的offset是否生效最后看Shader的边缘是否锯齿。基本上只要这几项稳定UI就能扛住大多数设备。5. 常见问题与排查技巧实录5.1 分辨率切来切去UI黑屏或者错乱这个问题基本不是你代码的Bug而是Canvas的RenderMode和Camera配置冲突。最常见的是Screen Space - Camera模式的Canvas其EventCamera或UICamera的TargetDisplay设置不对要么是切换分辨率时相机没刷新视口。还有一个很容易踩的坑是你在脚本里直接改Screen.SetResolution但UI Canvas的缩放值并没有随分辨率变化实时更新这个需要确保CanvasScaler组件没有被禁用并且需要手动触发Canvas.ForceUpdateCanvases。我排查这个问题时第一件事是打开Profiler看Canvas的Rebuild耗时第二件事是检查Canvas Group的alpha和Canvas上的Sorting Order基本上80%的黑屏问题都能在这两步里暴露。5.2 Mask边缘锯齿、发虚怎么办如果你用了Mask做圆形头像边缘发虚是常态。原因是Mask只是做了一个Stencil裁剪如果原始图片的分辨率不够高边缘没有AA信息你看到的就是锯齿。要解决不外乎两招一是放大图片分辨率让边缘有足够像素做抗锯齿二是在Mask这张Shapes图片上做边缘软化把图片的边缘从硬边改成半透明渐变。这个在Photoshop里处理很简单把圆形图周围做几像素的羽化就行。经过羽化的Mask裁剪出来的边缘会柔和很多代价是边缘会比设计稿虚一点你需要自己调一个平衡度。5.3 异形屏上按钮被刘海挡住这个问题的根源是忘了加SafeArea。我在项目里见过很多人只在启动时读取一次安全区没有处理屏幕旋转或者折叠屏展开的情况。解决方案就是我前面提的把SafeArea逻辑写成一个持续监听的组件并且放在UI父级容器上。还有一个细节部分Android设备的剪切区域不是在SafeArea里返回的而是通过cutout APIUnity高版本已经封装了Screen.cutout你需要在Android上额外判断一下。如果项目用的Unity版本比较老建议升级或者自己用AndroidJavaObject调用DisplayCutout接口。5.4 图文混排时点击区域和显示区域不一致这个问题我在做聊天系统时遇到过。文字显示是正常的但图片的位置永远差一点点击消息链接也经常点不中。排查下来发现问题是RichText的图片占位和实际图片节点的坐标没有同步。如果是自己扩展的文本网格你必须保证矩形占位和图片渲染用的UV区间是一套数据不能一边手动算一边用RectTransform自动排版。我的建议是把图片节点放到文本节点的同一RectTransform下然后通过代码在这个父节点下根据文本网格的占位矩形设置位置而不是让图片节点独立存在。5.5 常见问题速查表现象可能原因排查顺序UI黑屏或错乱Canvas渲染模式和相机目标设置问题检查CanvasScaler、Camera、ForceUpdateCanvasesMask边缘锯齿Stencil裁剪无AA信息检查图片分辨率和羽化边缘按钮被刘海遮挡缺少SafeArea或安全区更新不及时检查Screen.safeArea监听、Android cutout文字溢出多边形形状限制了内容布局但没做内缩调整布局Padding内容中心靠拢图片位置不对图文混排占位矩形不同步检查Mesh生成和图片节点位置逻辑分辨率切换后拉伸变形九宫格设置错误或Image type不对检查Image.type为Sliced或Filled6. 最后分享一个我自己一直在用的小工具这篇文章写到最后我想分享一个我自己写了很久的小工具。它不是某个现成的插件就是你项目里的一个Utility脚本核心功能就是监听屏幕变化动态调整CanvasScaler的安全区和匹配值。它也不是很复杂就是定期检查Screen.width、Screen.height、Screen.safeArea如果发现变化就把当前的安全区映射到一个CanvasContanier节点上同时根据宽高比设置CanvasScaler的matchWidthOrHeight。有了这个工具团队里任何新成员做的UI只要放进这个容器里跑在真机上基本不会出大乱子。我现在做新项目第一个搭建的就是这个基础层后面所有UI功能都在它上面生长。我自己最大的体会是不要指望一个适配方案能解决所有问题它只能给你兜底真正的UI设计还是得有人为不同屏幕比例做主次决策。但只要形状和分辨率这两个底层问题稳住了剩下的事就都是细节了。