BallAni.ts 13 KB

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