ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

NGUI UI挖洞实现指南:静态与动态遮罩方案详解

NGUI UI挖洞实现指南:静态与动态遮罩方案详解 1. 挖洞需求的本质与NGUI下的实现思路先说结论在NGUI里做“挖洞”本质上不是真的把图片像素抠掉而是通过“上层遮挡”的方式把不想显示的区域用透明通道盖住让下面的内容透出来。这个思路和大多数人第一反应完全相反很多人一上来就想着怎么修改原图的像素数据或者研究shader做透明度剔除实际落地时都会踩坑。先说这个需求从哪来。我刚接触这个功能时是策划提了一个新手引导的需求需要在UI界面上圈出一块区域让玩家集中注意力看某个按钮其余部分压暗。这类效果在游戏里太常见了包括地图迷雾、聚光灯高亮、区域解锁、图片局部擦除本质都是一回事在任意一张图片上的指定位置让某个区域显示出来其他区域被一层半透明遮罩盖住。NGUI做这个事有个天然的优势也有个天然的坑。优势在于NGUI的UI元素本身就是按Depth排序的四边形网格UITexture和UISprite都是纯粹的带纹理的Quad渲染链路非常直白坑在于很多人习惯把UGUI的思路带过来想着用RectMask2D或者模板缓冲去裁剪这在NGUI里不能说绝对做不了但代价极大会破坏DrawCall合并属于给自己找麻烦的路线。所以我在实际项目里选的主流做法是叠加一张带Alpha通道的遮罩图。这张遮罩图整张覆盖在背景图上遮罩图的Alpha通道在“洞”的位置是透明的其余位置是不透明的再配上NGUI的UIPanel裁剪或者干脆不做裁剪直接拉伸铺满视觉上就实现了“在图片某个地方挖了个洞”的效果。这里要插一句NGUI里的UITexture默认Shader是Unlit/Transparent Colored它本身就支持Alpha混合所以不需要任何自定义Shader普通做法即可。这也是NGUI在这类需求上比很多自研UI引擎更省事的原因。明确了方案之后第二步要解决的问题是洞的位置、大小、形状怎么控制。如果项目里只需要一个固定位置的洞美工出一张PNG遮罩图贴上就行但如果洞需要动态移动比如引导高亮跟随按钮滑动美工图就废了必须用代码动态生成纹理。这两种方案我都实现过下面分别拆开讲。2. 方案选型对比为什么不用Shader和模板缓冲在动手写代码之前我把踩过的方案挨个说一遍给后来人省点时间。第一个尝试过的是Shader方案。思路很直接写一个带透明镂空区域的Shader在片元着色器里根据UV坐标判断如果在洞口范围内就discard或者把Alpha设为0。这个方案的技术可行性没问题但和NGUI的UIDrawCall机制有冲突。NGUI在生成DrawCall的时候会把同一个UIPanel下材质相同的UIWidget合批一旦某个UITexture用了自定义Shader它会被单独拆成一个DrawCall而且Depth顺序和合批逻辑都要重新梳理。如果你只有一张图做挖洞拆一个DrawCall完全能接受但如果是引导系统要同时高亮五六个控件DrawCall数量立刻爆炸。再加上要处理Shader的Keyword变体、平台差异移动端discard的性能问题我最终放弃了这条线。第二个尝试过的是模板缓冲Stencil Buffer方案。这个方案在UGUI里很成熟原理是先把遮罩区域写入模板再让背景图只在模板匹配的地方输出。但在NGUI里问题不少NGUI没有像UGUI那样封装好Stencil操作的组件需要在材质上手动加Stencil参数并自己管理模板值的写入和清除稍有不慎就会导致其他UI渲染异常。而且NGUI的UIPanel本身会修改Shader的Stencil参数两者叠加很容易冲突排查问题非常痛苦。如果是单机Demo炫技可以用商用项目我劝你绕道。第三个看起来可行但其实有隐患的方案是直接修改Texture2D的像素数据把“洞”位置的像素Alpha改成0然后重新Apply给UITexture。这个方案在逻辑上最直观但有几个致命问题改的是原图原图如果同时被多个UI使用所有地方都会出现洞Texture2D.Apply会触发纹理上传GPU的操作如果频繁改比如洞的位置跟随手指移动CPU开销和GC压力非常大对导入纹理本身要求开启Read/Write内存占用高移动端容易OOM。最终我认为最稳妥的组合是静态洞用贴图方案动态洞用生成独立遮罩纹理方案。这两种方案都不动原图遮罩失败只影响视觉效果不影响数据而且DrawCall完全可控。下文的实操部分主要围绕这两个方案展开。3. 实操静态洞与动态洞的完整实现步骤3.1 准备工作场景搭建与基础配置不管哪种方案场景搭建是一样的。我先说基础结构。在NGUI的UIRoot下面我习惯建三个节点层级从上到下依次是挖洞遮罩层Depth较大、背景图Depth较小、底层UI比如按钮。顺序一定不能反遮罩层必须显示在背景图上方否则洞的效果出不来。背景图用UITexture因为后续要对图片尺寸做像素级计算UITexture的mainTexture可以直接拿到Texture2D。如果项目里背景是From图集加载的Sprite建议先把该图单独设置成Texture或者用UITexture加载外部图片不然图集里的Sprite拿不到独立的像素数据。我踩过这个坑图集的Sprite在Atlas里是打包后的整张大图的一部分直接改像素会污染图集里的其他Sprite。遮罩层也用UITexture不挂任何额外脚本。它的纹理在运行时替换成我们生成的遮罩图。遮罩UITexture的尺寸设置成和背景图完全一致Anchor也保持一致确保像素对齐。如果场景里已有其他UI注意Depth管理。背景图的Depth我设为100遮罩层设为101底层UI保持更小的Depth。这样遮罩必然盖在背景图上底层UI不会被遮罩影响——如果你想让底层UI同样被压暗给遮罩层加一个半透明的黑色底图即可下一节会细说。3.2 静态洞用PNG遮罩图实现5分钟搞定静态洞最简单适合洞的位置和大小永远不变的情况。操作步骤就三步第一步让美术出两张图一张是背景原图一张是同样的尺寸的遮罩PNG。遮罩PNG的规则是洞的位置填充完全透明的像素Alpha为0其他位置填充纯白色不透明像素RGB任意Alpha为255。如果需要半透明压暗效果Alpha填128之类的值也可以但一般洞周围要的就是不透明半透明会透出下面的按钮效果反而奇怪。第二步在NGUI里创建两个UITexture一个设背景一个设遮罩遮罩放在背景的上一层Depth比背景大。第三步运行看效果。如果洞的位置有偏差用UITexture的pivot和localPosition微调或者让美术重出图。注意几个要点遮罩PNG不需要MipMap导入设置里Generate Mip Maps关掉Alpha Is Transparency勾上。如果背景图本身在NGUI里被缩放显示遮罩也要等比例缩放保证相对位置不错位。我的经验是洞的位置在美术出图时就按最终屏幕分辨率给定遮罩和背景用同一个Anchor和同一个缩放别叠加多层不同缩放规则。静态洞的遮罩图可以用PS或任何绘图工具生成程序员不用写一行代码。这也是它最大的优点。3.3 动态洞运行时动态生成遮罩纹理洞随手指移动动态洞是这个需求真正的重头戏因为在引导玩法里策划不会满足于固定位置。核心思路是用代码创建一个Texture2D填充Alpha通道把洞的位置设置为透明然后赋给遮罩UITexture。完整代码如下我贴一个我项目里精简过的版本using UnityEngine; using System.Collections.Generic; /// summary /// 在NGUI UITexture上动态挖洞 /// 用法调用 CreateMaskTexture 创建遮罩纹理赋值给遮罩UITexture /// /summary public class NGUIMaskGenerator : MonoBehaviour { /// summary /// 创建一个带透明洞的遮罩纹理 /// /summary /// param namewidth纹理宽一般取背景图宽/param /// param nameheight纹理高一般取背景图高/param /// param nameholeCenter洞中心坐标以纹理左上角为原点单位像素/param /// param nameholeRadius洞半径单位像素/param /// param nameedgeSoftness边缘软边宽度单位像素0表示硬边/param public static Texture2D CreateMaskTexture(int width, int height, Vector2 holeCenter, float holeRadius, float edgeSoftness 2f) { Texture2D tex new Texture2D(width, height, TextureFormat.ARGB32, false); Color32[] pixels new Color32[width * height]; float maxDist holeRadius edgeSoftness; float softStart holeRadius; for (int y 0; y height; y) { for (int x 0; x width; x) { int index y * width x; float dist Vector2.Distance(new Vector2(x 0.5f, y 0.5f), holeCenter); byte alpha; if (dist softStart) { // 洞内完全透明 alpha 0; } else if (dist maxDist) { // 洞外完全不透明 alpha 255; } else { // 过渡带像素Alpha渐变实现抗锯齿 float t (dist - softStart) / (maxDist - softStart); alpha (byte)(t * 255f); } pixels[index] new Color32(255, 255, 255, alpha); } } tex.SetPixels32(pixels); tex.Apply(false, true); tex.wrapMode TextureWrapMode.Clamp; tex.filterMode FilterMode.Bilinear; return tex; } /// summary /// 在已有纹理上更新洞的位置性能优化版只更新洞周围区域 /// /summary public static void UpdateHole(ref Texture2D tex, int width, int height, Vector2 holeCenter, float holeRadius, float edgeSoftness 2f) { if (tex null) { tex CreateMaskTexture(width, height, holeCenter, holeRadius, edgeSoftness); return; } Color32[] oldPixels tex.GetPixels32(); float maxDist holeRadius edgeSoftness; float softStart holeRadius; // 只遍历洞周围的范围 int minX Mathf.Max(0, (int)(holeCenter.x - maxDist) - 1); int maxX Mathf.Min(width, (int)(holeCenter.x maxDist) 2); int minY Mathf.Max(0, (int)(holeCenter.y - maxDist) - 1); int maxY Mathf.Min(height, (int)(holeCenter.y maxDist) 2); // 先把旧洞范围全部置为不透明 for (int y minY; y maxY; y) { for (int x minX; x maxX; x) { int index y * width x; oldPixels[index] new Color32(255, 255, 255, 255); } } // 再写入新洞 for (int y minY; y maxY; y) { for (int x minX; x maxX; x) { float dist Vector2.Distance(new Vector2(x 0.5f, y 0.5f), holeCenter); byte alpha; if (dist softStart) { alpha 0; } else if (dist maxDist) { alpha 255; } else { float t (dist - softStart) / (maxDist - softStart); alpha (byte)(t * 255f); } int index y * width x; Color32 c oldPixels[index]; c.a alpha; oldPixels[index] c; } } tex.SetPixels32(oldPixels); tex.Apply(false, false); } }调用方式很简单在场景里给遮罩UITexture挂一个脚本代码类似这样public class DynamicHoleDemo : MonoBehaviour { public UITexture bgTexture; // 背景图 public UITexture maskTexture; // 遮罩UITexture挂在背景上一层 public UISprite buttonSprite; // 要挖洞高亮的按钮Sprite也行 void Start() { Vector2 center GetTextureLocalCenter(buttonSprite); Texture2D mask NGUIMaskGenerator.CreateMaskTexture( bgTexture.mainTexture.width, bgTexture.mainTexture.height, center, 80f, 4f ); maskTexture.mainTexture mask; maskTexture.width bgTexture.width; maskTexture.height bgTexture.height; } Vector2 GetTextureLocalCenter(UIWidget widget) { // 把目标UI的中心位置换算成背景图纹理坐标系左上角为原点 Vector3 worldPos widget.transform.position; Vector3 localPos bgTexture.transform.InverseTransformPoint(worldPos); // localPos是背景图本地坐标中心是(0,0)需要转换为纹理左上角为原点的坐标 float texCenterX localPos.x bgTexture.width * 0.5f; float texCenterY (bgTexture.height * 0.5f - localPos.y); return new Vector2(texCenterX, texCenterY); } }这段代码做了三件事根据目标控件位置计算洞中心点、生成遮罩纹理、赋给遮罩UITexture。洞中心点的换算逻辑我单独强调一下这是最容易被绕晕的地方。背景图UITexture的pivot如果默认是Center那么它的本地坐标系原点在图片中心x向右为正y向上为正。但是纹理坐标系的原点在左下角Unity Texture2D的像素坐标而且当我们在逻辑上讨论“挖洞”时常用的是“以图片左上角为原点、y向下”的坐标系。因此我从UI的Transform拿到的是中心原点坐标要换算成左上角原点坐标就做了一次平移和y翻转。如果你不写这一步转换洞会偏到完全不相干的位置。3.4 洞的形状扩展从圆形到矩形、任意多边形上面代码只实现了圆洞但实际需求里矩形洞也很常见——比如高亮一个按钮按钮是矩形你不可能用圆去框它。矩形洞的像素判断逻辑不复杂// 判断像素是否在矩形洞内 bool InRect(Vector2 p, Rect rect) { return p.x rect.xMin p.x rect.xMax p.y rect.yMin p.y rect.yMax; }边缘软边的处理可以改成四边各自计算到边界的距离取最小值再在软边范围内做Alpha渐变。矩形洞圆角是引导系统最常用的形态圆角可以让视觉柔和很多。实现时可以先算像素到矩形四条边的距离小于圆角半径时再算到圆心的距离再做软边。如果是任意形状的洞比如背景是一张地图要挖出不规则大陆的形状我的做法是让美术把洞口做成一张单独的纯白区域贴图洞口为透明把它作为“形状蒙版”加载进来蒙版上每个像素的Alpha就直接作为遮罩纹理对应位置的Alpha。本质上就是把“美术出遮罩图”和“代码生成纹理”结合代码负责把这个蒙版赋给生成的遮罩纹理然后根据洞的位置偏移和大小缩放。这种方式既保留动态灵活性又不需要程序手写复杂的几何求交。4. 踩坑记录洞偏了、锯齿、性能三个大问题4.1 洞的位置偏移九成是坐标系没对齐这个坑我自己踩了至少三次。症状是代码里算出来的洞中心和实际UI按钮的位置对不上偏了几十像素甚至跑到屏幕外。最常见的错因是坐标系混用。你在脚本里写了一个洞中心坐标它到底是哪个坐标系下的是背景图纹理像素坐标还是UITexture本地坐标还是屏幕坐标还是世界坐标这几个坐标系的换算完全不同从世界坐标到纹理左上角原点的坐标代码是Vector3 localPos bgTexture.transform.InverseTransformPoint(worldPos); float texCenterX localPos.x bgTexture.width * 0.5f; float texCenterY bgTexture.height * 0.5f - localPos.y;如果背景图挂在一个有缩放的父节点下面还要把缩放考虑进去。我的经验是统一在Start时把背景图和按钮的世界坐标都拿下来然后全部换算到遮罩纹理坐标系里计算最后再把洞的坐标转换为UV相关的数值时再除以纹理宽高。别在脚本里混用不同坐标系宁可多算几步也不要省变量。另外注意NGUI的UIWidget.width是面板层面的像素尺寸纹理像素尺寸是mainTexture.width两者在缩放显示时不一定相等。如果你的UITexture设了scale而不是width/height这两个数值会差异很大。我建议不要在动态方案里使用UITexture的scale来控制大小而是直接设置width和height属性NGUI会为UITexture生成对应尺寸的Quad纹理赋值上去后像素一一对应洞才不会变形。4.2 洞的边缘锯齿和半透明发白动态生成的遮罩纹理如果边缘是硬边——洞内Alpha是0洞外Alpha是255在斜边或曲线上会出现明显的锯齿。这是数字图像处理里的典型问题解决办法就是我在代码里写的软边过渡带。边缘过渡带的宽度我一般取2到4个像素太窄抗锯齿效果不明显太宽会显得洞的边缘发虚、模糊视觉上像蒙了一层雾。还有个小坑如果遮罩纹理用了Bilinear过滤洞边缘的Alpha会被纹理采样自动插值导致本来就锐利的边缘又多一层模糊。实测下来洞较大的时候Bilinear问题不大但如果洞很小比如只有20像素半径建议把filterMode改成Point配合代码里3像素左右的软边效果反而更好。这个小细节我当时调了好久。另一个发白问题是半透明混合导致的。当遮罩UITexture的Alpha从0渐变到255时如果背景图颜色本身很亮过渡区域会出现一层“白边”。本质上是因为Alpha混合公式里半透明像素覆盖在亮色背景上颜色合成后色阶突变被人眼感知。处理方式有两个要么把过渡带压缩到2像素以内要么让美术把背景图在洞口附近的颜色调暗一档削弱色阶突变。4.3 性能优化不要全图遍历像素上面代码里有两个方案CreateMaskTexture是全图遍历UpdateHole只遍历洞周围范围。如果背景图是1024x1024全图遍历是100万次循环每次还要算一个Vector2.Distance在移动端一帧内做一次就已经有明显卡顿了。如果洞的位置要跟随手指每帧更新必须用局部更新版本。我项目里的实测数据iPhone 8上1024x1024全图遍历加SetPixels32加Apply一次耗时约15到25毫秒已经超过一帧的16.6毫秒预算界面会明显掉帧改成局部更新洞直径100像素只更新100x100的区域一次耗时不到1毫秒完全无感。如果你的项目对性能很敏感建议更激进一点遮罩纹理尺寸不跟背景图一样大而是做成背景图的四分之一大小然后在UITexture上放大四倍显示。洞的边缘会有轻微模糊但换来的是填充像素量减少到十六分之一。这个取舍在移动端很值得。另外一个隐藏性能点Texture2D.Apply的调用频率。每次调用都会触发一次纹理上传频繁调用不仅耗CPU还可能引发GPU端的内存峰值。所以UpdateHole里我用了两个参数区分CreateMaskTexture里Apply(false, true)这个true表示“释放像素数据”因为纹理被赋予后CPU端不再需要C#数组而UpdateHole里Apply(false, false)表示保留像素数据因为下次更新洞时还要用GetPixels32读回来。这个细节不写清楚复用纹理时容易报错或者浪费内存。4.4 遮罩层和背景层的Depth与Anchor打架还有一次我在集成时发现明明是同一个Anchor下挂的两个UITexture运行后遮罩却和背景错位了一个像素。排查了很久发现是UITexture的pivot不一致——背景的pivot是TopLeft遮罩的pivot保持了默认的Center导致两者即使Anchor相同实际对齐的基准点也不同。Panels和Widgets的pivot是一个很容易被忽略的属性。强烈建议在动态方案里把背景图和遮罩UITexture的pivot都设为TopLeft且localPosition都设为(0,0)这样它们的左上角重合纹理坐标系直接一一对应计算洞的位置时不需要再考虑offset能省掉很多心智负担。5. 常见问题速查表挖洞功能运行不正常时先查这里我在不同项目里反复遇到同类问题整理成一张表遇到问题按表格排查效率最高。现象可能原因解决思路洞的位置和预期目标偏差很大坐标系换算错误 / pivot不一致统一用左上角原点坐标系pivot设为TopLeft加Log核对坐标洞是反的该透明的区域不透明纹理坐标的y轴方向搞反检查UV坐标换算逻辑尤其是从世界坐标转纹理坐标时的y翻转遮罩把整个背景全盖住或全透明纹理没赋值成功 / 遮罩尺寸错误打印遮罩纹理宽高与背景纹纹理宽高确认纹理尺寸一致洞的边缘锯齿极重边缘软边设置过小 / filterMode不匹配设置2~3像素过渡带试Bilinear和Point两种过滤模式洞的位置移动时画面卡顿明显全图遍历像素太频繁改用局部更新版本降低纹理分辨率减少Apply调用次数遮罩纹理更新多次后内存上涨Apply释放参数错误导致像素数据残留确认CreateMaskTexture用Apply(false, true)UpdateHole用Apply(false, false)设置纹理的UITexture显示黑块纹理格式不受支持 / 代码里创建时格式选错使用TextureFormat.ARGB32导入的纹理需要勾选Read/Write Enable遮罩和背景的Depth无问题但洞仍显示在底层之上Anchor/Pivot导致层级错乱统一pivot为TopLeft检查遮罩UITexture的depth是否大于背景手机上洞边缘发虚发灰MipMap或压缩格式导致Alpha精度损失关闭MipMap纹理格式用RGBA32或ASTC/ETC2带Alpha的压缩格式排查顺序我的建议是先核对代码里洞中心坐标的数值再核对UITexture的pivot和depth最后看纹理的过滤模式和格式。80%的问题出在坐标系和pivot上而不是纹理生成逻辑里。6. 扩展不同形状挖洞与多层压暗效果基础功能做完之后很容易遇到变体需求。比如洞的形状要变成带圆角的矩形、要挖一个跟随小地图标记移动的不规则区域又或者洞周围的遮罩要带一个半透明的底图而不是全透明。统一思路是遮罩UITexture的纹理本质上是一张任意形状的Alpha遮罩。洞的形状由纹理里Alpha为0的区域决定所以只要想办法让对应区域的Alpha变成0即可。我实现的“压暗”变体是这样的在遮罩UITexture下面再挂一层UISprite颜色设为黑色Alpha设为150不参与任何逻辑。因为遮罩UITexture的洞是透明的所以透过洞能看到下面的黑色Sprite——此时洞的位置显示的就是压暗的黑色。如果想要“高亮”而不是“压暗”把黑色Sprite换成暗色的半透明再把目标UI按钮的Depth调高到遮罩层之上目标按钮正常亮度整个背景的其余部分被半透明黑色压暗一个典型的新手引导高亮框就出来了。不同形状的遮罩纹理我写了一个更通用的版本支持传一个函数进来决定像素Alphapublic static Texture2D CreateMaskWithFunc(int width, int height, System.Funcint, int, float alphaFunc) { Texture2D tex new Texture2D(width, height, TextureFormat.ARGB32, false); Color32[] pixels new Color32[width * height]; for (int y 0; y height; y) { for (int x 0; x width; x) { float alpha alphaFunc(x, y); pixels[y * width x] new Color32(255, 255, 255, (byte)(alpha * 255f)); } } tex.SetPixels32(pixels); tex.Apply(false, true); return tex; }调用时把判断逻辑写成lambda或函数对象传入。矩形、圆角矩形、椭圆、任意多边形都可以用这个函数生成。多边形判断可以用射线法或直接调PolygonCollider2D的OverlapPoint灵活度很高。矩形圆角的一个实现判断逻辑参考这里float RoundRectAlpha(int x, int y, Rect rect, float radius, float soft) { float centerX Mathf.Max(rect.xMin radius, Mathf.Min(x 0.5f, rect.xMax - radius)); float centerY Mathf.Max(rect.yMin radius, Mathf.Min(y 0.5f, rect.yMax - radius)); float dist Mathf.Sqrt((x 0.5f - centerX) * (x 0.5f - centerX) (y 0.5f - centerY) * (y 0.5f - centerY)); float edgeDist Mathf.Min(Mathf.Min(x - rect.xMin, rect.xMax - x), Mathf.Min(y - rect.yMin, rect.yMax - y)); if (edgeDist 0 edgeDist radius) { // 圆角区域用圆弧距离判断 if (dist radius - soft) return 0f; if (dist radius) return 1f; return (dist - (radius - soft)) / soft; } // 直边区域 if (edgeDist 0) return 1f; if (edgeDist soft) return edgeDist / soft; return 0f; }这种通用函数的方式比每个形状单独写一个Create函数要简洁得多扩展新形状时不需要动原有代码。7. 我踩过最深的一个坑UIPanel和UITexture的关系最后单独用一段把这个坑写清楚因为它的隐蔽性极高。NGUI里UITexture只有在它所属的UIPanel的可见范围内才会被渲染。如果你的遮罩UITexture设置了很大的尺寸但它的节点挂在了一个裁剪范围很小的UIPanel下面运行后很可能出现“洞显示不完整”或者“遮罩干脆不显示”的现象。症状是遮罩纹理生成正确mainTexture赋值正确Depth正确但运行时就是看不到预期效果。我排查了一个多小时最后UDrawCall里没有这个UITexture的绘制才意识到是UIPanel的clip范围把遮罩裁剪掉了。解决方案有两种。第一种把遮罩UITexture单独挂在一个独立的UIPanel下这个UIPanel不启用裁剪只负责渲染遮罩层。第二种如果背景和遮罩都必须在同一个UIPanel下比如必须与其他UI共享同一个裁剪区域那就把这个UIPanel的clip范围加大到能完全容纳遮罩节点。同理如果遇到IDrawCall为空的情况先查这个UITexture所处UIPanel的裁剪范围再查Depth最后查纹理是否成功赋值。这个排查顺序能节省你大量时间。还有一次我在做WebGL版本时发现动态生成的纹理在某些浏览器上不显示。排查后原因是Texture2D.Apply在WebGL下有异步上传的坑需要调用一次textureCompressionQuality相关设置或改用CompressedTextureLoader。这不是NGUI的锅是Unity在WebGL纹理上传上的老问题。如果你也遇到浏览器不显示试试在生成后手动把纹理的总引用保持住并且用Resources.UnloadUnusedAssets前先确认纹理已被引用否则纹理可能被引擎回收掉。这个场景是我在发布WebGL Demo时遇到的生成的遮罩偶尔变成全黑后来排查发现是纹理被GC了保持一个根引用即可解决。再说一个和Unity版本相关的坑比较新的Unity版本里Texture2D.SetPixels32在编辑器下的性能表现和真机差异很大。编辑器里一顿操作秒开真机上滑动洞的位置时掉帧明显建议真机调试时直接用局部更新版本别拿编辑器数据当参考。我最初一直被编辑器下的流畅表现误导后来真机一测才发现问题花了不少时间做性能优化。按我现在的习惯动态挖洞功能的性能验收标准就两条一是在最低端测试机上洞跟随手指拖动时帧率不低于项目基准值二是每帧产生的GC Alloc低于5KB。如果达不到优先用降分辨率方案其次才考虑改C/Job System不要让简单功能复杂化。8. 最后的实操心得这套方案我在两个正式项目里落地过一次是新手引导高亮一次是地图区域解锁。现在回头看最核心的心得就一条先想清楚洞是在静态场景还是动态场景下使用再决定是美术出图还是代码生成。很多人一开始就想做最复杂的动态方案结果被性能和坐标系问题折磨得半死其实需求文档里根本没提洞要动。如果你只是做一次性的演示效果PNG遮罩图方案半小时内就能搞定完全不需要写代码。如果你要做引导系统这种需要高亮任意UI控件的功能动态生成遮罩纹理方案才是正路一次把通用工具类写好后面所有引导配置都能复用。最后说一个容易被忽略的小技巧生成遮罩纹理时记得在资源释放时把Texture2D销毁。动态方案生成的纹理是运行时创建的不会自动进入场景资源管理如果你的引导功能会被反复触发每次触发都创建一个新纹理内存会缓慢增长。在功能结束时调用Destroy(mask)或Resources.UnloadUnusedAssets才能把这块内存真正释放掉。这种小细节平时不出问题一旦上了内存监控工具就现形了。根据我个人经验在NGUI里处理挖洞这类需求多花半小时把坐标系、Depth、纹理格式这些基础概念理清楚比直接抄网上代码更有价值——很多代码贴出来能用但换一个场景就崩根源都是没理解NGUI的渲染链路。希望这篇能帮你少走一点弯路。
返回列表