BallAni.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. import { _decorator, Component, Node, Vec3, tween, Color,find, Sprite, resources, sp, Material } 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. // 如果没有活跃敌人,延迟一小段时间再次检查(处理游戏启动时的时序问题)
  45. if (!hasActiveEnemies) {
  46. console.log('[BallAni] 首次检查未发现敌人,启动延迟检查机制');
  47. // 延迟0.5秒后再次检查,给敌人生成一些时间
  48. this.scheduleOnce(() => {
  49. console.log('[BallAni] 开始延迟检查敌人状态');
  50. eventBus.emit(GameEvents.BALL_FIRE_BULLET, {
  51. canFire: (canFire: boolean) => {
  52. hasActiveEnemies = canFire;
  53. }
  54. });
  55. console.log('[BallAni] 延迟检查敌人状态结果:', hasActiveEnemies);
  56. if (hasActiveEnemies) {
  57. console.log('[BallAni] 延迟检查成功找到敌人,执行完整动画');
  58. } else {
  59. console.log('[BallAni] 延迟检查仍未找到敌人,执行简化动画');
  60. }
  61. // 如果延迟检查后仍然没有敌人,则按无敌人处理
  62. this.executeBlockAnimation(blockNode, hasActiveEnemies);
  63. }, 0.5);
  64. return; // 提前返回,等待延迟检查
  65. }
  66. console.log('[BallAni] 开始播放方块撞击动画', blockNode.name, '有敌人:', hasActiveEnemies);
  67. console.log('[BallAni] hasActiveEnemies 检查结果:', hasActiveEnemies);
  68. // 立即执行动画
  69. this.executeBlockAnimation(blockNode, hasActiveEnemies);
  70. }
  71. /**
  72. * 执行方块动画的具体逻辑
  73. * @param blockNode 方块节点
  74. * @param hasActiveEnemies 是否有活跃敌人
  75. */
  76. private executeBlockAnimation(blockNode: Node, hasActiveEnemies: boolean) {
  77. if (!blockNode || !blockNode.isValid) {
  78. console.warn('[BallAni] 方块节点无效');
  79. return;
  80. }
  81. // 如果该方块已经在播放动画,跳过
  82. if (this.activeBlockAnimations.has(blockNode)) {
  83. console.log('[BallAni] 方块动画已在播放中,跳过');
  84. return;
  85. }
  86. const sprite = blockNode.getComponent(Sprite);
  87. // 保存原始材质和Sprite缩放
  88. const originalMaterial = sprite ? sprite.material : null;
  89. const originalSpriteScale = sprite ? new Vec3(1, 1, 1) : new Vec3(1, 1, 1);
  90. if (hasActiveEnemies) {
  91. // 有敌人时播放Sprite缩放动画(不影响碰撞体积)
  92. const shrinkSpriteScale = new Vec3(0.7, 0.7, 1);
  93. // 应用材质
  94. console.log('[BallAni] 检查材质状态:', {
  95. hasSprite: !!sprite,
  96. hasCustomMaterial: sprite ? !!sprite.customMaterial : false,
  97. currentMaterial: sprite ? sprite.material : null
  98. });
  99. if (sprite && sprite.customMaterial) {
  100. sprite.material = sprite.customMaterial;
  101. console.log('[BallAni] 应用自定义材质成功');
  102. } else if (sprite) {
  103. // 动态加载灰色材质
  104. const grayMaterialPath = 'shaders/ui-sprite-white-material.mtl';
  105. resources.load(grayMaterialPath, Material, (err, material) => {
  106. if (!err && material && sprite && sprite.isValid) {
  107. sprite.material = material;
  108. console.log('[BallAni] 动态加载并应用灰色材质成功');
  109. } else {
  110. console.log('[BallAni] 无法加载灰色材质,不使用任何材质');
  111. }
  112. });
  113. }
  114. // 对Sprite节点进行缩放动画(不影响方块碰撞体积)
  115. const animationTween = tween(sprite.node)
  116. .to(0.2, { scale: shrinkSpriteScale })
  117. .to(0.2, { scale: originalSpriteScale })
  118. .call(() => {
  119. // 动画完成时恢复原始材质
  120. console.log('[BallAni] 恢复材质状态:', {
  121. hasSprite: !!sprite,
  122. hasOriginalMaterial: !!originalMaterial,
  123. currentMaterial: sprite ? sprite.material : null
  124. });
  125. if (sprite && originalMaterial) {
  126. sprite.material = originalMaterial;
  127. console.log('[BallAni] 恢复原始材质成功');
  128. } else {
  129. console.log('[BallAni] 无法恢复原始材质 - 原始材质丢失');
  130. }
  131. console.log('[BallAni] 方块动画完成,恢复原状');
  132. // 动画完成,从活动列表中移除
  133. this.activeBlockAnimations.delete(blockNode);
  134. })
  135. .start();
  136. // 添加到活动动画列表
  137. this.activeBlockAnimations.set(blockNode, animationTween);
  138. } else {
  139. // 没有敌人时直接结束动画,不应用材质
  140. const animationTween = tween({})
  141. .delay(0.2)
  142. .call(() => {
  143. console.log('[BallAni] 无敌人,直接恢复原状');
  144. // 动画完成,从活动列表中移除
  145. this.activeBlockAnimations.delete(blockNode);
  146. })
  147. .start();
  148. // 添加到活动动画列表
  149. this.activeBlockAnimations.set(blockNode, animationTween);
  150. }
  151. }
  152. /**
  153. * 预加载撞击特效资源
  154. */
  155. private preloadImpactEffect() {
  156. if (BallAni.impactEffectSkeleton || BallAni.isLoading) {
  157. return;
  158. }
  159. BallAni.isLoading = true;
  160. const path = 'Animation/WeaponTx/tx0005/tx0005';
  161. resources.load(path, sp.SkeletonData, (err, sData: sp.SkeletonData) => {
  162. BallAni.isLoading = false;
  163. if (err || !sData) {
  164. console.warn('[BallAni] 加载撞击特效失败:', err);
  165. return;
  166. }
  167. BallAni.impactEffectSkeleton = sData;
  168. console.log('[BallAni] 撞击特效资源加载成功');
  169. });
  170. }
  171. /**
  172. * 在指定位置播放撞击特效
  173. * @param worldPosition 世界坐标位置
  174. */
  175. public playImpactEffect(worldPosition: Vec3) {
  176. // 如果资源未加载,直接加载并播放
  177. if (!BallAni.impactEffectSkeleton) {
  178. const path = 'Animation/WeaponTx/tx0005/tx0005';
  179. resources.load(path, sp.SkeletonData, (err, sData: sp.SkeletonData) => {
  180. if (err || !sData) {
  181. console.warn('[BallAni] 加载撞击特效失败:', err);
  182. return;
  183. }
  184. BallAni.impactEffectSkeleton = sData;
  185. this.createAndPlayEffect(worldPosition, sData);
  186. });
  187. return;
  188. }
  189. this.createAndPlayEffect(worldPosition, BallAni.impactEffectSkeleton);
  190. }
  191. /**
  192. * 创建并播放特效
  193. */
  194. private createAndPlayEffect(worldPosition: Vec3, skeletonData: sp.SkeletonData) {
  195. const effectNode = new Node('ImpactEffect');
  196. const skeleton = effectNode.addComponent(sp.Skeleton);
  197. skeleton.skeletonData = skeletonData;
  198. skeleton.premultipliedAlpha = false;
  199. skeleton.setAnimation(0, 'animation', false);
  200. skeleton.setCompleteListener(() => {
  201. this.removeEffect(effectNode);
  202. });
  203. const canvas = find('Canvas');
  204. if (canvas) {
  205. canvas.addChild(effectNode);
  206. effectNode.setWorldPosition(worldPosition);
  207. // 设置特效缩放
  208. effectNode.setScale(0.8, 0.8, 1);
  209. // 添加到活动特效列表
  210. this.activeEffects.push(effectNode);
  211. } else {
  212. effectNode.destroy();
  213. }
  214. }
  215. /**
  216. * 移除特效节点
  217. * @param effectNode 要移除的特效节点
  218. */
  219. private removeEffect(effectNode: Node) {
  220. // 从活动特效列表中移除
  221. const index = this.activeEffects.indexOf(effectNode);
  222. if (index !== -1) {
  223. this.activeEffects.splice(index, 1);
  224. }
  225. // 销毁节点
  226. if (effectNode && effectNode.isValid) {
  227. effectNode.destroy();
  228. }
  229. }
  230. /**
  231. * 停止指定方块的动画
  232. * @param blockNode 方块节点
  233. */
  234. public stopBlockAnimation(blockNode: Node) {
  235. const animation = this.activeBlockAnimations.get(blockNode);
  236. if (animation) {
  237. animation.stop();
  238. this.activeBlockAnimations.delete(blockNode);
  239. }
  240. }
  241. /**
  242. * 清理所有活动的特效
  243. */
  244. public clearAllEffects() {
  245. for (const effect of this.activeEffects) {
  246. if (effect && effect.isValid) {
  247. effect.destroy();
  248. }
  249. }
  250. this.activeEffects = [];
  251. }
  252. /**
  253. * 清除所有方块动画
  254. */
  255. public clearAllBlockAnimations() {
  256. // 停止所有方块动画
  257. for (const [blockNode, animation] of this.activeBlockAnimations) {
  258. if (animation) {
  259. animation.stop();
  260. }
  261. }
  262. this.activeBlockAnimations.clear();
  263. }
  264. onDestroy() {
  265. // 组件销毁时清理所有特效和动画
  266. this.clearAllEffects();
  267. this.clearAllBlockAnimations();
  268. }
  269. /**
  270. * 获取BallAni实例
  271. * @returns BallAni实例
  272. */
  273. public static getInstance(): BallAni | null {
  274. const gameArea = find('Canvas/GameLevelUI/GameArea');
  275. if (!gameArea) {
  276. console.warn('[BallAni] 未找到GameArea节点');
  277. return null;
  278. }
  279. const ballAni = gameArea.getComponent(BallAni);
  280. if (!ballAni) {
  281. console.warn('[BallAni] GameArea节点上未找到BallAni组件');
  282. return null;
  283. }
  284. return ballAni;
  285. }
  286. }