Cocos Creator Mask组件详解:从原理到实战实现刮刮卡效果 1. 项目概述为什么我们需要Mask组件在Cocos Creator里做UI或者特效尤其是新手阶段经常会遇到一个让人头疼的问题我画了一个圆形的头像框但图片是方形的四个角露出来了我想做一个滚动公告但希望文字只在特定的矩形区域内显示超出部分别让我看见更经典的是刮刮卡效果刮开前是一层涂层刮开后露出底下的奖品图片。这些场景的核心需求其实就一个如何把显示内容限制在一个特定的形状区域内区域外的部分统统隐藏掉这个需求光靠调整节点位置和大小是搞不定的。比如那个圆形头像你总不能把一张方形图片的四个角用PS抠掉吧那样既不灵活性能也差。这时候Cocos Creator内置的Mask遮罩组件就派上用场了。你可以把它理解为一个“裁剪模具”或者“蒙版”。它定义了一个可见区域只有在这个区域内的子节点内容才会被渲染出来区域外的像素则被“遮住”或“隐藏”了。Mask组件是Cocos Creator中实现这类“局部显示”效果最基础、最核心的组件之一。无论是简单的UI裁剪还是复杂的交互效果如刮刮卡、雷达图、技能冷却等其底层往往都离不开Mask的支撑。理解并熟练运用Mask是从“能做出东西”到“能做好东西”的关键一步。这篇文章我就结合自己踩过的坑和实战经验带你彻底搞懂Mask组件并实现一个完整的刮刮卡案例。2. Mask组件核心原理与类型详解Mask组件的工作原理简单说就是利用模板测试Stencil Test。这是图形渲染管线中的一个环节。你可以想象Mask节点自己先画出一个形状比如矩形或圆形这个画的过程会在一个叫“模板缓冲区”的地方做上标记。然后它的所有子节点在渲染时会去检查这个缓冲区只有对应位置有标记即在Mask形状内的像素才会被画到屏幕上没标记的形状外就直接跳过。这个过程是GPU硬件加速的效率非常高。在Cocos Creator中Mask组件主要提供三种类型的遮罩对应不同的“模具”形状2.1 矩形遮罩 (RECT)这是最常用、性能最好的遮罩类型。它定义一个轴对齐的矩形区域。原理根据节点本身的contentSize内容尺寸来确定矩形范围。应用场景所有需要矩形裁剪的地方。例如滚动视图ScrollView的内容容器确保内容只在滚动视窗内显示。聊天框限制文本和表情的显示范围。技能图标冷却一个灰色的矩形遮罩从上往下移动覆盖技能图标。注意事项矩形的轴心点Anchor会影响遮罩的起始位置。通常我们会把轴心点设为(0, 0)或(0.5, 0.5)具体看需求。修改节点的scale属性会同步缩放遮罩区域。如果你只想缩放子节点内容而不想改变遮罩范围需要将Mask节点和内容节点分开层级。2.2 圆形遮罩 (ELLIPSE)定义一个圆形或椭圆形区域。原理以节点中心为圆心以节点宽度和高度为椭圆的长短轴绘制一个椭圆区域。当宽高相等时就是正圆形。应用场景圆形头像这是最典型的应用。将一张方形图片作为Mask节点的子节点即可得到圆形头像。圆形进度条结合脚本动态修改遮罩范围可以实现圆形的血量条、加载进度条。雷达图/波图多个扇形或圆形区域的组合显示。注意事项圆形遮罩的性能开销比矩形遮罩稍大。边缘锯齿问题在低分辨率设备上圆形边缘可能出现锯齿。可以通过开启抗锯齿项目设置中或为Mask节点添加一个略大的、同样形状的边框背景图来视觉上缓解。2.3 图像遮罩 (GRAPHICS)这是最灵活也是最需要手动控制的一种类型。原理通过调用Graphics组件的绘图API如rect,circle,lineTo,fill等动态绘制出一个任意形状的多边形区域作为遮罩。应用场景不规则形状遮罩如星形、心形、多边形头像框。刮刮卡效果通过触摸轨迹动态擦除绘制遮罩层。自定义形状的进度条。注意事项性能敏感每一帧重绘Graphics例如在update中开销很大必须谨慎优化。对于静态形状绘制一次即可对于动态变化的形状如刮刮卡需要控制重绘的区域和频率。需要脚本驱动你必须编写代码来定义绘制的形状。重要选择SECTOR类型去哪了在Cocos Creator的早期版本如v1.x中存在SECTOR扇形遮罩类型。但在v2.x及以后的版本中官方移除了这个类型。这是因为扇形遮罩完全可以通过更强大的GRAPHICS类型绘制出来且GRAPHICS更通用。如果你需要扇形遮罩比如做技能释放范围指示器请直接使用GRAPHICS类型并用arc方法绘制扇形。3. 实战从零构建一个刮刮卡效果理论说再多不如动手做一遍。我们来实现一个经典的刮刮卡效果上层是一层灰色涂层用户用手指或鼠标刮开涂层露出底下的中奖信息图片。3.1 场景结构与节点准备创建UI节点新建一个场景创建一个Canvas下的空节点命名为ScratchCard。创建底层奖品层在ScratchCard下创建一个Sprite节点命名为Prize。为其添加一个Sprite组件导入一张作为奖品的图片比如“恭喜中奖”的文字图。创建Mask层在ScratchCard下创建一个空节点命名为MaskLayer。这个节点将承载我们的遮罩。为MaskLayer节点添加Mask组件。将Type属性设置为GRAPHICS。同时勾选Inverted反向属性。这是关键默认遮罩是显示区域内内容隐藏区域外。而刮刮卡逻辑是涂层我们接下来要画的一开始覆盖全部刮掉的地方才显示奖品。所以我们需要“反向遮罩”——即Graphics画出的部分不显示奖品没画到的部分才显示。接着为MaskLayer节点添加Graphics组件。我们将用这个组件来绘制涂层。创建涂层表面层在MaskLayer节点下创建一个Sprite节点命名为Cover。为其添加Sprite组件使用一张灰色或带有“刮开”纹理的图片。这个涂层将作为视觉上的刮刮卡涂层它的显示不受Mask影响但我们会通过Mask来控制它底下的奖品层的显示。最终的节点树应该是这样的Canvas └── ScratchCard (Node) ├── Prize (Sprite) // 奖品图 └── MaskLayer (Node) // 遮罩层 ├── Mask (Component, Type: GRAPHICS, Inverted: true) ├── Graphics (Component) └── Cover (Sprite) // 视觉上的涂层3.2 编写刮刮卡交互脚本在MaskLayer节点上创建一个新的TypeScript脚本命名为ScratchCardMask.ts。import { _decorator, Component, Graphics, Mask, Input, input, EventTouch, Vec2, Color } from cc; const { ccclass, property } _decorator; ccclass(ScratchCardMask) export class ScratchCardMask extends Component { // 获取Graphics组件引用用于绘图 private _graphics: Graphics null!; // 获取Mask组件引用 private _mask: Mask null!; // 上一次触摸点的世界坐标用于画线 private _lastPos: Vec2 | null null; // 刮擦的笔触半径 property({ tooltip: 刮擦的笔触半径 }) public brushRadius: number 20; start() { // 初始化组件引用 this._graphics this.node.getComponent(Graphics)!; this._mask this.node.getComponent(Mask)!; // 确保Mask类型是GRAPHICS this._mask.type Mask.Type.GRAPHICS; this._mask.inverted true; // 必须开启反向 // 初始化Graphics绘制一个覆盖全屏的矩形作为初始涂层即全部遮住奖品 this._initMaskCover(); // 注册触摸事件监听 input.on(Input.EventType.TOUCH_START, this.onTouchStart, this); input.on(Input.EventType.TOUCH_MOVE, this.onTouchMove, this); input.on(Input.EventType.TOUCH_END, this.onTouchEnd, this); } onDestroy() { // 记得移除事件监听防止内存泄漏 input.off(Input.EventType.TOUCH_START, this.onTouchStart, this); input.off(Input.EventType.TOUCH_MOVE, this.onTouchMove, this); input.off(Input.EventType.TOUCH_END, this.onTouchEnd, this); } // 初始化用黑色填充整个节点区域这样奖品就完全被遮住了 private _initMaskCover() { const g this._graphics; g.clear(); // 清除之前的绘制 // 获取当前节点的包围盒在本地坐标系 // 这里我们简单画一个足够大的矩形覆盖整个可能区域 // 实际项目中可能需要根据奖品图大小动态计算 g.rect(-1000, -1000, 2000, 2000); // 画一个很大的矩形 g.fillColor Color.BLACK; // 填充颜色颜色本身不重要重要的是填充了这个区域 g.fill(); // 重要调用mask的updateGraphics方法通知遮罩更新 this._mask.updateGraphics(); } // 触摸开始 private onTouchStart(event: EventTouch) { const touchPos event.getUILocation(); // 获取UI坐标系下的触摸点 this._drawCircle(touchPos); // 在触摸点画一个圆表示刮开了一个点 this._lastPos touchPos; // 记录为上一次位置为画线做准备 } // 触摸移动 private onTouchMove(event: EventTouch) { if (!this._lastPos) return; const touchPos event.getUILocation(); // 在上一位置和当前位置之间画线用连续的圆来模拟笔触 this._drawLine(this._lastPos, touchPos); this._lastPos touchPos; } // 触摸结束 private onTouchEnd() { this._lastPos null; // 清空上一次位置 } // 在指定位置世界坐标画一个圆实际上是绘制一个“洞” private _drawCircle(pos: Vec2) { const g this._graphics; // 1. 将世界坐标转换到Graphics节点的本地坐标 const localPos this.node.inverseTransformPoint(new Vec3(pos.x, pos.y, 0)); // 2. 设置绘制模式为“打洞”。这是关键 // setFillRule是Graphics的APIGraphics.FillRule.EVEN_ODD是奇偶填充规则。 // 简单理解后画的图形如果和先画的图形有重叠重叠部分会被“挖空”。 g.setFillRule(Graphics.FillRule.EVEN_ODD); // 3. 先“保留”原有填充这里其实不需要额外操作因为_initMaskCover已经画了一个大矩形 // 4. 在触摸点位置画一个圆并填充。由于使用了奇偶规则这个圆区域会从之前的大矩形中被“减去”。 g.circle(localPos.x, localPos.y, this.brushRadius); g.fillColor Color.BLACK; // 颜色任意填充只是为了应用“打洞”规则 g.fill(); // 5. 通知Mask更新 this._mask.updateGraphics(); } // 在两点之间画线用连续的圆模拟 private _drawLine(startPos: Vec2, endPos: Vec2) { const g this._graphics; g.setFillRule(Graphics.FillRule.EVEN_ODD); // 计算两点之间的距离和方向 const deltaX endPos.x - startPos.x; const deltaY endPos.y - startPos.y; const distance Math.sqrt(deltaX * deltaX deltaY * deltaY); const step this.brushRadius * 0.5; // 步长决定圆的密度 if (distance 0) { const segments Math.ceil(distance / step); for (let i 0; i segments; i) { const t i / segments; const x startPos.x deltaX * t; const y startPos.y deltaY * t; // 将每个插值点转换到本地坐标并画圆 const localPos this.node.inverseTransformPoint(new Vec3(x, y, 0)); g.circle(localPos.x, localPos.y, this.brushRadius); } g.fillColor Color.BLACK; g.fill(); this._mask.updateGraphics(); } } // 提供一个重置刮卡的方法 public resetScratch() { this._initMaskCover(); } }3.3 关键逻辑解析与避坑指南Inverted反向属性的核心作用这是实现刮刮卡逻辑的灵魂。我们视觉上有一个涂层Cover逻辑上有一个遮罩Mask。我们希望用户“刮掉”涂层看到奖品。在代码里我们实际上是在Graphics上“挖洞”。Inverted: false默认时画出的图形区域是显示奖品的。这不符合直觉。Inverted: true时画出的图形区域不显示奖品即被遮住而画布上其他没画到的区域则显示奖品。这正好对应了“刮掉涂层挖洞露出奖品”的流程。Graphics.FillRule.EVEN_ODD奇偶填充规则这是实现“挖洞”效果的技术关键。默认的填充规则是Graphics.FillRule.NON_ZERO后画的图形会直接覆盖先画的。而EVEN_ODD规则下从一点向外发射射线穿过图形边界奇数个交点算内部偶数个算外部。当我们先画一个大矩形全遮罩再在上面画一个小圆时小圆区域的重叠部分会被计算为“外部”从而实现“挖洞”。你可以尝试注释掉g.setFillRule(Graphics.FillRule.EVEN_ODD);这行会发现刮擦无效因为圆只是覆盖了上去没有形成空洞。坐标转换触摸事件获取的坐标是UI世界坐标。而Graphics绘图是在其节点本地坐标系下进行的。必须使用inverseTransformPoint方法进行转换否则画的位置会完全不对。这是新手最容易忽略导致“刮不动”或“刮的位置错位”的原因。性能优化在_drawLine方法中我们通过插值画多个圆来模拟连续线条。step步长越小线条越连续平滑但需要绘制的圆越多性能开销越大。需要根据brushRadius笔刷半径来权衡。通常步长为半径的0.3-0.5倍即可取得较好效果。另外updateGraphics的调用要节制我们在每次绘制操作fill后调用一次而不是在for循环里每画一个圆就调用一次。重置功能resetScratch方法通过重新调用_initMaskCover来清空所有绘制恢复初始的全遮盖状态。这在游戏需要重置刮卡时非常有用。4. Mask组件深度使用技巧与常见问题排查掌握了基础用法和实战后我们再来深入一些高级技巧和常见坑点。4.1 性能优化要点Mask组件虽然方便但滥用或误用会对性能造成显著影响尤其是在移动设备上。层级合并与Draw CallMask会打断合批。简单说引擎为了优化会尽量将相同材质的精灵合并一次绘制一个Draw Call。但如果两个精灵中间隔着一个Mask节点它们就无法合并了导致Draw Call增加。优化建议尽可能将需要被相同遮罩裁剪的节点放在同一个Mask节点下并确保它们的渲染顺序连续。避免一个Mask只包含一个子节点然后大量使用这样的结构。GRAPHICS类型的重绘开销动态Graphics每帧变化是性能杀手。对于刮刮卡这种必须动态绘制的场景要控制绘制频率和区域。例如可以积累一定数量的触摸点再进行一次批量绘制而不是每帧都画。遮罩区域大小尽量让Mask节点的尺寸contentSize精确匹配需要显示的区域不要设置得过大。过大的遮罩区域意味着更多的像素需要进行模板测试。慎用嵌套Mask多个Mask嵌套使用会导致模板测试多次进行性能开销成倍增加。除非绝对必要否则应避免。4.2 与其他组件的配合与冲突与Widget对齐挂件的配合Mask节点本身可以使用Widget组件来定位。但要注意Mask的裁剪区域是基于其节点变换后的最终区域。如果子节点也使用了Widget要确保计算无误防止子节点跑出遮罩范围。与Spine/Skeleton动画Mask可以正常裁剪Spine骨骼动画。但需要注意如果Spine动画的边界计算bounding box很大即使视觉上在遮罩内也可能因为边界超出而被错误裁剪。可以在Spine软件中优化网格边界或在Cocos Creator中调整Mask大小。与粒子系统ParticleSystemMask可以裁剪粒子但性能开销需要关注。大量粒子与动态Mask结合时建议进行充分的性能测试。4.3 常见问题排查速查表问题现象可能原因解决方案遮罩完全不起作用子节点全部显示1. Mask组件未启用enabled为false。2. 子节点不在Mask节点层级之下。3. 对于GRAPHICS类型未调用updateGraphics()。1. 检查Inspector中Mask组件的勾选框。2. 在层级管理器中将子节点拖拽为Mask节点的子级。3. 在绘制代码后手动调用this.mask.updateGraphics()。遮罩区域显示反了想显示的部分被隐藏Inverted属性设置错误。检查Inverted属性。对于刮刮卡这类“擦除”效果通常需要设为true。GRAPHICS遮罩绘制的位置不对绘图坐标未进行坐标转换。确保将触摸或逻辑的世界坐标/UI坐标通过node.inverseTransformPoint()转换为Graphics节点的本地坐标后再绘图。圆形/图像遮罩边缘有锯齿分辨率较低且抗锯齿未开启。1. 在项目设置 - 项目数据 - 抗锯齿中开启抗锯齿。2. 美术资源为被遮罩的图片添加一个1-2像素的透明外边框出血。3. 使用Sprite的Sliced模式并设置合适的inset有时能改善边缘。使用Mask后游戏变得很卡1. 嵌套使用了多个Mask。2. 动态GRAPHICS遮罩每帧重绘且区域很大。3. Mask节点下子节点过多、材质各异导致Draw Call暴涨。1. 检查节点层级消除不必要的嵌套Mask。2. 优化动态绘制的逻辑减少绘制调用和区域。3. 使用渲染分组调试工具如Cocos Creator内置的cc.renderer信息查看Draw Call合并同材质子节点调整渲染顺序。在ScrollView中使用Mask滚动时内容闪烁或裁剪异常可能是Mask节点与ScrollView的content节点在变换更新顺序上存在冲突。1. 确保Mask节点是content的直接子节点且结构清晰。2. 尝试调整节点或组件在Inspector中的Priority优先级。3. 在极端情况下可以考虑使用RectMask代替GraphicsMask来实现滚动区域的裁剪。Web/Mobile平台效果不一致不同平台对图形APIWebGL, OpenGL ES的实现有细微差异特别是Graphics的填充规则。1. 尽量使用简单的图形矩形、圆。2. 避免使用过于复杂的Graphics路径。3. 在所有目标平台进行测试。4.4 进阶思路非交互式动态遮罩Mask组件不仅能用于交互还能用于创造纯视觉的动态效果。例如实现一个波浪形的进度条创建一个使用GRAPHICS类型Mask的节点其子节点是一个填充了颜色的长条形Sprite进度条底色。编写脚本在update中使用Graphics的bezierCurveTo或一系列lineTo绘制一条正弦曲线路径。将这条路径填充。由于Mask的作用只有曲线下方的条形Sprite部分会显示出来。通过随时间偏移正弦曲线的相位就能产生波浪上升的动画效果。这里的关键是控制重绘频率可以每几帧重绘一次而不是每帧都重绘以平衡效果和性能。Mask组件是Cocos Creator中一个强大而灵活的“空间裁剪”工具。理解其“模板测试”的底层原理能帮助你更准确地预测其行为。从简单的头像裁剪到复杂的刮刮卡交互其核心都在于对“可见区域”的定义与控制。在实际项目中多思考是否真的需要Mask以及如何以最节省性能的方式使用它是成为一名优秀Cocos开发者的必经之路。