news 2026/8/18 4:04:48

Cocos Creator Mask组件详解:从原理到实战实现刮刮卡效果

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 场景结构与节点准备

  1. 创建UI节点:新建一个场景,创建一个Canvas下的空节点,命名为ScratchCard
  2. 创建底层(奖品层):在ScratchCard下创建一个Sprite节点,命名为Prize。为其添加一个Sprite组件,导入一张作为奖品的图片(比如“恭喜中奖”的文字图)。
  3. 创建Mask层:在ScratchCard下创建一个空节点,命名为MaskLayer。这个节点将承载我们的遮罩。
    • MaskLayer节点添加Mask组件。将Type属性设置为GRAPHICS。同时,勾选Inverted(反向)属性。这是关键!默认遮罩是显示区域内内容,隐藏区域外。而刮刮卡逻辑是:涂层(我们接下来要画的)一开始覆盖全部,刮掉的地方才显示奖品。所以我们需要“反向遮罩”——即Graphics画出的部分不显示奖品,没画到的部分才显示。
    • 接着,为MaskLayer节点添加Graphics组件。我们将用这个组件来绘制涂层。
  4. 创建涂层(表面层):在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的API,`Graphics.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 关键逻辑解析与避坑指南

  1. Inverted(反向)属性的核心作用:这是实现刮刮卡逻辑的灵魂。我们视觉上有一个涂层(Cover),逻辑上有一个遮罩(Mask)。我们希望用户“刮掉”涂层,看到奖品。在代码里,我们实际上是在Graphics上“挖洞”。Inverted: false(默认)时,画出的图形区域是显示奖品的。这不符合直觉。Inverted: true时,画出的图形区域不显示奖品(即被遮住),而画布上其他没画到的区域则显示奖品。这正好对应了“刮掉涂层(挖洞),露出奖品”的流程。

  2. Graphics.FillRule.EVEN_ODD(奇偶填充规则):这是实现“挖洞”效果的技术关键。默认的填充规则是Graphics.FillRule.NON_ZERO,后画的图形会直接覆盖先画的。而EVEN_ODD规则下,从一点向外发射射线,穿过图形边界,奇数个交点算内部,偶数个算外部。当我们先画一个大矩形(全遮罩),再在上面画一个小圆时,小圆区域的重叠部分会被计算为“外部”,从而实现“挖洞”。你可以尝试注释掉g.setFillRule(Graphics.FillRule.EVEN_ODD);这行,会发现刮擦无效,因为圆只是覆盖了上去,没有形成空洞。

  3. 坐标转换:触摸事件获取的坐标是UI世界坐标。而Graphics绘图是在其节点本地坐标系下进行的。必须使用inverseTransformPoint方法进行转换,否则画的位置会完全不对。这是新手最容易忽略导致“刮不动”或“刮的位置错位”的原因。

  4. 性能优化:在_drawLine方法中,我们通过插值画多个圆来模拟连续线条。step(步长)越小,线条越连续平滑,但需要绘制的圆越多,性能开销越大。需要根据brushRadius(笔刷半径)来权衡。通常步长为半径的0.3-0.5倍即可取得较好效果。另外,updateGraphics的调用要节制,我们在每次绘制操作(fill)后调用一次,而不是在for循环里每画一个圆就调用一次。

  5. 重置功能resetScratch方法通过重新调用_initMaskCover来清空所有绘制,恢复初始的全遮盖状态。这在游戏需要重置刮卡时非常有用。

4. Mask组件深度使用技巧与常见问题排查

掌握了基础用法和实战后,我们再来深入一些高级技巧和常见坑点。

4.1 性能优化要点

Mask组件虽然方便,但滥用或误用会对性能造成显著影响,尤其是在移动设备上。

  • 层级合并与Draw Call:Mask会打断合批。简单说,引擎为了优化,会尽量将相同材质的精灵合并一次绘制(一个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大小。
  • 与粒子系统(ParticleSystem):Mask可以裁剪粒子,但性能开销需要关注。大量粒子与动态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. 使用SpriteSliced模式并设置合适的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平台效果不一致不同平台对图形API(WebGL, OpenGL ES)的实现有细微差异,特别是Graphics的填充规则。1. 尽量使用简单的图形(矩形、圆)。
2. 避免使用过于复杂的Graphics路径。
3. 在所有目标平台进行测试。

4.4 进阶思路:非交互式动态遮罩

Mask组件不仅能用于交互,还能用于创造纯视觉的动态效果。例如,实现一个波浪形的进度条:

  1. 创建一个使用GRAPHICS类型Mask的节点,其子节点是一个填充了颜色的长条形Sprite(进度条底色)。
  2. 编写脚本,在update中,使用GraphicsbezierCurveTo或一系列lineTo绘制一条正弦曲线路径。
  3. 将这条路径填充。由于Mask的作用,只有曲线下方的条形Sprite部分会显示出来。
  4. 通过随时间偏移正弦曲线的相位,就能产生波浪上升的动画效果。这里的关键是控制重绘频率,可以每几帧重绘一次,而不是每帧都重绘,以平衡效果和性能。

Mask组件是Cocos Creator中一个强大而灵活的“空间裁剪”工具。理解其“模板测试”的底层原理,能帮助你更准确地预测其行为。从简单的头像裁剪到复杂的刮刮卡交互,其核心都在于对“可见区域”的定义与控制。在实际项目中,多思考是否真的需要Mask,以及如何以最节省性能的方式使用它,是成为一名优秀Cocos开发者的必经之路。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/18 4:03:06

AI数字人舞蹈视频生成:从开源工具到本地部署全流程指南

这次我们来看一个名为《峰哥胜诉之舞》的项目&#xff0c;这是一个基于开源技术实现的AI视频生成或数字人舞蹈视频案例。从项目标题和“感谢雅痞开源”的描述来看&#xff0c;这很可能是一个利用开源AI工具&#xff08;如SadTalker、D-ID、HeyGen或类似技术栈&#xff09;制作的…

作者头像 李华
网站建设 2026/8/18 4:00:23

视线追踪与LLM Agent融合:实现个性化认知负荷评估的技术实践

1. 项目概述&#xff1a;当视线分析遇上大语言模型最近在可穿戴计算和人机交互的圈子里&#xff0c;一个融合了视线追踪&#xff08;Gaze Tracking&#xff09;和大语言模型&#xff08;LLM&#xff09;Agent的新方向正在悄然兴起。我关注到的一个典型项目&#xff0c;我们暂且…

作者头像 李华
网站建设 2026/8/18 3:56:25

Java直接内存原理、性能优化与内存泄漏排查实战

1. 项目概述&#xff1a;揭开直接内存的神秘面纱在Java的世界里&#xff0c;我们最常打交道的就是堆内存&#xff0c;也就是那个通过new关键字创建对象的地方。但如果你深入做过网络编程、文件处理&#xff0c;或者用过一些高性能的框架&#xff08;比如Netty&#xff09;&…

作者头像 李华
网站建设 2026/8/18 3:55:05

SRD-12VDC-SL-C继电器详解:从型号解读到驱动电路设计

1. 项目概述&#xff1a;从一颗继电器说起在电子电路和自动化控制的世界里&#xff0c;有一种元件&#xff0c;它体积不大&#xff0c;价格不贵&#xff0c;却扮演着“无声指挥官”的关键角色&#xff0c;这就是继电器。今天我们要聊的主角&#xff0c;是型号为SRD-12VDC-SL-C的…

作者头像 李华
网站建设 2026/8/18 3:50:07

大语言模型后训练:数据与环境管理的工程化实践指南

这次我们来看一个对AI工程师和算法研究员来说非常关键&#xff0c;但在实践中又容易被忽视的环节&#xff1a;大语言模型的后训练数据与环境管理。当模型完成预训练&#xff0c;进入指令微调、对齐或领域适配阶段时&#xff0c;数据的质量、多样性和处理流程&#xff0c;以及训…

作者头像 李华
网站建设 2026/8/18 3:49:58

安卓系统应用化实战:从权限提升到Magisk模块部署

1. 项目概述&#xff1a;为什么要把自己的App变成系统应用&#xff1f;在安卓开发或者玩机圈子里&#xff0c;把第三方App“刷”成系统应用&#xff0c;一直是个挺有吸引力的操作。你可能在某个论坛看到过&#xff0c;或者自己心里也琢磨过&#xff1a;这到底有啥用&#xff1f…

作者头像 李华