Unity刮刮乐交互蒙版实战:从Sprite Mask到Render Texture的GPU绘制方案 1. 项目概述从“刮刮乐”到交互式蒙版最近在做一个线上营销活动产品经理提了个需求想做一个类似线下彩票刮刮卡的互动效果。用户用手指或鼠标在屏幕上“刮”开一层涂层露出下面的奖品信息增加参与感和趣味性。这听起来不就是个“刮刮乐”效果吗在Unity里实现这个乍一想挺简单不就是画个蒙版然后擦除嘛。但真做起来你会发现要兼顾性能、效果和跨平台兼容性里头的门道还真不少。这个效果的核心本质上是一个动态的、可交互的蒙版系统。它不仅仅是UI层面的一个简单动画而是涉及到了渲染管线、GPU编程、输入处理和实时计算的综合性功能。无论是用于H5小游戏、移动端App内的营销活动还是VR/AR中的交互解密这个技术点都非常实用。我自己在实现过程中从最基础的Sprite Mask方案一路踩坑最终摸索出了一套兼顾效果和性能的实战方案今天就来详细拆解一下把原理、步骤和避坑经验都分享给你。2. 核心原理与方案选型为什么不用Sprite Mask刚开始接到需求很多人的第一反应可能是用Unity自带的Sprite Mask组件。这确实是最快能跑通的原型方案准备两张图一张底图奖品信息一张遮罩图灰色涂层把遮罩图作为Sprite Mask的“遮罩精灵”底图作为被遮罩的子对象。用户触摸时动态修改遮罩图的alpha通道模拟擦除。我最初也是这么做的但很快就发现了几个致命问题精度与性能的冲突Sprite Mask基于CPU对网格进行裁剪。要想擦除效果精细比如圆滑的笔刷边缘就需要把遮罩精灵的网格划分得非常细密高顶点数这会带来巨大的CPU开销和Draw Call。在移动端大量顶点变换是性能杀手。交互不自然通过修改整张纹理的alpha来模拟“刮”的动作很难做到笔触实时跟随。要么是区域擦除一块一块地消失要么需要每帧更新整张纹理性能消耗巨大。功能单一很难实现“刮开比例统计”、“刮开区域平均色”等进阶功能而这些往往是活动运营需要的比如刮开超过50%才可领奖或者根据刮出的颜色判断奖项。所以Sprite Mask方案只适用于对性能要求极低、交互非常简单的静态展示场景。对于需要实时、流畅、可定制刮擦效果的互动需求我们必须换一条路走基于Render Texture和Shader的GPU绘制方案。2.1 最终方案的核心思路我们放弃动态修改精灵纹理的思路转而利用GPU的渲染能力。核心架构分为三层底层刮擦画布使用一张Render Texture作为我们的“刮擦蒙版”。这张纹理初始为全黑代表涂层完全覆盖。我们不在CPU端修改它而是将用户的刮擦轨迹触摸点实时传递给GPU。中层绘制逻辑在GPU上通过一个绘制Shader将用户的每次刮擦动作视为一个“笔刷”渲染到上述的Render Texture上。笔刷通常渲染为白色或特定灰度值。这样Render Texture上白色区域就代表“已被刮开”的区域。上层效果合成在最终显示到屏幕的UI或物体上使用一个合成Shader。这个Shader同时采样“背景纹理”奖品信息和我们的“蒙版Render Texture”。根据蒙版纹理每个像素的亮度即黑白程度来决定是显示背景纹理还是显示涂层颜色从而实现刮开的视觉效果。这个方案的巨大优势在于将最耗时的“绘制”工作从CPU转移到了GPU并且笔刷效果可以通过Shader灵活定制模糊边缘、纹理笔刷等性能极高效果也最好。2.2 工具与插件评估在确定核心思路后你可能会搜索到一些现成的插件或开源项目比如GitHub上热门的Misaka-Mikoto-Tech/ScratchImage。这类项目通常已经实现了上述架构并可能附加了统计刮开比例、使用ComputeShader进行高效计算等高级功能。注意直接使用成熟的开源项目是快速开发的捷径但务必深入理解其代码。我曾遇到过因为插件使用了某些不兼容的Shader语法如旧版CGPROGRAM导致在部分Android设备上崩溃的情况。最好的方式是“借鉴思路自主实现”至少要做到能修改和调试。对于我们这个教程我将带领你从零搭建这个系统这样你能彻底掌握每一环。我们将使用Unity URP通用渲染管线进行实现因为这是目前和未来的主流方向。3. 分步实现构建你的刮刮乐系统接下来我们进入实战环节。我会创建一个新的URP项目并一步步实现核心功能。3.1 项目初始化与资源准备创建新项目使用Unity Hub创建一个新的3D项目核心模板并在创建时选择“Universal Render Pipeline (URP)”。确保你的Unity版本是2021 LTS或更新版本。导入必要资源在项目中创建以下文件夹结构ScratchCard/Scripts,ScratchCard/Shaders,ScratchCard/Materials,ScratchCard/Textures。准备纹理BackgroundTexture: 这是刮开后会显示的图片比如“一等奖”、“谢谢参与”等。尺寸建议为1024x1024或512x5122的幂次方。BrushTexture: 笔刷纹理。一个简单的圆形渐变纹理中心白边缘透明就能实现柔边笔刷效果。你可以在PS中创建或使用程序生成。保存为PNG格式并导入Unity后确保设置为“Sprite (2D and UI)”且Read/Write Enabled。3.2 创建刮擦蒙版Render Texture蒙版纹理是我们系统的核心数据容器。在Project窗口中右键 -Create - Render Texture命名为ScratchMaskRT。选中它在Inspector面板中设置参数。这里的设置至关重要Size: 设置为与你的背景图一致例如1024。尺寸越大精度越高但GPU内存占用和带宽也越大。对于手机端的全屏刮卡512可能是个平衡点。Color Format: 选择R8G8B8A8_UNORM即可。我们只需要灰度信息但RGBA格式兼容性最好。Depth Buffer: 设置为No depth buffer我们不需要深度信息。Anti-aliasing: 设置为None。多重采样对于这个用途是多余的且消耗性能。初始化蒙版我们需要在游戏开始时将蒙版纹理清空为黑色全遮盖。这可以通过一个简单的C#脚本来完成。创建一个新的C#脚本ScratchCardManager.cs。using UnityEngine; public class ScratchCardManager : MonoBehaviour { public RenderTexture scratchMaskRT; // 拖入我们创建的ScratchMaskRT void Start() { InitializeMaskTexture(); } void InitializeMaskTexture() { if (scratchMaskRT null) return; // 激活这个RenderTexture作为当前渲染目标 RenderTexture.active scratchMaskRT; // 使用GL.Clear来清除颜色缓冲区设置为黑色(0,0,0,1) GL.Clear(true, true, Color.black); // 恢复之前的渲染目标 RenderTexture.active null; } }将脚本挂载到场景中任意GameObject如ScratchCardSystem并将ScratchMaskRT拖拽赋值。运行游戏ScratchMaskRT就会被初始化为全黑。3.3 编写绘制Shader笔刷渲染这个Shader负责将用户的刮擦动作“画”到蒙版纹理上。我们使用一个Unlit Shader。在Shaders文件夹右键 -Create - Shader - Unlit Shader命名为ScratchBrushDraw。打开Shader文件修改关键部分如下Shader Unlit/ScratchBrushDraw { Properties { _BrushTex (Brush Texture, 2D) white {} // 笔刷纹理 _BrushColor (Brush Color, Color) (1,1,1,1) // 笔刷颜色通常为白色 _BrushSize (Brush Size, Range(0.001, 0.1)) 0.05 // 笔刷大小UV空间 } SubShader { Tags { RenderTypeOpaque } Blend SrcAlpha OneMinusSrcAlpha // 启用Alpha混合让笔刷叠加 Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; }; sampler2D _BrushTex; float4 _BrushTex_ST; // 用于处理纹理的缩放和偏移 fixed4 _BrushColor; float _BrushSize; v2f vert (appdata v) { v2f o; o.vertex UnityObjectToClipPos(v.vertex); o.uv TRANSFORM_TEX(v.uv, _BrushTex); return o; } fixed4 frag (v2f i) : SV_Target { // 核心将UV坐标中心化并缩放模拟笔刷大小 float2 centeredUV (i.uv - 0.5) * (1.0 / _BrushSize); // 采样笔刷纹理越靠近中心alpha越高 fixed4 brushSample tex2D(_BrushTex, centeredUV 0.5); // 输出笔刷颜色与纹理alpha的乘积 return _BrushColor * brushSample.a; } ENDCG } } }这个Shader的关键在于frag函数。我们假设这个Shader会被绘制到一个与蒙版纹理对齐的四边形上。通过变换UV使得无论这个四边形画在蒙版纹理的哪个位置我们都能采样到笔刷纹理的中心部分并通过_BrushSize控制笔刷在蒙版上的实际大小。基于此Shader创建一个Material命名为Mat_BrushDraw并为其_BrushTex属性赋上之前准备的圆形笔刷纹理。3.4 编写合成Shader最终显示这个Shader用于将背景图和刮擦蒙版合成显示最终效果。创建另一个Unlit Shader命名为ScratchCardDisplay。修改Shader如下Shader Unlit/ScratchCardDisplay { Properties { _BackgroundTex (Background Texture, 2D) white {} // 背景图奖品 _MaskTex (Mask Render Texture, 2D) black {} // 刮擦蒙版 _CoverColor (Cover Color, Color) (0.5,0.5,0.5,1) // 涂层颜色 _Threshold (Threshold, Range(0, 1)) 0.1 // 刮开阈值 } SubShader { Tags { RenderTypeOpaque QueueTransparent} Blend SrcAlpha OneMinusSrcAlpha // 如果需要透明背景 Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; }; sampler2D _BackgroundTex; sampler2D _MaskTex; fixed4 _CoverColor; float _Threshold; v2f vert (appdata v) { v2f o; o.vertex UnityObjectToClipPos(v.vertex); o.uv v.uv; return o; } fixed4 frag (v2f i) : SV_Target { // 采样背景纹理 fixed4 bgColor tex2D(_BackgroundTex, i.uv); // 采样蒙版纹理R通道即可因为我们是灰度图 float maskValue tex2D(_MaskTex, i.uv).r; // 判断逻辑如果蒙版值大于阈值则显示背景否则显示涂层 // 可以使用smoothstep实现边缘平滑过渡 float revealFactor smoothstep(_Threshold - 0.05, _Threshold 0.05, maskValue); // 混合颜色 fixed4 finalColor lerp(_CoverColor, bgColor, revealFactor); // 如果背景图有透明通道可以再与bgColor.a相乘 finalColor.a bgColor.a; return finalColor; } ENDCG } } }这个Shader的逻辑很清晰对每个像素同时查看背景图和蒙版图。蒙版图该像素点的亮度maskValue代表了被刮开的程度。通过一个可调的_Threshold我们决定多“亮”才算被刮开并使用lerp函数在涂层色和背景色之间进行平滑混合。创建材质Mat_ScratchDisplay并为其_MaskTex属性赋值为ScratchMaskRT_BackgroundTex赋值为你的奖品背景图。3.5 实现核心绘制逻辑C#脚本现在我们需要将输入鼠标/触摸转化为对蒙版纹理的绘制命令。修改ScratchCardManager.cs。using UnityEngine; public class ScratchCardManager : MonoBehaviour { public RenderTexture scratchMaskRT; public Material brushDrawMaterial; // Mat_BrushDraw材质 public float brushSize 0.05f; // 笔刷大小与Shader中的_BrushSize对应 public Camera renderCamera; // 用于将屏幕坐标转换到Render Texture空间的相机 private bool isDragging false; private Vector2 lastMousePos; void Start() { InitializeMaskTexture(); if (brushDrawMaterial ! null) { brushDrawMaterial.SetFloat(_BrushSize, brushSize); } } void InitializeMaskTexture(){ /* 同上 */ } void Update() { HandleInput(); } void HandleInput() { // 鼠标/触摸按下 if (Input.GetMouseButtonDown(0)) { isDragging true; lastMousePos Input.mousePosition; DrawBrush(lastMousePos); } // 鼠标/触摸拖动 else if (Input.GetMouseButton(0) isDragging) { Vector2 currentMousePos Input.mousePosition; // 简单的线性插值在两点间连续绘制避免笔触断开 float distance Vector2.Distance(lastMousePos, currentMousePos); if (distance 1f) // 防止距离过近时采样点过多 { int steps Mathf.CeilToInt(distance / 5f); // 每5像素一个插值点 for (int i 0; i steps; i) { Vector2 lerpPos Vector2.Lerp(lastMousePos, currentMousePos, i / (float)steps); DrawBrush(lerpPos); } } else { DrawBrush(currentMousePos); } lastMousePos currentMousePos; } // 鼠标/触摸抬起 else if (Input.GetMouseButtonUp(0)) { isDragging false; } } void DrawBrush(Vector2 screenPosition) { if (scratchMaskRT null || brushDrawMaterial null || renderCamera null) return; // 1. 将屏幕坐标转换为世界坐标在renderCamera的近平面上 Ray ray renderCamera.ScreenPointToRay(screenPosition); Plane canvasPlane new Plane(Vector3.forward, Vector3.zero); // 假设我们的刮卡画布在XY平面Z0 float enter; if (canvasPlane.Raycast(ray, out enter)) { Vector3 worldPos ray.GetPoint(enter); // 2. 创建一个临时四边形Mesh代表笔刷 Mesh brushMesh CreateBrushMesh(worldPos); // 3. 设置渲染目标到我们的蒙版纹理 RenderTexture previousRT RenderTexture.active; RenderTexture.active scratchMaskRT; // 4. 设置材质属性如果需要传递世界位置等可以在这里设置 // brushDrawMaterial.SetVector(_Center, worldPos); // 5. 使用当前材质绘制Mesh到激活的RenderTexture brushDrawMaterial.SetPass(0); Graphics.DrawMeshNow(brushMesh, Matrix4x4.identity); // 6. 恢复之前的渲染目标 RenderTexture.active previousRT; // 7. 销毁临时Mesh避免内存泄漏或在对象池中回收 DestroyImmediate(brushMesh); } } Mesh CreateBrushMesh(Vector3 center) { Mesh mesh new Mesh(); float halfSize 0.5f; // 对应Shader中UV从-0.5到0.5的范围 Vector3[] vertices new Vector3[4] { center new Vector3(-halfSize, -halfSize, 0), center new Vector3(-halfSize, halfSize, 0), center new Vector3(halfSize, -halfSize, 0), center new Vector3(halfSize, halfSize, 0) }; Vector2[] uv new Vector2[4] { new Vector2(0, 0), new Vector2(0, 1), new Vector2(1, 0), new Vector2(1, 1) }; int[] triangles new int[6] { 0, 1, 2, 2, 1, 3 }; mesh.vertices vertices; mesh.uv uv; mesh.triangles triangles; return mesh; } }这段代码是核心驱动逻辑。HandleInput方法处理连续的输入并在拖动时在两点间插值确保绘制连贯。DrawBrush方法负责将一次点击或一个插值点转换为一次绘制调用将屏幕坐标转换到我们假设的“刮卡画布”世界空间Z0平面。动态生成一个以该点为中心的小四边形Mesh。将渲染目标切换为我们的蒙版Render Texture。使用笔刷材质(Mat_BrushDraw)将这个四边形绘制上去。由于我们的笔刷Shader是根据四边形内的UV来显示笔刷纹理的因此这个四边形的大小就决定了笔刷在蒙版上的实际尺寸。绘制完成后恢复渲染目标。3.6 场景搭建与测试在场景中创建一个QuadGameObject - 3D Object - Quad重命名为ScratchCard。将其位置设为(0,0,0)缩放设为(5,5,1)模拟一个卡片大小。将材质Mat_ScratchDisplay拖拽给这个Quad的Mesh Renderer组件。创建一个新的Camera重命名为RenderCamera。调整其位置使其正对着ScratchCard例如位置(0,0,-10)旋转(0,0,0)。将其Projection设置为Orthographic正交投影并调整Size使得Quad完整位于视野内例如Size3。关键一步将这个RenderCamera的Target Texture设置为我们的ScratchMaskRT。这样这个相机渲染的内容就会输出到蒙版纹理但我们不直接用这个相机来画笔刷。我们只是借用它的视口矩阵进行坐标转换。务必禁用此相机取消勾选Camera组件或将其Culling Mask设置为Nothing防止它干扰主相机渲染。将ScratchCardManager脚本挂载到场景中并将scratchMaskRT、brushDrawMaterialMat_BrushDraw和renderCameraRenderCamera拖拽赋值。运行游戏。点击或拖动鼠标你应该能看到灰色的Quad上被“刮”出底图的内容。4. 性能优化与进阶功能基础功能跑通后我们面临两个现实问题性能和功能扩展。上面的简单实现每帧可能产生多次DrawMeshNow调用和Mesh创建/销毁在移动端是吃不消的。4.1 性能优化实战优化1使用CommandBuffer与GPU Instancing直接每帧调用Graphics.DrawMeshNow会产生大量单独的绘制调用。我们可以使用CommandBuffer来批量提交绘制命令并结合GPU Instancing来合并相同材质的绘制。// 在ScratchCardManager中增加 private CommandBuffer commandBuffer; private ListMatrix4x4 brushMatrices new ListMatrix4x4(); // 存储本帧所有笔刷的位置矩阵 private Mesh staticBrushMesh; // 一个共享的笔刷Mesh void Start() { // ... 其他初始化 commandBuffer new CommandBuffer { name ScratchCardDraw }; staticBrushMesh CreateBrushMesh(Vector3.zero); // 创建一个原点处的标准Mesh } void LateUpdate() // 在每帧渲染前收集所有绘制命令 { if (brushMatrices.Count 0) return; // 清除RT如果需要每帧累积则不清除这里我们选择累积效果 // commandBuffer.ClearRenderTarget(true, true, Color.black); // 设置渲染目标 commandBuffer.SetRenderTarget(scratchMaskRT); // 设置材质 commandBuffer.SetViewMatrix(renderCamera.worldToCameraMatrix); commandBuffer.SetProjectionMatrix(renderCamera.projectionMatrix); // 使用GPU Instancing批量绘制 MaterialPropertyBlock props new MaterialPropertyBlock(); // 可以在这里设置每实例的属性如果需要不同的笔刷大小或颜色 for (int i 0; i brushMatrices.Count; i 1023) // 每个批次最多1023个实例 { int count Mathf.Min(1023, brushMatrices.Count - i); commandBuffer.DrawMeshInstanced(staticBrushMesh, 0, brushDrawMaterial, 0, brushMatrices.GetRange(i, count).ToArray(), count, props); } // 执行命令缓冲区 Graphics.ExecuteCommandBuffer(commandBuffer); commandBuffer.Clear(); // 清空本帧的笔刷矩阵列表 brushMatrices.Clear(); } void DrawBrush(Vector2 screenPosition) { // ... 坐标转换部分不变得到worldPos // 不再立即绘制而是将变换矩阵加入列表 Matrix4x4 matrix Matrix4x4.TRS(worldPos, Quaternion.identity, Vector3.one * brushSize); // 缩放控制大小 brushMatrices.Add(matrix); }这个优化将每帧的多次绘制调用合并为一次或几次Instancing调用性能提升巨大。注意brushDrawMaterial需要支持GPU Instancing在其Shader的Properties块上方添加[PerRendererData]并确保使用合适的变体。优化2对象池与Mesh复用避免在DrawBrush中频繁创建和销毁Mesh。使用一个简单的对象池来管理一个共享的笔刷Mesh。优化3降低绘制分辨率与频率对于移动端如果刮卡区域很大可以考虑使用分辨率低一档的Render Texture。同时可以在HandleInput中限制绘制频率例如每帧只处理一个点或者在拖动时每N像素采样一个点而不是连续插值。4.2 实现刮开比例统计运营经常需要知道用户刮开了多少面积。这需要读取Render Texture的数据进行计算。切记从GPU读取纹理到CPU是极其耗时的操作ReadPixels绝不能每帧进行。正确做法使用异步计算与节流使用ComputeShader这是最高效的方式。编写一个ComputeShader对蒙版纹理进行并行遍历统计白色像素刮开部分的数量。将结果写入一个ComputeBuffer然后在C#中间隔数帧如每秒1-2次去读取这个Buffer。使用协程与低频率采样如果不用ComputeShader可以创建一个低分辨率的临时纹理如32x32使用Graphics.CopyTexture从蒙版RT快速拷贝到临时RTGPU间拷贝较快然后使用AsyncGPUReadback异步读取这个小纹理的数据在回调函数中计算比例。这个操作可以每0.5秒或1秒进行一次。// 伪代码示例使用AsyncGPUReadback进行低频采样 IEnumerator CalculateScratchRatioCoroutine() { while (true) { yield return new WaitForSeconds(0.5f); // 每0.5秒计算一次 if (scratchMaskRT null) yield break; // 创建一个低分辨率临时RT RenderTexture tempRT RenderTexture.GetTemporary(32, 32, 0, RenderTextureFormat.R8); Graphics.Blit(scratchMaskRT, tempRT); // 快速缩放拷贝 // 异步读取 AsyncGPUReadback.Request(tempRT, 0, TextureFormat.R8, OnReadbackComplete); RenderTexture.ReleaseTemporary(tempRT); } } void OnReadbackComplete(AsyncGPUReadbackRequest request) { if (request.hasError) { Debug.LogError(GPU readback error!); return; } var data request.GetDatabyte(); int totalPixels data.Length; int scratchedPixels 0; for (int i 0; i data.Length; i) { if (data[i] 10) // 阈值判断大于10即认为被刮开 scratchedPixels; } float ratio (float)scratchedPixels / totalPixels; Debug.Log($Scratched Ratio: {ratio:P2}); // 可以触发事件更新UI等 }4.3 常见问题与排查技巧实录问题刮擦没有效果蒙版一直是黑色。排查首先检查ScratchCardManager中的renderCamera是否赋值正确并且其worldToCameraMatrix能正确覆盖刮卡物体。在DrawBrush方法中Debug.DrawRay画出射线看是否与画布平面相交。其次检查笔刷材质Mat_BrushDraw的Shader是否编译正确其渲染队列是否透明混合。最后在DrawBrush中Graphics.DrawMeshNow前后添加Debug.Log确认绘制函数被调用。问题笔刷绘制有延迟或断点。排查这是输入采样和插值的问题。确保在Update中处理输入LateUpdate中执行绘制。增加HandleInput中的插值步数steps变量。如果使用了优化方案确保brushMatrices列表在每帧LateUpdate后正确清空。问题在WebGL或部分移动设备上效果异常或性能极差。排查浮点数精度在Shader中避免使用float尽量用half或fixed特别是在片段着色器中。Render Texture格式尝试使用R8格式如果支持代替RGBA32节省带宽。Instancing兼容性部分低端GPU对Instancing支持不好。可以回退到使用Graphics.DrawMesh循环但通过MaterialPropertyBlock传递位置信息减少SetPassCall。抗锯齿确保scratchMaskRT的Anti-aliasing为None。图形API在Player Settings中对于Android/iOS优先使用Vulkan/Metal它们对Render Texture的操作通常比OpenGL ES更高效。问题刮开边缘有锯齿不光滑。解决这主要是笔刷纹理和合成Shader的阈值处理问题。使用一张边缘渐变更柔和的笔刷纹理。在合成Shader的frag函数中不要用if(maskValue _Threshold)这样的硬判断而是像我示例中那样使用smoothstep进行平滑过渡。smoothstep会在阈值附近产生一个平滑的渐变区间视觉上边缘就柔和了。问题如何实现“刮开区域平均色”这种炫酷功能思路这同样需要ComputeShader。准备两个Buffer一个AccumulateBufferfloat4类型累加RGB和计数一个ResultBuffer。ComputeShader遍历蒙版纹理对于刮开区域maskValue threshold将其对应的背景图像素颜色累加到AccumulateBuffer中。最后将累加结果除以计数得到平均色。这个计算非常重务必在低频如刮卡结束时触发一次或使用极低分辨率的下采样纹理进行计算。这套从原理到实现再到优化和排坑的完整方案是我经过多个项目迭代总结出来的。它已经足够应对大多数中高复杂度的刮刮乐需求。记住理解“Render Texture作为动态画布”这个核心思想比记住代码更重要。你可以在此基础上发挥创意实现更多样的交互效果比如不同硬度的笔刷、刮开时粒子特效、甚至刮出特定图案触发奖励等。