展示代码
使用工具消除的mask区域
重要
onLoad【1】:获取对应组件信息。
start【3】:对mask组件效果取反,使用claerMask。
onComplete【4】:调用fullmask,后续处理自己需要的逻辑。
// Learn TypeScript:
// - https://docs.cocos.com/creator/2.4/manual/en/scripting/typescript.html
// Learn Attribute:
// - https://docs.cocos.com/creator/2.4/manual/en/scripting/reference/attributes.html
// Learn life-cycle callbacks:
// - https://docs.cocos.com/creator/2.4/manual/en/scripting/life-cycle-callbacks.html
import { engine } from "../../../core/Engine";
const { ccclass, property } = cc._decorator;
/**
* BaoGaiBanZhuRen_YWQ 擦除Mask组件
* 功能:使用工具节点擦除Mask,支持进度检测和完成回调
*/
@ccclass
export default class BaoGaiBanZhuRen_YWQ_EraserMask extends cc.Component {
/** ==================== 可配置属性 ==================== */
@property({
type: cc.Node,
displayName: "工具检测节点",
tooltip: "用于检测触摸/移动的工具节点(如橡皮擦)"
})
toolDetectNode: cc.Node = null;
@property({
type: cc.Node,
displayName: "工具区域节点",
tooltip: "定义擦除实际区域的工具节点,clearSize为0时自动使用此节点尺寸"
})
toolAreaNode: cc.Node = null;
@property({
type: cc.Node,
displayName: "工具节点(旧)",
tooltip: "【已废弃】请使用 toolDetectNode 和 toolAreaNode"
})
toolNode: cc.Node = null;
@property({
type: cc.Node,
displayName: "Mask节点",
tooltip: "包含cc.Mask组件的节点"
})
maskNode: cc.Node = null;
@property({
type: cc.Node,
displayName: "被擦除区域节点",
tooltip: "用于计算擦除范围的节点"
})
targetNode: cc.Node = null;
@property({
type: Number,
displayName: "擦除范围",
tooltip: "每次擦除的圆形半径,设为0时自动使用工具区域节点尺寸",
min: 0,
max: 100,
step: 5
})
clearSize: number = 40;
@property({
type: Number,
displayName: "分块大小",
tooltip: "用于计算进度的分块大小,越小越精确但性能消耗越大",
min: 10,
max: 100,
step: 5
})
blockSize: number = 40;
@property({
type: Number,
displayName: "完成阈值",
tooltip: "擦除进度达到此值时触发完成(0-1)",
min: 0,
max: 1,
step: 0.05
})
completeThreshold: number = 0.8;
@property({
type: Boolean,
displayName: "启用音效"
})
enableSound: boolean = true;
@property({
type: String,
displayName: "擦除音效名称",
visible: function () { return this.enableSound; }
})
scratchSoundName: string = "JJMPCS";
@property({
type: cc.Node,
displayName: "完成时显示的节点",
tooltip: "擦除完成后需要显示的节点列表"
})
showNodesOnComplete: cc.Node[] = [];
@property({
type: cc.Node,
displayName: "完成时隐藏的节点",
tooltip: "擦除完成后需要隐藏的节点列表"
})
hideNodesOnComplete: cc.Node[] = [];
@property({
type: Boolean,
displayName: "完成后销毁组件"
})
destroyOnComplete: boolean = true;
@property({
type: Boolean,
displayName: "调试模式"
})
isDebug: boolean = false;
/** ==================== 私有成员 ==================== */
// Mask组件
private maskComponent: cc.Mask = null;
// Graphics组件(用于绘制擦除)
private maskGraphics: cc.Graphics = null;
// 分块检测列表
private blockList: { rect: cc.Rect; isHit: boolean }[] = [];
// 临时绘制点列表
private tempDrawPoints: cc.Vec2[] = [];
// 是否已完成
private isCompleted: boolean = false;
// 当前进度(0-1)
private currentProgress: number = 0;
// 音效播放锁
private canPlaySound: boolean = true;
// 完成回调函数
private completeCallback: Function = null;
/** ==================== 生命周期函数 ==================== */
onLoad() {
this.handleBackwardCompatibility();
this.initMask();
this.initBlocks();
this.registerEvents();
}
/**
* 处理向后兼容:如果新属性未设置但旧属性已设置,使用旧属性值
*/
private handleBackwardCompatibility(): void {
if (!this.toolDetectNode && this.toolNode) {
this.toolDetectNode = this.toolNode;
cc.warn("[BaoGaiBanZhuRen_YWQ_EraserMask] toolNode 已废弃,请使用 toolDetectNode");
}
if (!this.toolAreaNode && this.toolNode) {
this.toolAreaNode = this.toolNode;
}
}
onDestroy() {
this.unregisterEvents();
}
protected start(): void {
this.maskGraphics.clear(); // 清除之前的绘制内容
this.maskComponent.inverted = true; // 设置为非反转模式,即默认遮挡
}
/** ==================== 初始化函数 ==================== */
/**
* 初始化Mask组件
*/
private initMask(): void {
if (!this.maskNode) {
cc.error("[BaoGaiBanZhuRen_YWQ_EraserMask] maskNode未设置!");
return;
}
this.maskComponent = this.maskNode.getComponent(cc.Mask);
if (!this.maskComponent) {
cc.error("[BaoGaiBanZhuRen_YWQ_EraserMask] maskNode上未找到cc.Mask组件!");
return;
}
// 获取Graphics组件
this.maskGraphics = this.maskComponent["_graphics"];
if (!this.maskGraphics) {
cc.error("[BaoGaiBanZhuRen_YWQ_EraserMask] 无法获取Mask的Graphics组件!");
return;
}
if (this.isDebug) {
cc.log("[BaoGaiBanZhuRen_YWQ_EraserMask] Mask初始化成功");
}
}
/**
* 初始填充Mask(白色填充,表示全部遮挡)
*/
private fillMask(): void {
if (!this.maskGraphics || !this.targetNode) return;
let stencil = this.maskGraphics;
// 先clear确保干净
this.maskGraphics.clear();
// 绘制白色矩形填充整个区域
const width = this.targetNode.width;
const height = this.targetNode.height;
this.maskGraphics.fillColor = cc.Color.BLACK;
this.maskGraphics.rect(-width / 2, -height / 2, width, height);
this.maskGraphics.fill();
if (this.isDebug) {
cc.log("[BaoGaiBanZhuRen_YWQ_EraserMask] Mask已填充");
}
}
/**
* 初始化分块检测列表
*/
private initBlocks(): void {
if (!this.targetNode) {
cc.error("[BaoGaiBanZhuRen_YWQ_EraserMask] targetNode未设置!");
return;
}
this.blockList = [];
const width = this.targetNode.width;
const height = this.targetNode.height;
// 将目标区域划分为小块
for (let x = 0; x < width; x += this.blockSize) {
for (let y = 0; y < height; y += this.blockSize) {
this.blockList.push({
rect: cc.rect(
x - width / 2,
y - height / 2,
this.blockSize,
this.blockSize
),
isHit: false
});
}
}
if (this.isDebug) {
cc.log(`[BaoGaiBanZhuRen_YWQ_EraserMask] 分块初始化完成,共${this.blockList.length}块`);
}
}
/**
* 注册触摸事件
*/
private registerEvents(): void {
if (!this.toolDetectNode) {
cc.error("[BaoGaiBanZhuRen_YWQ_EraserMask] toolDetectNode未设置!");
return;
}
this.toolDetectNode.on(cc.Node.EventType.TOUCH_START, this.onTouchStart, this);
this.toolDetectNode.on(cc.Node.EventType.TOUCH_MOVE, this.onTouchMove, this);
this.toolDetectNode.on(cc.Node.EventType.TOUCH_END, this.onTouchEnd, this);
this.toolDetectNode.on(cc.Node.EventType.TOUCH_CANCEL, this.onTouchEnd, this);
}
/**
* 注销触摸事件
*/
private unregisterEvents(): void {
if (!this.toolDetectNode) return;
this.toolDetectNode.off(cc.Node.EventType.TOUCH_START, this.onTouchStart, this);
this.toolDetectNode.off(cc.Node.EventType.TOUCH_MOVE, this.onTouchMove, this);
this.toolDetectNode.off(cc.Node.EventType.TOUCH_END, this.onTouchEnd, this);
this.toolDetectNode.off(cc.Node.EventType.TOUCH_CANCEL, this.onTouchEnd, this);
}
/** ==================== 触摸事件处理 ==================== */
/**
* 触摸开始
*/
private onTouchStart(event: cc.Event.EventTouch): void {
if (this.isCompleted) return;
// 清空之前的点
this.tempDrawPoints = [];
const pos = this.getToolPosInMask();
this.clearAtPos(pos);
}
/**
* 触摸移动
*/
private onTouchMove(event: cc.Event.EventTouch): void {
if (this.isCompleted) return;
const pos = this.getToolPosInMask();
this.clearAtPos(pos);
}
/**
* 触摸结束
*/
private onTouchEnd(event: cc.Event.EventTouch): void {
this.tempDrawPoints = [];
}
/** ==================== 擦除功能 ==================== */
/**
* 获取工具节点在Mask本地坐标系中的位置
*/
private getToolPosInMask(): cc.Vec2 {
if (!this.toolAreaNode || !this.maskNode) return cc.v2(0, 0);
const worldPos = this.toolAreaNode.convertToWorldSpaceAR(cc.v2(0, 0));
return this.maskNode.convertToNodeSpaceAR(worldPos);
}
/**
* 获取实际擦除尺寸
* @returns 擦除圆形半径
*/
private getActualClearSize(): number {
if (this.clearSize > 0) {
return this.clearSize / 2;
}
// clearSize 为 0 时,使用 toolAreaNode 的尺寸
if (this.toolAreaNode) {
return Math.max(this.toolAreaNode.width, this.toolAreaNode.height) / 2;
}
return 20; // 默认回退值
}
/**
* 在指定位置擦除
* @param pos Mask本地坐标系中的位置
*/
private clearAtPos(pos: cc.Vec2): void {
if (!this.maskGraphics || !this.targetNode) return;
// 检查是否在目标区域内
if (!this.isInTargetArea(pos)) return;
// 播放音效
this.playScratchSound();
// 绘制擦除
const actualClearSize = this.getActualClearSize();
const len = this.tempDrawPoints.length;
let prevPos: cc.Vec2 = null;
let stencil = this.maskGraphics;
if (len <= 1) {
// 第一个点,绘制圆形
stencil.circle(pos.x, pos.y, actualClearSize);
// this.maskGraphics.strokeColor = cc.Color.BLACK;
stencil.fill();
prevPos = pos;
} else {
// 连续移动,绘制线段
prevPos = this.tempDrawPoints[len - 2];
stencil.moveTo(prevPos.x, prevPos.y);
stencil.lineTo(pos.x, pos.y);
stencil.lineWidth = actualClearSize * 2; // lineWidth 是直径
stencil.lineCap = cc.Graphics.LineCap.ROUND;
stencil.lineJoin = cc.Graphics.LineJoin.ROUND;
// this.maskGraphics.strokeColor = cc.Color.BLACK;
stencil.stroke();
}
// 更新分块检测
this.updateBlockHit(prevPos, pos);
// 计算进度
this.calculateProgress();
}
/**
* 更新分块命中状态
*/
private updateBlockHit(prevPos: cc.Vec2, curPos: cc.Vec2): void {
// 单点检测
if (prevPos.equals(curPos)) {
this.blockList.forEach(block => {
if (!block.isHit) {
const xFlag = curPos.x > block.rect.x && curPos.x < block.rect.x + block.rect.width;
const yFlag = curPos.y > block.rect.y && curPos.y < block.rect.y + block.rect.height;
if (xFlag && yFlag) {
block.isHit = true;
}
}
});
} else {
// 线段检测
this.blockList.forEach(block => {
if (!block.isHit) {
block.isHit = cc.Intersection.lineRect(prevPos, curPos, block.rect);
}
});
}
}
/**
* 计算当前进度
*/
private calculateProgress(): void {
let hitCount = 0;
for (const block of this.blockList) {
if (block.isHit) hitCount++;
}
this.currentProgress = hitCount / this.blockList.length;
if (this.isDebug) {
cc.log(`[BaoGaiBanZhuRen_YWQ_EraserMask] 当前进度: ${(this.currentProgress * 100).toFixed(1)}%`);
}
// 检查是否完成
if (this.currentProgress >= this.completeThreshold && !this.isCompleted) {
this.onComplete();
}
}
/**
* 检查位置是否在目标区域内
*/
private isInTargetArea(pos: cc.Vec2): boolean {
if (!this.targetNode) return false;
const boundingBox = this.targetNode.getBoundingBox();
return boundingBox.contains(pos);
}
/** ==================== 完成处理 ==================== */
/**
* 完成回调
*/
private onComplete(): void {
this.isCompleted = true;
cc.log("[BaoGaiBanZhuRen_YWQ_EraserMask] 擦除完成!");
// 显示节点
for (const node of this.showNodesOnComplete) {
if (node) node.active = true;
}
// 隐藏节点
for (const node of this.hideNodesOnComplete) {
if (node) node.active = false;
}
// 执行完成回调
if (this.completeCallback) {
this.completeCallback();
}
// 发送完成事件
this.node.emit("eraserComplete", this);
// 是否销毁组件
if (this.destroyOnComplete) {
this.scheduleOnce(() => {
this.destroy();
}, 0.1);
}
}
/** ==================== 公共接口 ==================== */
/**
* 设置完成回调
* @param callback 完成时的回调函数
*/
public setCompleteCallback(callback: Function): void {
this.completeCallback = callback;
}
/**
* 获取当前进度(0-1)
*/
public getProgress(): number {
return this.currentProgress;
}
/**
* 是否已完成
*/
public getIsCompleted(): boolean {
return this.isCompleted;
}
/**
* 重置擦除状态
*/
public reset(): void {
if (!this.maskGraphics) return;
this.tempDrawPoints = [];
this.isCompleted = false;
this.currentProgress = 0;
// 重置分块
for (const block of this.blockList) {
block.isHit = false;
}
// 重新填充mask
this.fillMask();
if (this.isDebug) {
cc.log("[BaoGaiBanZhuRen_YWQ_EraserMask] 已重置");
}
}
/**
* 全部清除(一次性清除整个mask)
*/
public clearAll(): void {
if (!this.maskGraphics || !this.targetNode) return;
// 绘制透明填充整个区域(相当于全部擦除)
const width = this.targetNode.width;
const height = this.targetNode.height;
// 使用透明色填充
this.maskGraphics.fillColor = cc.color(0, 0, 0, 0);
this.maskGraphics.rect(-width / 2, -height / 2, width, height);
this.maskGraphics.fill();
this.isCompleted = true;
this.currentProgress = 1;
// 标记所有分块为已擦除
for (const block of this.blockList) {
block.isHit = true;
}
this.onComplete();
}
public clearMask() {
cc.log('清除遮罩');
if (!this.maskGraphics || !this.targetNode) return;
let stencil = this.maskGraphics;
// 清空图形,让整个区域都不被遮罩
stencil.clear();
}
/**
* 启用/禁用擦除功能
* @param enabled 是否启用
*/
public setEnabled(enabled: boolean): void {
if (enabled) {
this.registerEvents();
} else {
this.unregisterEvents();
}
}
/** ==================== 音效 ==================== */
/**
* 播放擦除音效
*/
private playScratchSound(): void {
if (!this.enableSound || !this.canPlaySound) return;
this.canPlaySound = false;
engine.audio.playEffect(this.scratchSoundName, false, (time) => {
this.scheduleOnce(() => {
this.canPlaySound = true;
}, time);
});
}
}
Comments NOTHING