import { _decorator, Component, Node, Vec3, tween, Color,find, Sprite, resources, sp, Material } from 'cc'; import EventBus, { GameEvents } from '../Core/EventBus'; import { BundleLoader } from '../Core/BundleLoader'; const { ccclass, property } = _decorator; /** * 小球撞击动画管理器 * 负责在小球撞击时播放特效动画 */ @ccclass('BallAni') export class BallAni extends Component { // 撞击特效骨骼数据 @property({ type: sp.SkeletonData, tooltip: '拖拽撞击特效骨骼数据到这里(Animation/WeaponTx/tx0005/tx0005)' }) public impactEffectSkeleton: sp.SkeletonData = null; // 白色描边材质 @property({ type: Material, tooltip: '拖拽白色描边材质到这里(shaders/ui-sprite-white-material)' }) public whiteMaterial: Material = null; // 撞击特效预制体缓存(保持静态缓存用于性能优化) private static impactEffectSkeleton: sp.SkeletonData = null; private static isLoading: boolean = false; // 白色描边材质缓存(保持静态缓存用于性能优化) private static whiteMaterial: Material = null; private static isMaterialLoading: boolean = false; // 当前播放中的特效节点列表 private activeEffects: Node[] = []; // 当前播放中的方块动画列表 private activeBlockAnimations: Map = new Map(); // 敌人生成状态 private enemySpawningActive: boolean = false; start() { // 使用装饰器挂载的资源初始化静态缓存 if (this.impactEffectSkeleton) { BallAni.impactEffectSkeleton = this.impactEffectSkeleton; console.log('[BallAni] ✅ 撞击特效资源通过装饰器加载成功'); } else { console.warn('[BallAni] ⚠️ 撞击特效资源未设置,将使用动态加载'); this.preloadImpactEffect(); } if (this.whiteMaterial) { BallAni.whiteMaterial = this.whiteMaterial; console.log('[BallAni] ✅ 白色描边材质通过装饰器加载成功'); } else { console.warn('[BallAni] ⚠️ 白色描边材质未设置,将使用动态加载'); this.preloadWhiteMaterial(); } // 监听敌人生成状态事件 const eventBus = EventBus.getInstance(); eventBus.on(GameEvents.ENEMY_SPAWNING_STARTED, this.onEnemySpawningStarted, this); eventBus.on(GameEvents.ENEMY_SPAWNING_STOPPED, this.onEnemySpawningStopped, this); } private onEnemySpawningStarted() { this.enemySpawningActive = true; } private onEnemySpawningStopped() { this.enemySpawningActive = false; // 当敌人生成停止时,立即检查是否需要发射子弹 const eventBus = EventBus.getInstance(); eventBus.emit(GameEvents.BALL_FIRE_BULLET, { canFire: (canFire: boolean) => { } }); } /** * 播放方块被撞击动画 * @param blockNode 方块节点 */ public playBlockHitAnimation(blockNode: Node) { if (!blockNode || !blockNode.isValid) { console.warn('[BallAni] 方块节点无效'); return; } // 如果该方块已经在播放动画,跳过 if (this.activeBlockAnimations.has(blockNode)) { console.log('[BallAni] 方块动画已在播放中,跳过'); return; } // 技能选择UI弹出且当前是拖拽态时,跳过任何挂载/动画以避免误绑定 const selectSkillUI = find('Canvas/GameLevelUI/SelectSkillUI'); const isSkillUIActive = !!(selectSkillUI && selectSkillUI.active); const isDragging = (blockNode as any)['isDragging'] === true; if (isSkillUIActive && isDragging) { return; } // 仅对GameArea中的真实方块执行动画,防止误用到UI节点 if (!this.isNodeUnder(blockNode, 'GameArea') || this.isNodeUnder(blockNode, 'SelectSkillUI')) { return; } // 检查是否有活跃敌人,只有有敌人时才播放缩放动画 const eventBus = EventBus.getInstance(); let hasActiveEnemies = false; // 通过事件系统检查是否有活跃敌人 eventBus.emit(GameEvents.BALL_FIRE_BULLET, { canFire: (canFire: boolean) => { hasActiveEnemies = canFire; } }); // 立即执行动画 this.executeBlockAnimation(blockNode, hasActiveEnemies); // 如果敌人生成未激活,立即检查是否需要发射子弹 if (!this.enemySpawningActive) { eventBus.emit(GameEvents.BALL_FIRE_BULLET, { canFire: (canFire: boolean) => { } }); } } /** * 判断节点是否位于指定祖先之下 */ private isNodeUnder(node: Node, ancestorName: string): boolean { let current: Node | null = node; while (current) { if (current.name === ancestorName) return true; current = current.parent; } return false; } /** * 执行方块动画的具体逻辑 * @param blockNode 方块节点 * @param hasActiveEnemies 是否有活跃敌人 */ private executeBlockAnimation(blockNode: Node, hasActiveEnemies: boolean) { if (!blockNode || !blockNode.isValid) { console.warn('[BallAni] 方块节点无效'); return; } // 如果该方块已经在播放动画,跳过 if (this.activeBlockAnimations.has(blockNode)) { console.log('[BallAni] 方块动画已在播放中,跳过'); return; } // 新的预制体结构:Sprite组件在Node子节点上 const nodeChild = blockNode.getChildByName('Node'); const sprite = nodeChild ? nodeChild.getComponent(Sprite) : null; // 查找预制体根节点和Weapon节点 // 如果传入的是子节点(如"Node"),需要找到预制体根节点 let prefabRoot = blockNode; if (blockNode.name === 'Node' && blockNode.parent) { prefabRoot = blockNode.parent; } const weaponNode = prefabRoot.getChildByName('Weapon'); // 保存原始材质和缩放 // 优先保存customMaterial,如果没有则保存当前material const originalMaterial = sprite ? (sprite.customMaterial || sprite.material) : null; const originalBlockScale = new Vec3(blockNode.scale.x, blockNode.scale.y, blockNode.scale.z); const originalWeaponScale = weaponNode ? new Vec3(weaponNode.scale.x, weaponNode.scale.y, weaponNode.scale.z) : null; if (hasActiveEnemies) { // 有敌人时播放方块缩放动画(包括Node和Weapon子节点) const shrinkBlockScale = new Vec3(0.8, 0.8, 1); // 应用白色描边材质到Node子节点的Sprite组件 // 优先使用装饰器挂载的白色材质 let whiteMaterial = null; if (this.whiteMaterial) { whiteMaterial = this.whiteMaterial; } else if (BallAni.whiteMaterial) { whiteMaterial = BallAni.whiteMaterial; } if (sprite && whiteMaterial) { sprite.material = whiteMaterial; } // 对方块预制体进行缩放动画 // Weapon节点需要在其原有0.4倍基础上再进行缩放动画 const weaponShrinkScale = originalWeaponScale ? new Vec3( originalWeaponScale.x * 0.8, // 在0.4倍基础上再缩小到0.8倍 originalWeaponScale.y * 0.8, originalWeaponScale.z ) : null; // 统一使用代码控制的 Tween 动画(不使用组件) const animationTween = tween(blockNode) .to(0.1, { scale: shrinkBlockScale }, { onStart: () => { // 开始动画时,同时缩放Weapon节点 if (weaponNode && weaponShrinkScale) { tween(weaponNode) .to(0.1, { scale: weaponShrinkScale }) .start(); } } }) .to(0.2, { scale: originalBlockScale }, { onStart: () => { // 恢复动画时,同时恢复Weapon节点 if (weaponNode && originalWeaponScale) { tween(weaponNode) .to(0.2, { scale: originalWeaponScale }) .start(); } } }) .call(() => { // 确保Weapon节点恢复到原始缩放 if (weaponNode && originalWeaponScale) { weaponNode.scale = originalWeaponScale; } // 动画完成时恢复原始材质 if (sprite && originalMaterial) { // 如果原始材质是customMaterial,则恢复customMaterial if (sprite.customMaterial === originalMaterial) { sprite.material = sprite.customMaterial; } else { sprite.material = originalMaterial; } } else if (sprite) { // 如果没有保存的原始材质,尝试恢复到customMaterial if (sprite.customMaterial) { sprite.material = sprite.customMaterial; } else { sprite.material = null; } } // 动画完成,从活动列表中移除 this.activeBlockAnimations.delete(blockNode); }) .start(); // 添加到活动动画列表 this.activeBlockAnimations.set(blockNode, animationTween); } else { // 没有敌人时直接结束动画,不应用材质 const animationTween = tween({}) .delay(0.2) .call(() => { // 动画完成,从活动列表中移除 this.activeBlockAnimations.delete(blockNode); }) .start(); // 添加到活动动画列表 this.activeBlockAnimations.set(blockNode, animationTween); } } /** * 预加载撞击特效资源 */ private preloadImpactEffect() { if (BallAni.impactEffectSkeleton || BallAni.isLoading) { return; } BallAni.isLoading = true; const path = 'WeaponTx/tx0005/tx0005'; BundleLoader.loadSkeletonData(path).then((sData) => { BallAni.isLoading = false; if (!sData) { console.warn('[BallAni] 加载撞击特效失败: 资源为空'); return; } BallAni.impactEffectSkeleton = sData; console.log('[BallAni] 撞击特效资源加载成功'); }).catch((err) => { BallAni.isLoading = false; console.warn('[BallAni] 加载撞击特效失败:', err); }); } /** * 预加载白色描边材质 */ private preloadWhiteMaterial() { if (BallAni.whiteMaterial || BallAni.isMaterialLoading) { return; } BallAni.isMaterialLoading = true; const materialPath = 'shaders/ui-sprite-white-material'; resources.load(materialPath, Material, (err, material: Material) => { BallAni.isMaterialLoading = false; if (err || !material) { console.warn('[BallAni] 加载白色描边材质失败:', err); return; } BallAni.whiteMaterial = material; console.log('[BallAni] 白色描边材质加载成功'); }); } /** * 在指定位置播放撞击特效 * @param worldPosition 世界坐标位置 */ public playImpactEffect(worldPosition: Vec3) { // 优先使用装饰器挂载的资源 if (this.impactEffectSkeleton) { this.createAndPlayEffect(worldPosition, this.impactEffectSkeleton); return; } // 如果装饰器资源未设置,使用静态缓存 if (BallAni.impactEffectSkeleton) { this.createAndPlayEffect(worldPosition, BallAni.impactEffectSkeleton); return; } // 最后才使用动态加载 const path = 'WeaponTx/tx0005/tx0005'; const bundleLoader = BundleLoader.getInstance(); BundleLoader.loadSkeletonData(path).then((sData) => { if (!sData) { console.warn('[BallAni] 加载撞击特效失败: 资源为空'); return; } BallAni.impactEffectSkeleton = sData; this.createAndPlayEffect(worldPosition, sData); }).catch((err) => { console.warn('[BallAni] 加载撞击特效失败:', err); }); } /** * 创建并播放特效 */ private createAndPlayEffect(worldPosition: Vec3, skeletonData: sp.SkeletonData) { const effectNode = new Node('ImpactEffect'); const skeleton = effectNode.addComponent(sp.Skeleton); skeleton.skeletonData = skeletonData; skeleton.premultipliedAlpha = false; skeleton.setAnimation(0, 'animation', false); skeleton.setCompleteListener(() => { this.removeEffect(effectNode); }); const canvas = find('Canvas'); if (canvas) { canvas.addChild(effectNode); effectNode.setWorldPosition(worldPosition); // 设置特效缩放 effectNode.setScale(1.5, 1.5, 1); // 设置特效更亮 - 增加亮度 const color = new Color(255, 255, 255, 255); color.r = Math.min(255, color.r * 1.5); color.g = Math.min(255, color.g * 1.5); color.b = Math.min(255, color.b * 1.5); effectNode.getComponent(sp.Skeleton).color = color; // 添加到活动特效列表 this.activeEffects.push(effectNode); } else { effectNode.destroy(); } } /** * 移除特效节点 * @param effectNode 要移除的特效节点 */ private removeEffect(effectNode: Node) { // 从活动特效列表中移除 const index = this.activeEffects.indexOf(effectNode); if (index !== -1) { this.activeEffects.splice(index, 1); } // 销毁节点 if (effectNode && effectNode.isValid) { effectNode.destroy(); } } /** * 停止指定方块的动画 * @param blockNode 方块节点 */ public stopBlockAnimation(blockNode: Node) { const animation = this.activeBlockAnimations.get(blockNode); if (animation) { animation.stop(); this.activeBlockAnimations.delete(blockNode); } // 代码控制,无需组件停止 } /** * 清理所有活动的特效 */ public clearAllEffects() { for (const effect of this.activeEffects) { if (effect && effect.isValid) { effect.destroy(); } } this.activeEffects = []; } /** * 清除所有方块动画 */ public clearAllBlockAnimations() { // 停止所有方块动画 for (const [blockNode, animation] of this.activeBlockAnimations) { if (animation) { animation.stop(); } } this.activeBlockAnimations.clear(); } onDestroy() { // 解绑事件监听 const eventBus = EventBus.getInstance(); eventBus.off(GameEvents.ENEMY_SPAWNING_STARTED, this.onEnemySpawningStarted, this); eventBus.off(GameEvents.ENEMY_SPAWNING_STOPPED, this.onEnemySpawningStopped, this); // 组件销毁时清理所有特效和动画 this.clearAllEffects(); this.clearAllBlockAnimations(); } /** * 获取BallAni实例 * @returns BallAni实例 */ public static getInstance(): BallAni | null { const gameArea = find('Canvas/GameLevelUI/GameArea'); if (!gameArea) { console.warn('[BallAni] 未找到GameArea节点'); return null; } const ballAni = gameArea.getComponent(BallAni); if (!ballAni) { console.warn('[BallAni] GameArea节点上未找到BallAni组件'); return null; } return ballAni; } }