首页 / 资讯中心 / 文章详情

Unity刮刮乐插件实战:原理、调优与避坑指南

Unity刮刮乐插件实战:原理、调优与避坑指南 ★ FEATURED ARTICLE
刮刮乐这种交互形式玩家手指在屏幕上蹭两下涂层被抹开底下露出奖品或者图案那种即时反馈带来的爽感是很多重度玩法替代不了的。我在几个休闲项目和活动运营页里都做过类似效果早期用自己写的 Mask 加 RenderTexture 硬啃后来接触到现成的 Unity 刮刮乐插件才发现很多坑其实早有人踩过并封装好了。这篇就围绕 Unity 刮刮乐插件Scratch Card这个主题把它的核心原理、插件结构、参数调优、实战接入流程以及我实际踩过的那些坑完整地拆一遍。不管你是刚接触 Unity 不久、想给项目加个轻量互动玩法的新手还是接过运营活动需求、需要快速交付刮奖模块的熟手下面这些内容应该都能直接拿去用。我会尽量把“为什么这么设计”讲透而不是只丢一堆 API 让你自己猜。1. 刮刮乐玩法的整体设计与技术选型思路1.1 这个玩法到底在解决什么问题先想清楚一件事刮刮乐在游戏或应用里本质上不是一个“游戏机制”而是一个反馈包装层。它把“抽奖结果”这个冷冰冰的判定包装成“你自己刮出来的”这个过程。用户以为自己靠手指刮开了涂层实际上奖品在刮之前就已经定好了。理解这一点非常关键因为它决定了技术实现的重心——刮开过程要足够顺滑、真实、有手感而结果判定要和刮开进度解耦。所以一个合格的刮刮乐插件必须同时处理好两件事一是视觉上的涂层擦除让用户看到涂层被一点点刮掉二是逻辑上的进度统计判断刮开了多少比例、什么时候算“刮完”、什么时候触发开奖回调。很多新手一上来只盯着“怎么把图擦掉”结果做完发现没法判断刮开面积或者刮到 90% 就卡住不触发这就是把两件事混在一起了。从技术选型角度看市面上常见的实现路径大概有三类我做个对照表方便你判断自己该用哪种实现方式核心原理优点缺点适用场景自定义 Mask RenderTexture用一张遮罩图记录擦除轨迹实时渲染到目标图完全可控效果细腻实现复杂性能开销大跨平台适配麻烦对效果有极致要求的大项目Shader 顶点/片元擦除通过传入笔刷位置在片元着色器里做透明度混合性能好无额外纹理开销需要写 Shader进度统计要额外做有一定图形基础的团队现成插件贴图读写型直接对 Texture2D 做像素级 Alpha 写入上手快逻辑清晰进度统计天然支持高频写入有性能压力需要节流活动页、休闲玩法、快速交付我个人的经验是绝大多数刮刮乐需求用现成插件里的贴图读写方案就够了。原因很实在——刮刮乐通常出现在活动弹窗、奖励领取这种低频、短时的场景里用户刮个十几秒就结束了性能压力远没有想象中大。真正需要上 Shader 方案的是那种把刮刮乐做成核心玩法、要长时间反复刮的项目这种其实很少见。1.2 为什么插件普遍选择“贴图读写”这条路你去看主流刮刮乐插件的源码会发现它们大多走的是Texture2D.SetPixels或GetPixel/SetPixel这条路。具体做法是准备一张和涂层区域等大的可读写纹理初始全不透明或者全白代表“未刮”用户手指划过时把笔刷范围内对应像素的 Alpha 设为 0然后Apply()提交。渲染层用一个 RawImage 或 SpriteRenderer 显示这张纹理涂层就一点点消失了。这条路被广泛采用理由有这么几条。第一进度统计天然成立。既然你已经在逐像素改 Alpha那顺手统计一下“Alpha 为 0 的像素占比”就能得到刮开比例不需要额外做射线检测或者面积估算。第二跨平台一致性好。像素操作是 CPU 层面的不依赖具体图形 API安卓、iOS、PC 表现基本一致不像某些 Shader 方案在不同机型上会有精度差异。第三实现门槛低。不需要懂图形学会读写纹理数组就能改团队里任何人接手都能看懂。当然它也有代价。SetPixels是相对重的操作如果每帧对整张大图调用帧率会明显掉。所以插件里通常会有两个优化一是只更新笔刷覆盖的那一小块区域用SetPixels32配合局部坐标二是节流不是每次触摸移动都提交而是攒一小段距离或者隔几帧提交一次。这两点在后面讲参数调优时我会展开。1.3 插件结构拆解一个刮刮乐组件通常包含什么拿到一个刮刮乐插件别急着往场景里拖先花十分钟把它的组成看清楚。典型的插件会拆成这么几块涂层渲染层一个挂载了 RawImage 或 SpriteRenderer 的物体负责把可读写纹理显示出来。它通常还带一个遮罩Mask 或 RectMask2D保证刮开区域不超出卡片边界。纹理数据层核心的Texture2D对象标记为可读写Read/Write Enabled保存当前涂层的像素状态。输入处理层把屏幕触摸或鼠标位置转换成纹理坐标再映射到笔刷中心点。这一层要处理坐标翻转问题UI 坐标和纹理坐标的 Y 轴方向经常是反的这是新手最容易翻车的地方。笔刷系统定义笔刷的形状圆形、方形、半径、硬度边缘羽化程度。圆形笔刷最常用因为手指刮出来的痕迹本来就是圆的。进度与回调层统计刮开比例达到阈值时触发OnScratchComplete之类的回调同时可能提供OnScratchProgress的实时进度事件。重置与初始化接口提供ResetCard()之类的方法方便同一张卡片复用比如用户刮完一张后点“再来一次”。理解了这个分层你在接入时就知道该改哪里、不该动哪里。比如想换涂层图案只动纹理数据层想改手感只调笔刷系统想加音效挂在进度回调上就行。这种“各管一摊”的结构是判断一个插件是否成熟的重要标志。2. 核心细节解析涂层擦除、笔刷与进度统计2.1 涂层擦除的像素级原理别被“擦除”两个字骗了很多人以为刮刮乐是“把涂层擦掉”其实从像素角度看根本没有“擦掉”这个动作只有“把某个像素的 Alpha 改成 0”。涂层纹理本身一直存在只是某些像素变透明了底下的奖品图就透出来了。这个认知很重要因为它解释了为什么刮刮乐的层级结构通常是“底层奖品图 上层涂层图”而不是“一张图被挖了洞”。具体到代码层面一次笔刷落点大概做这么几件事。先根据触摸点算出笔刷在纹理上的中心坐标(cx, cy)然后遍历笔刷半径r范围内的所有像素对每个像素判断它到中心的距离d如果d r就把这个像素的 Alpha 设为 0或者按硬度做渐变。伪代码大概长这样for (int y cy - r; y cy r; y) { for (int x cx - r; x cx r; x) { if (x 0 || x texWidth || y 0 || y texHeight) continue; float dist Mathf.Sqrt((x - cx) * (x - cx) (y - cy) * (y - cy)); if (dist r) { Color32 c pixels[y * texWidth x]; c.a 0; pixels[y * texWidth x] c; } } }这里有个细节值得说边界判断不能省。手指划到卡片边缘时笔刷中心可能已经超出纹理范围如果不做x/y的越界检查pixels数组会直接抛越界异常。我见过不止一个项目因为这个问题在真机上闪退编辑器里却复现不了因为编辑器里鼠标很难精确划到边缘外。另一个细节是硬度Hardness。纯圆形笔刷刮出来的边缘是硬邦邦的像用橡皮擦擦出来的不够自然。成熟插件会引入硬度参数让笔刷边缘的 Alpha 从中心到边缘做渐变模拟真实手指按压的柔和感。实现上就是把dist r换成按dist/r的比例做插值越靠边缘 Alpha 衰减越多。这个参数调好了手感提升非常明显。2.2 笔刷半径与纹理分辨率的匹配关系笔刷半径这个参数新手最容易拍脑袋填。填小了刮起来费劲填大了两下就刮完了没意思。但更隐蔽的问题是笔刷半径的单位是像素而像素的物理大小取决于纹理分辨率。同样填 30 像素的半径在 512×512 的纹理上显得很小在 2048×2048 的纹理上就显得很大。所以正确的做法是先定纹理分辨率再反推笔刷半径。我一般用这个思路假设卡片在屏幕上的显示尺寸是W×H单位是屏幕像素纹理分辨率是TW×TH那么纹理像素和屏幕像素的比例是TW/W。如果我希望笔刷在屏幕上看起来半径是R个屏幕像素那纹理上的半径应该是R × TW/W。举个例子卡片显示尺寸 400×400纹理 1024×1024比例是 2.56。我希望笔刷在屏幕上半径约 25 像素差不多是手指肚的大小那纹理半径就是25 × 2.56 ≈ 64像素。这个换算看着简单但不做的话换个分辨率纹理手感就全变了调参调到怀疑人生。提示纹理分辨率不是越高越好。1024×1024 对绝大多数刮刮乐场景已经足够2048 以上除了增加内存和SetPixels开销肉眼几乎看不出差别。移动端尤其要克制。2.3 进度统计的两种口径选错了会出 bug进度统计听起来简单但口径选错会直接导致“刮完了不触发回调”或者“没刮完就提前开奖”。常见的有两种口径第一种是按像素统计遍历整张纹理数 Alpha 为 0 的像素占总像素的比例。这个口径最准确但每次统计都要遍历全图开销大所以通常不是每帧算而是隔一段时间算一次或者用增量方式维护一个计数器。第二种是按网格统计把纹理划分成比如 20×20 的网格每个格子记录“是否被刮过”进度就是已刮格子数除以总格子数。这个口径开销小但精度粗可能出现“视觉上刮得差不多了但某些格子还差一点点没碰到”的情况。我一般推荐像素口径 增量维护的组合。具体做法是维护一个scratchedCount计数器每次笔刷把某个像素从“不透明”改成“透明”时计数器加一注意要判断它之前是不是已经不透明避免重复计数。进度就是scratchedCount / totalPixels。这样统计是 O(1) 的不需要遍历全图。这里有个坑重复计数。如果笔刷在同一片区域反复刮每次都会把已经是 0 的像素再设成 0如果不判断“之前是否已经透明”计数器会虚高导致进度提前到 100%。所以每次改像素前要先读一下原值只有原值 Alpha 大于阈值时才计数。这个细节插件里一般处理了但如果你自己改代码务必留意。2.4 完成阈值的设定为什么不是 100%几乎所有刮刮乐插件都允许你设置一个“完成阈值”比如 0.7 或 0.8意思是刮开 70%~80% 就算完成而不是等到 100%。这个设计非常必要原因有两个。一是边缘死角。卡片四个角、边缘这些地方用户的手指很难精确刮到如果非要 100%用户会觉得“明明刮干净了怎么还不开奖”体验很差。二是性能与体验的平衡。让用户把最后那点边角料也刮干净纯属折磨阈值设在 0.7~0.8用户刮得差不多就自动开奖节奏更舒服。阈值具体设多少我的经验是卡片越大、涂层越厚阈值可以越低。小卡片比如 300×300设 0.8大卡片比如全屏设 0.6~0.7 都合理。另外可以配合一个“自动完成”逻辑当进度超过阈值时不是立刻开奖而是播放一个“剩余涂层快速淡出”的动画视觉上更完整用户也不会觉得突兀。3. 实战接入从零把插件跑起来的完整流程3.1 环境准备与插件导入假设你拿到的是一个典型的 Unity 刮刮乐插件包通常是一个.unitypackage或者一个包含 Scripts、Prefabs、Textures 的文件夹。导入流程没什么特别的但有几个点要提前确认。先看 Unity 版本兼容性。大多数刮刮乐插件对版本要求不高2019 之后的 LTS 基本都能跑但如果插件里用了 URP 相关的 Shader而你的项目是内置管线就可能出现材质显示异常。导入前扫一眼插件的 Shader 文件确认渲染管线匹配。然后是纹理的可读写设置。这是最容易忽略的一步。插件用到的涂层纹理必须在导入设置里勾选Read/Write Enabled否则运行时GetPixels会直接报错。如果是运行时动态创建的Texture2D构造时要把TextureFormat设成带 Alpha 的格式比如RGBA32并且最后一个参数mipChain传false避免多级纹理带来的额外开销。Texture2D coating new Texture2D(width, height, TextureFormat.RGBA32, false); coating.wrapMode TextureWrapMode.Clamp; coating.filterMode FilterMode.Bilinear;wrapMode设成Clamp是为了防止纹理边缘采样时出现重复拉伸filterMode用Bilinear让涂层显示更平滑。这两个设置虽小但对最终观感有影响。3.2 场景搭建层级与遮罩的正确姿势场景搭建这块我见过太多人把层级搞乱导致刮开区域溢出卡片。正确的结构应该是这样的Canvas └── ScratchCardRoot (RectTransform, 卡片尺寸) ├── PrizeImage (RawImage/Sprite, 底层奖品图) └── CoatingLayer (RawImage, 涂层) └── (可选) Mask 组件关键点在于涂层必须被限制在卡片范围内。有两种做法一是给涂层父节点加RectMask2D二是给涂层本身加Mask组件配合一张形状图。RectMask2D性能更好适合矩形卡片如果卡片是不规则形状比如圆形、异形就得用Mask加形状图。还有一个容易忽略的点涂层的 RawImage 要设置正确的 UV Rect。如果纹理和显示区域的宽高比不一致涂层会被拉伸刮开的手感也会跟着变形。稳妥的做法是让纹理分辨率的宽高比和卡片显示尺寸的宽高比保持一致或者用AspectRatioFitter组件强制约束。3.3 输入坐标到纹理坐标的映射翻车重灾区这一节我要重点讲因为坐标映射是刮刮乐接入过程中翻车率最高的环节。问题出在坐标系不一致屏幕/UI 坐标原点在左下或左上取决于平台和 Canvas 设置纹理坐标原点在左下但Texture2D的像素数组是从左下往右上排的而RawImage的 UV 又是从下往上。这几套坐标系混在一起稍不注意就刮反了。稳妥的映射流程是这样的。先用RectTransformUtility.ScreenPointToLocalPointInRectangle把屏幕点转成卡片本地坐标再根据卡片尺寸归一化到 0~1最后乘以纹理分辨率得到像素坐标Vector2 localPoint; RectTransformUtility.ScreenPointToLocalPointInRectangle( cardRect, screenPos, eventCamera, out localPoint); // 归一化到 0~1 float u (localPoint.x cardRect.rect.width * 0.5f) / cardRect.rect.width; float v (localPoint.y cardRect.rect.height * 0.5f) / cardRect.rect.height; // 转像素坐标 int px Mathf.RoundToInt(u * texWidth); int py Mathf.RoundToInt(v * texHeight);这里v的方向要和纹理数组的行方向对齐。如果发现刮出来的位置上下颠倒把py换成texHeight - py试试。这个“颠倒”问题几乎每个新手都会遇到一次别慌就是坐标系方向的事。注意如果卡片在 Canvas 里被旋转或缩放过ScreenPointToLocalPointInRectangle依然能正确处理但如果你手动用InverseTransformPoint之类的做转换就要自己考虑旋转缩放容易出错。优先用官方提供的 RectTransformUtility 方法。3.4 笔刷落点与连续刮擦的处理单点刮擦好办难的是连续滑动。用户手指快速划过时两次触摸采样点之间可能隔了很远如果只在采样点画圆刮出来的痕迹会是一串断开的圆点像虚线一样很难看。解决办法是在相邻两个采样点之间做插值补点按固定间距比如笔刷半径的一半在两点连线上均匀撒点每个点都画一次笔刷。float distance Vector2.Distance(lastPoint, currentPoint); int steps Mathf.CeilToInt(distance / (brushRadius * 0.5f)); for (int i 1; i steps; i) { Vector2 p Vector2.Lerp(lastPoint, currentPoint, (float)i / steps); PaintBrush(p); }这个补点逻辑是刮刮乐手感的关键。间距太大痕迹会断间距太小又浪费性能。我实测下来间距取笔刷半径的 0.3~0.5 倍比较合适既连贯又不至于点太密。另外补点只在同一笔手指没抬起内做手指抬起后要重置lastPoint否则下一笔会从上一笔的终点连一条线过来出现莫名其妙的划痕。3.5 性能节流什么时候 Apply什么时候不 ApplyTexture2D.Apply()是把 CPU 端改动的像素上传到 GPU 的操作开销不小。如果每次触摸移动都调用在低端机上帧率会明显下降。所以必须做节流。常见的节流策略有两种。第一种是按距离节流累积触摸移动距离超过某个阈值比如 5 个屏幕像素才提交一次。第二种是按帧节流每隔 N 帧提交一次比如每 2~3 帧。我一般用距离节流因为它和用户操作直接相关手感更可控。accumulatedDistance Vector2.Distance(lastPoint, currentPoint); if (accumulatedDistance applyThreshold) { coating.Apply(false); // false 表示不重新生成 mipmap accumulatedDistance 0; }Apply(false)这个参数别传错传true会重新生成 mipmap开销翻倍而刮刮乐根本用不到 mipmap。另外手指抬起时OnPointerUp要强制Apply一次保证最后一段改动不丢失。4. 常见问题排查与实战避坑实录4.1 刮不动、刮反了、刮出界三大高频问题速查我把实际项目里遇到最多的问题整理成一张表方便你对照排查现象可能原因排查方向解决办法完全刮不动纹理未开 Read/Write输入事件没接上看 Console 有无报错打日志确认触摸回调是否触发勾选纹理 Read/Write检查 EventSystem 和射线检测刮出来的位置和手指对不上坐标映射方向错Canvas 缩放未考虑打印 localPoint 和像素坐标对比修正 Y 轴方向用 RectTransformUtility 做转换刮开区域溢出卡片缺少遮罩遮罩层级不对检查涂层父节点有无 Mask/RectMask2D加 RectMask2D确认遮罩在涂层之上刮完不触发回调进度统计重复计数或阈值设成 1.0打印实时进度值修正计数逻辑阈值降到 0.7~0.8低端机卡顿每帧 Apply笔刷半径过大Profiler 看 Apply 耗时加节流减小笔刷半径或纹理分辨率这张表里“刮不动”和“刮反了”占了新手问题的八成。前者基本是纹理没开可读写或者事件没接上后者基本是坐标方向问题。把这两个先排除剩下的都好办。4.2 真机与编辑器表现不一致怎么办编辑器里刮得好好的打包到真机上就出问题这种情况我遇到过好几次。最常见的原因是触摸坐标和鼠标坐标的差异。编辑器里用鼠标模拟Input.mousePosition的坐标系和真机触摸的Input.GetTouch(0).position在多数情况下一致但如果你的 Canvas 用了Screen Space - Camera模式相机参数不同就会导致映射偏差。另一个原因是纹理格式在真机上的压缩。编辑器里纹理可能是未压缩的 RGBA32打包时如果被压成了 ETC2 之类的格式Alpha 通道的精度会下降导致刮开边缘出现锯齿或者半透明残留。解决办法是在纹理导入设置里把涂层纹理的压缩格式强制设为RGBA32或者不压缩虽然会多占点内存但刮开效果稳定。还有一个隐蔽的坑多分辨率适配。真机屏幕比例和编辑器 Game 视图不一致时卡片的实际显示尺寸变了但笔刷半径是按固定像素算的手感就变了。所以笔刷半径最好在运行时根据卡片实际尺寸动态计算而不是写死一个值。4.3 刮开后的收尾动画、音效与状态复位刮到阈值触发回调后别急着直接显示奖品加一点收尾处理体验会好很多。我的常规做法是触发完成回调时先播放一个“剩余涂层快速淡出”的动画0.2~0.3 秒内把涂层整体 Alpha 降到 0同时播放一个“叮”的开奖音效然后再弹出奖品展示。这个短暂的过渡能掩盖“突然出现”的突兀感。状态复位这块如果卡片要复用比如用户点“再刮一张”必须把纹理恢复到初始状态。最直接的办法是重新SetPixels一遍初始数据再Apply但要注意同时把scratchedCount归零、lastPoint清空、完成标记重置。我见过有项目复用时忘了重置计数器结果第二张卡一刮就瞬间完成查了半天才发现是计数器没清零。public void ResetCard() { coating.SetPixels32(initialPixels); coating.Apply(false); scratchedCount 0; lastPoint Vector2.zero; isCompleted false; hasPointer false; }initialPixels是初始化时缓存的一份原始像素数据复位时直接拷回去比重新生成纹理快得多。4.4 几个只有踩过才知道的实操心得最后分享几个文档里不会写、但实际很影响体验的点。第一笔刷不要用纯圆形硬边。前面提过硬度和羽化这里再强调一次边缘做 20%~30% 的羽化刮出来的痕迹会自然很多像真的用手指蹭掉的。纯硬边看起来像用橡皮图章盖的很假。第二涂层图案可以加点纹理。纯色涂层刮起来缺乏层次感如果在涂层上叠一层细微的噪点或者金属质感纹理刮开时的视觉反馈会丰富很多。这个成本很低一张小纹理叠上去就行。第三进度回调别每帧都发。如果OnScratchProgress每帧都触发订阅方比如更新进度条 UI每帧都要处理浪费性能。改成进度变化超过 1% 才发一次或者按固定间隔发UI 更新完全够用。第四考虑“刮开提示”。有些用户不知道要刮可以在涂层上叠一个半透明的“手指滑动”提示动画几秒后自动消失。这个小细节能显著提升首次使用的完成率。第五测试时一定要用真机、用不同尺寸的屏幕测。编辑器里鼠标刮和真机手指刮的手感差异很大小屏和大屏的笔刷半径感受也不同。我一般至少在两种尺寸的真机上各刮十几次确认阈值和笔刷都合适再交付。这套东西跑通之后刮刮乐其实可以玩出不少花样——比如多层涂层刮开一层还有一层、限定区域刮开只有特定区域能刮、刮开进度联动粒子特效等等。但核心的像素擦除、坐标映射、进度统计这三块地基打牢了上面怎么加都是顺水推舟的事。我自己现在接这类需求基本半天就能从零搭出一个可交付的版本剩下的时间都花在调手感和适配不同屏幕上而这部分恰恰是最需要真机反复试的急不来。
阅读完成 · 觉得有帮助?
咨询建站