BallAni.ts 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. import { _decorator, Component, Node, Vec3, tween, Color, Sprite, resources, sp, find } from 'cc';
  2. import EventBus, { GameEvents } from '../Core/EventBus';
  3. const { ccclass, property } = _decorator;
  4. /**
  5. * 小球撞击动画管理器
  6. * 负责在小球撞击时播放特效动画
  7. */
  8. @ccclass('BallAni')
  9. export class BallAni extends Component {
  10. // 撞击特效预制体缓存
  11. private static impactEffectSkeleton: sp.SkeletonData = null;
  12. private static isLoading: boolean = false;
  13. // 当前播放中的特效节点列表
  14. private activeEffects: Node[] = [];
  15. // 当前播放中的方块动画列表
  16. private activeBlockAnimations: Map<Node, any> = new Map();
  17. start() {
  18. // 预加载撞击特效资源
  19. this.preloadImpactEffect();
  20. }
  21. /**
  22. * 播放方块被撞击动画
  23. * @param blockNode 方块节点
  24. */
  25. public playBlockHitAnimation(blockNode: Node) {
  26. if (!blockNode || !blockNode.isValid) {
  27. console.warn('[BallAni] 方块节点无效');
  28. return;
  29. }
  30. // 如果该方块已经在播放动画,跳过
  31. if (this.activeBlockAnimations.has(blockNode)) {
  32. console.log('[BallAni] 方块动画已在播放中,跳过');
  33. return;
  34. }
  35. // 检查是否有活跃敌人,只有有敌人时才播放缩放动画
  36. const eventBus = EventBus.getInstance();
  37. let hasActiveEnemies = false;
  38. // 通过事件系统检查是否有活跃敌人
  39. eventBus.emit(GameEvents.BALL_FIRE_BULLET, {
  40. canFire: (canFire: boolean) => {
  41. hasActiveEnemies = canFire;
  42. }
  43. });
  44. console.log('[BallAni] 开始播放方块撞击动画', blockNode.name, '有敌人:', hasActiveEnemies);
  45. const sprite = blockNode.getComponent(Sprite);
  46. const originalScale = blockNode.scale.clone();
  47. const originalColor = sprite ? sprite.color.clone() : Color.WHITE.clone();
  48. console.log('[BallAni] 原始缩放:', originalScale, '原始颜色:', originalColor);
  49. if (hasActiveEnemies) {
  50. // 有敌人时播放完整动画:收缩到0.5倍 -> 变白发亮 -> 恢复正常
  51. const shrinkScale = originalScale.clone().multiplyScalar(0.5);
  52. const animationTween = tween(blockNode)
  53. .to(0.2, { scale: shrinkScale })
  54. .call(() => {
  55. console.log('[BallAni] 方块收缩完成,开始变白');
  56. // 变白发光效果
  57. if (sprite) {
  58. sprite.color = Color.WHITE;
  59. }
  60. })
  61. .to(0.2, { scale: originalScale })
  62. .call(() => {
  63. console.log('[BallAni] 方块动画完成,恢复原状');
  64. // 恢复原始颜色
  65. if (sprite) {
  66. sprite.color = originalColor;
  67. }
  68. // 动画完成,从活动列表中移除
  69. this.activeBlockAnimations.delete(blockNode);
  70. })
  71. .start();
  72. // 添加到活动动画列表
  73. this.activeBlockAnimations.set(blockNode, animationTween);
  74. } else {
  75. // 没有敌人时只播放白光效果,不缩放
  76. const animationTween = tween({})
  77. .call(() => {
  78. console.log('[BallAni] 无敌人,仅播放白光效果');
  79. // 变白发光效果
  80. if (sprite) {
  81. sprite.color = Color.WHITE;
  82. }
  83. })
  84. .delay(0.2) // 保持白光一段时间
  85. .call(() => {
  86. console.log('[BallAni] 白光效果完成,恢复原状');
  87. // 恢复原始颜色
  88. if (sprite) {
  89. sprite.color = originalColor;
  90. }
  91. // 动画完成,从活动列表中移除
  92. this.activeBlockAnimations.delete(blockNode);
  93. })
  94. .start();
  95. // 添加到活动动画列表
  96. this.activeBlockAnimations.set(blockNode, animationTween);
  97. }
  98. }
  99. /**
  100. * 预加载撞击特效资源
  101. */
  102. private preloadImpactEffect() {
  103. if (BallAni.impactEffectSkeleton || BallAni.isLoading) {
  104. return;
  105. }
  106. BallAni.isLoading = true;
  107. const path = 'Animation/WeaponTx/tx0005/tx0005';
  108. resources.load(path, sp.SkeletonData, (err, sData: sp.SkeletonData) => {
  109. BallAni.isLoading = false;
  110. if (err || !sData) {
  111. console.warn('[BallAni] 加载撞击特效失败:', err);
  112. return;
  113. }
  114. BallAni.impactEffectSkeleton = sData;
  115. console.log('[BallAni] 撞击特效资源加载成功');
  116. });
  117. }
  118. /**
  119. * 在指定位置播放撞击特效
  120. * @param worldPosition 世界坐标位置
  121. */
  122. public playImpactEffect(worldPosition: Vec3) {
  123. // 如果资源未加载,直接加载并播放
  124. if (!BallAni.impactEffectSkeleton) {
  125. const path = 'Animation/WeaponTx/tx0005/tx0005';
  126. resources.load(path, sp.SkeletonData, (err, sData: sp.SkeletonData) => {
  127. if (err || !sData) {
  128. console.warn('[BallAni] 加载撞击特效失败:', err);
  129. return;
  130. }
  131. BallAni.impactEffectSkeleton = sData;
  132. this.createAndPlayEffect(worldPosition, sData);
  133. });
  134. return;
  135. }
  136. this.createAndPlayEffect(worldPosition, BallAni.impactEffectSkeleton);
  137. }
  138. /**
  139. * 创建并播放特效
  140. */
  141. private createAndPlayEffect(worldPosition: Vec3, skeletonData: sp.SkeletonData) {
  142. const effectNode = new Node('ImpactEffect');
  143. const skeleton = effectNode.addComponent(sp.Skeleton);
  144. skeleton.skeletonData = skeletonData;
  145. skeleton.premultipliedAlpha = false;
  146. skeleton.setAnimation(0, 'animation', false);
  147. skeleton.setCompleteListener(() => {
  148. this.removeEffect(effectNode);
  149. });
  150. const canvas = find('Canvas');
  151. if (canvas) {
  152. canvas.addChild(effectNode);
  153. effectNode.setWorldPosition(worldPosition);
  154. // 设置特效缩放
  155. effectNode.setScale(0.8, 0.8, 1);
  156. // 添加到活动特效列表
  157. this.activeEffects.push(effectNode);
  158. } else {
  159. effectNode.destroy();
  160. }
  161. }
  162. /**
  163. * 移除特效节点
  164. * @param effectNode 要移除的特效节点
  165. */
  166. private removeEffect(effectNode: Node) {
  167. // 从活动特效列表中移除
  168. const index = this.activeEffects.indexOf(effectNode);
  169. if (index !== -1) {
  170. this.activeEffects.splice(index, 1);
  171. }
  172. // 销毁节点
  173. if (effectNode && effectNode.isValid) {
  174. effectNode.destroy();
  175. }
  176. }
  177. /**
  178. * 停止指定方块的动画
  179. * @param blockNode 方块节点
  180. */
  181. public stopBlockAnimation(blockNode: Node) {
  182. const animation = this.activeBlockAnimations.get(blockNode);
  183. if (animation) {
  184. animation.stop();
  185. this.activeBlockAnimations.delete(blockNode);
  186. // 恢复原始状态
  187. const sprite = blockNode.getComponent(Sprite);
  188. if (sprite) {
  189. sprite.color = Color.WHITE;
  190. }
  191. }
  192. }
  193. /**
  194. * 清理所有活动的特效
  195. */
  196. public clearAllEffects() {
  197. for (const effect of this.activeEffects) {
  198. if (effect && effect.isValid) {
  199. effect.destroy();
  200. }
  201. }
  202. this.activeEffects = [];
  203. }
  204. /**
  205. * 清除所有方块动画
  206. */
  207. public clearAllBlockAnimations() {
  208. // 停止所有方块动画
  209. for (const [blockNode, animation] of this.activeBlockAnimations) {
  210. if (animation) {
  211. animation.stop();
  212. }
  213. // 恢复方块状态
  214. const sprite = blockNode.getComponent(Sprite);
  215. if (sprite) {
  216. sprite.color = Color.WHITE;
  217. }
  218. }
  219. this.activeBlockAnimations.clear();
  220. }
  221. onDestroy() {
  222. // 组件销毁时清理所有特效和动画
  223. this.clearAllEffects();
  224. this.clearAllBlockAnimations();
  225. }
  226. /**
  227. * 获取BallAni实例
  228. * @returns BallAni实例
  229. */
  230. public static getInstance(): BallAni | null {
  231. const gameArea = find('Canvas/GameLevelUI/GameArea');
  232. if (!gameArea) {
  233. console.warn('[BallAni] 未找到GameArea节点');
  234. return null;
  235. }
  236. const ballAni = gameArea.getComponent(BallAni);
  237. if (!ballAni) {
  238. console.warn('[BallAni] GameArea节点上未找到BallAni组件');
  239. return null;
  240. }
  241. return ballAni;
  242. }
  243. }