import { _decorator, Component, Node, Vec3, director, sp, NodePool, find } from 'cc'; const { ccclass, property } = _decorator; export enum AttackEffectType { Mage = 'mage', Archer = 'archer', } @ccclass('EnemyAttackEffectManager') export class EnemyAttackEffectManager extends Component { public static instance: EnemyAttackEffectManager = null; @property({ type: sp.SkeletonData }) public mageImpactEffect: sp.SkeletonData = null; @property({ type: sp.SkeletonData }) public archerImpactEffect: sp.SkeletonData = null; @property public mageImpactAnimName: string = 'hit'; @property public archerImpactAnimName: string = 'hit'; @property public effectScale: number = 1.0; private magePool: NodePool = new NodePool(); private archerPool: NodePool = new NodePool(); onLoad() { EnemyAttackEffectManager.instance = this; console.log(`[EnemyAttackEffectManager] 组件已加载,挂载节点: ${this.node?.name}`); } /** * 在指定世界坐标播放墙体命中特效 */ public playImpactEffect(type: AttackEffectType, worldPos: Vec3): void { const parent = this.resolveEffectParent(); if (!parent) { console.warn('[EnemyAttackEffectManager] 未找到特效父节点,取消播放'); return; } const node = this.getEffectNode(type); console.log(`[EnemyAttackEffectManager] 准备播放命中特效: type=${type}, 位置=(${worldPos.x.toFixed(1)}, ${worldPos.y.toFixed(1)}),父节点=${parent.name}`); parent.addChild(node); node.setWorldPosition(worldPos); const skeleton = node.getComponent(sp.Skeleton); if (!skeleton) { // 没有Spine组件,不播放(安全返回) console.warn('[EnemyAttackEffectManager] 节点无sp.Skeleton组件,跳过播放并回收'); this.recycleNode(type, node); return; } if (!skeleton.skeletonData) { console.warn('[EnemyAttackEffectManager] 未绑定SkeletonData资源,无法播放特效'); this.recycleNode(type, node); return; } const animName = type === AttackEffectType.Mage ? this.mageImpactAnimName : this.archerImpactAnimName; if (animName) { console.log(`[EnemyAttackEffectManager] setAnimation: ${animName}`); skeleton.setAnimation(0, animName, false); skeleton.setCompleteListener(() => { console.log('[EnemyAttackEffectManager] 特效播放完成,回收节点'); this.recycleNode(type, node); }); } else { // 未提供动画名时,直接回收 console.warn('[EnemyAttackEffectManager] 未提供动画名,直接回收节点'); this.recycleNode(type, node); } } private resolveEffectParent(): Node | null { // 优先挂在GameArea,保障层级正确 const parent = find('Canvas/GameLevelUI/GameArea') || find('Canvas/GameLevelUI') || director.getScene(); if (parent) { console.log(`[EnemyAttackEffectManager] 选择特效父节点: ${parent.name}`); } return parent; } private getEffectNode(type: AttackEffectType): Node { const pool = type === AttackEffectType.Mage ? this.magePool : this.archerPool; const data = type === AttackEffectType.Mage ? this.mageImpactEffect : this.archerImpactEffect; let node = pool.get(); if (!node) { node = new Node(`EnemyWallImpact_${type}`); const skeleton = node.addComponent(sp.Skeleton); skeleton.skeletonData = data; console.log(`[EnemyAttackEffectManager] 新建特效节点: ${node.name},绑定数据=${data ? '有' : '无'}`); } else { const skeleton = node.getComponent(sp.Skeleton); if (skeleton) skeleton.skeletonData = data; console.log(`[EnemyAttackEffectManager] 复用池中节点: ${node.name},重新绑定数据=${data ? '有' : '无'}`); } // 统一缩放到 X/Y 轴,Z 轴保持 1 node.setScale(new Vec3(this.effectScale, this.effectScale, 1)); console.log(`[EnemyAttackEffectManager] 设置缩放: ${this.effectScale}`); return node; } private recycleNode(type: AttackEffectType, node: Node): void { if (!node || !node.isValid) return; node.removeFromParent(); const pool = type === AttackEffectType.Mage ? this.magePool : this.archerPool; pool.put(node); console.log(`[EnemyAttackEffectManager] 节点已回收到对象池: ${node.name}`); } }