import { _decorator, Component, Material, Vec4, Sprite, Node, UITransform, Vec3, Vec2, tween } from 'cc'; const { ccclass, property } = _decorator; interface HoleData { node: Node; radius: number; softness: number; id: string; // 这里存 uuid isActive: boolean; } @ccclass('SdfHoleController') export class SdfHoleController extends Component { @property({ tooltip: "默认边缘柔化度 (像素)0-0.5" }) defaultSoftness: number = 0.1; @property({ tooltip: "默认洞的半径 (像素)" }) defaultRadius: number = 60.0; @property({ tooltip: "最大支持的洞数量" }) maxHoles: number = 20; private holes: HoleData[] = []; private material: Material | null = null; private uiTransform: UITransform | null = null; private sprite: Sprite | null = null; // 👇 修改:使用 Set 存储已激活节点的 UUID private activatedUuids: Set = new Set(); // 缓存材质参数数据,避免 updateHoles 里反复 new 数组/Vec private _leftBottom: Vec3 = new Vec3(); private _textureSize: Vec2 = new Vec2(0, 0); private _circlesData: Vec4[] = []; isCanMove: boolean=false; stratMove: number=0; moveEndPosiY: number=0; speed = 2 start() { this.sprite = this.getComponent(Sprite); this.uiTransform = this.getComponent(UITransform); if (!this.sprite || !this.sprite.material || !this.uiTransform) { console.error("❌ 组件缺失!"); return; } this.material = this.sprite.material; // 预分配 Vec4,循环复用,降低 GC this._circlesData.length = 0; for (let i = 0; i < this.maxHoles; i++) { this._circlesData.push(new Vec4(0, 0, 0, 0)); } this.updateHoles(); } startMove(fogDownDistance, speed = 2){ this.speed = speed this.isCanMove = true this.stratMove = 0 this.moveEndPosiY = fogDownDistance } update(dt: number) { if (!gg.game.CurentBattle.canUpdateFrame()) return; if(this.isCanMove){ // 原逻辑按「每帧像素」移动;用 dt 与倍速对齐不同帧率,并与战斗倍速一致 const step = this.speed * 60 * dt * gg.game.CurentBattle.TimeScale; const pos = this.node.getPosition(); const newY = pos.y - step; this.node.setPosition(pos.x, newY, pos.z); this.stratMove += step if(this.stratMove > this.moveEndPosiY){ this.isCanMove = false this.stratMove = 0 } } if (this.holes.length > 0 && this.isCanMove) { this.updateHoles(); } } /** * 【核心功能】检测固定节点列表,若与雾气相交且未激活,则生成洞 * @param fixedNodes 需要检测的固定节点数组 (直接传节点!) * @param radiusOverride 可选:覆盖默认半径 * @param softnessOverride 可选:覆盖默认柔化度 */ checkAndActivateHoles(fixedNodes: Node[], radiusOverride?: number, softnessOverride?: number) { if (!this.uiTransform) return; // 1. 获取雾气当前的世界边界 const fogWorldPos = this.node.getWorldPosition(); const contentSize = this.uiTransform.contentSize; const anchor = this.uiTransform.anchorPoint; // 计算雾气的四至边界 (世界坐标) const fogBottomY = fogWorldPos.y - contentSize.height * anchor.y; const fogTopY = fogBottomY + contentSize.height; const fogLeftX = fogWorldPos.x - contentSize.width * anchor.x; const fogRightX = fogLeftX + contentSize.width; let hasNewHole = false; // 2. 遍历所有传入的固定节点 for (const node of fixedNodes) { // 跳过无效节点 if (!node.isValid) continue; const point = node.getWorldPosition(); // --- 相交判断逻辑 (AABB 包围盒检测) --- // 判断点是否在雾气矩形内部 const inVertical = point.y <= fogTopY && point.y >= fogBottomY; const inHorizontal = point.x >= fogLeftX && point.x <= fogRightX; if (inVertical && inHorizontal) { // ✅ 相交了! // 👇 关键:使用 node.uuid 作为唯一标识 const uuid = node.uuid; // 检查是否已经激活过 if (!this.activatedUuids.has(uuid)) { // 添加到洞列表 // 注意:这里直接复用传入的 node,不需要创建虚拟节点了! // 这样如果节点本身在动,洞也会跟着动(如果需要的话) this.addHole(node, radiusOverride || this.defaultRadius, softnessOverride || this.defaultSoftness); // 标记该 UUID 为已激活 this.activatedUuids.add(uuid); hasNewHole = true; } } } // addHole 内部会调用 updateHoles,所以这里不需要额外调用 } /** * 【重置功能】清除所有洞,并清空 UUID 记录 * 调用后,之前的节点再次进入雾气范围时,会重新触发挖洞 */ resetHoles() { this.holes = []; this.activatedUuids.clear(); // 👈 清空 UUID 记录 this.updateHoles(); } // --- 辅助方法 --- addHole(node: Node, radius: number = this.defaultRadius, softness: number = this.defaultSoftness) { if (this.holes.length >= this.maxHoles) { return; } // 防止重复添加同一个节点到 holes 数组 if (this.holes.find(h => h.node === node)) return; this.holes.push({ node, radius, softness, id: node.uuid, // 存储 uuid isActive: true }); this.updateHoles(); } updateHoles() { if (!this.material || !this.uiTransform) return; const fogWorldPos = this.node.getWorldPosition(); const contentSize = this.uiTransform.contentSize; const anchor = this.uiTransform.anchorPoint; // 左下角世界坐标(复用,避免每帧 new Vec3) this._leftBottom.x = fogWorldPos.x - contentSize.width * anchor.x; this._leftBottom.y = fogWorldPos.y - contentSize.height * anchor.y; this._leftBottom.z = 0; const width = contentSize.width; const height = contentSize.height; const count = this.holes.length; // 填充穴点数据到 Vec4 数组(循环复用对象,避免 new/GC) for (let i = 0; i < count; i++) { const hole = this.holes[i]; const pos = hole.node.getWorldPosition(); const u = (pos.x - this._leftBottom.x) / width; const v = 1.0 - ((pos.y - this._leftBottom.y) / height); const circle = this._circlesData[i]; circle.x = u; circle.y = v; circle.z = hole.radius; circle.w = hole.softness; } // 清理剩余空槽位,避免 shader 因实现细节读到旧值 for (let i = count; i < this.maxHoles; i++) { const circle = this._circlesData[i]; circle.x = 0; circle.y = 0; circle.z = 0; circle.w = 0; } this.material.setProperty('holeCount', count); this._textureSize.x = width; this._textureSize.y = height; this.material.setProperty('textureSize', this._textureSize); this.material.setProperty('circles', this._circlesData); } recoverySpr(){ if (!this.material || !this.uiTransform) return; const contentSize = this.uiTransform.contentSize; const width = contentSize.width; const height = contentSize.height; this.material.setProperty('holeCount', 0); this._textureSize.x = width; this._textureSize.y = height; this.material.setProperty('textureSize', this._textureSize); this.material.setProperty('circles', this._circlesData); } //返回到原来的位置 returnToOriginalPosition(moveY = 600){ // let moveY = 1000 this.isCanMove = false this.stratMove = 0 //this.resetHoles() this.recoverySpr() tween(this.node) .to(1,{y:moveY}) .start() } }