BallAni.ts 14 KB

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