BallAni.ts 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468
  1. import { _decorator, Component, Node, Vec3, tween, Color,find, Sprite, resources, sp, Material } from 'cc';
  2. import EventBus, { GameEvents } from '../Core/EventBus';
  3. import { BundleLoader } from '../Core/BundleLoader';
  4. const { ccclass, property } = _decorator;
  5. /**
  6. * 小球撞击动画管理器
  7. * 负责在小球撞击时播放特效动画
  8. */
  9. @ccclass('BallAni')
  10. export class BallAni extends Component {
  11. // 撞击特效骨骼数据
  12. @property({
  13. type: sp.SkeletonData,
  14. tooltip: '拖拽撞击特效骨骼数据到这里(Animation/WeaponTx/tx0005/tx0005)'
  15. })
  16. public impactEffectSkeleton: sp.SkeletonData = null;
  17. // 白色描边材质
  18. @property({
  19. type: Material,
  20. tooltip: '拖拽白色描边材质到这里(shaders/ui-sprite-white-material)'
  21. })
  22. public whiteMaterial: Material = null;
  23. // 撞击特效预制体缓存(保持静态缓存用于性能优化)
  24. private static impactEffectSkeleton: sp.SkeletonData = null;
  25. private static isLoading: boolean = false;
  26. // 白色描边材质缓存(保持静态缓存用于性能优化)
  27. private static whiteMaterial: Material = null;
  28. private static isMaterialLoading: boolean = false;
  29. // 当前播放中的特效节点列表
  30. private activeEffects: Node[] = [];
  31. // 当前播放中的方块动画列表
  32. private activeBlockAnimations: Map<Node, any> = new Map();
  33. // 敌人生成状态
  34. private enemySpawningActive: boolean = false;
  35. start() {
  36. // 使用装饰器挂载的资源初始化静态缓存
  37. if (this.impactEffectSkeleton) {
  38. BallAni.impactEffectSkeleton = this.impactEffectSkeleton;
  39. console.log('[BallAni] ✅ 撞击特效资源通过装饰器加载成功');
  40. } else {
  41. console.warn('[BallAni] ⚠️ 撞击特效资源未设置,将使用动态加载');
  42. this.preloadImpactEffect();
  43. }
  44. if (this.whiteMaterial) {
  45. BallAni.whiteMaterial = this.whiteMaterial;
  46. console.log('[BallAni] ✅ 白色描边材质通过装饰器加载成功');
  47. } else {
  48. console.warn('[BallAni] ⚠️ 白色描边材质未设置,将使用动态加载');
  49. this.preloadWhiteMaterial();
  50. }
  51. // 监听敌人生成状态事件
  52. const eventBus = EventBus.getInstance();
  53. eventBus.on(GameEvents.ENEMY_SPAWNING_STARTED, this.onEnemySpawningStarted, this);
  54. eventBus.on(GameEvents.ENEMY_SPAWNING_STOPPED, this.onEnemySpawningStopped, this);
  55. }
  56. private onEnemySpawningStarted() {
  57. this.enemySpawningActive = true;
  58. }
  59. private onEnemySpawningStopped() {
  60. this.enemySpawningActive = false;
  61. // 当敌人生成停止时,立即检查是否需要发射子弹
  62. const eventBus = EventBus.getInstance();
  63. eventBus.emit(GameEvents.BALL_FIRE_BULLET, {
  64. canFire: (canFire: boolean) => { }
  65. });
  66. }
  67. /**
  68. * 播放方块被撞击动画
  69. * @param blockNode 方块节点
  70. */
  71. public playBlockHitAnimation(blockNode: Node) {
  72. if (!blockNode || !blockNode.isValid) {
  73. console.warn('[BallAni] 方块节点无效');
  74. return;
  75. }
  76. // 如果该方块已经在播放动画,跳过
  77. if (this.activeBlockAnimations.has(blockNode)) {
  78. console.log('[BallAni] 方块动画已在播放中,跳过');
  79. return;
  80. }
  81. // 技能选择UI弹出且当前是拖拽态时,跳过任何挂载/动画以避免误绑定
  82. const selectSkillUI = find('Canvas/GameLevelUI/SelectSkillUI');
  83. const isSkillUIActive = !!(selectSkillUI && selectSkillUI.active);
  84. const isDragging = (blockNode as any)['isDragging'] === true;
  85. if (isSkillUIActive && isDragging) {
  86. return;
  87. }
  88. // 仅对GameArea中的真实方块执行动画,防止误用到UI节点
  89. if (!this.isNodeUnder(blockNode, 'GameArea') || this.isNodeUnder(blockNode, 'SelectSkillUI')) {
  90. return;
  91. }
  92. // 检查是否有活跃敌人,只有有敌人时才播放缩放动画
  93. const eventBus = EventBus.getInstance();
  94. let hasActiveEnemies = false;
  95. // 通过事件系统检查是否有活跃敌人
  96. eventBus.emit(GameEvents.BALL_FIRE_BULLET, {
  97. canFire: (canFire: boolean) => {
  98. hasActiveEnemies = canFire;
  99. }
  100. });
  101. // 立即执行动画
  102. this.executeBlockAnimation(blockNode, hasActiveEnemies);
  103. // 如果敌人生成未激活,立即检查是否需要发射子弹
  104. if (!this.enemySpawningActive) {
  105. eventBus.emit(GameEvents.BALL_FIRE_BULLET, {
  106. canFire: (canFire: boolean) => { }
  107. });
  108. }
  109. }
  110. /**
  111. * 判断节点是否位于指定祖先之下
  112. */
  113. private isNodeUnder(node: Node, ancestorName: string): boolean {
  114. let current: Node | null = node;
  115. while (current) {
  116. if (current.name === ancestorName) return true;
  117. current = current.parent;
  118. }
  119. return false;
  120. }
  121. /**
  122. * 执行方块动画的具体逻辑
  123. * @param blockNode 方块节点
  124. * @param hasActiveEnemies 是否有活跃敌人
  125. */
  126. private executeBlockAnimation(blockNode: Node, hasActiveEnemies: boolean) {
  127. if (!blockNode || !blockNode.isValid) {
  128. console.warn('[BallAni] 方块节点无效');
  129. return;
  130. }
  131. // 如果该方块已经在播放动画,跳过
  132. if (this.activeBlockAnimations.has(blockNode)) {
  133. console.log('[BallAni] 方块动画已在播放中,跳过');
  134. return;
  135. }
  136. // 新的预制体结构:Sprite组件在Node子节点上
  137. const nodeChild = blockNode.getChildByName('Node');
  138. const sprite = nodeChild ? nodeChild.getComponent(Sprite) : null;
  139. // 查找预制体根节点和Weapon节点
  140. // 如果传入的是子节点(如"Node"),需要找到预制体根节点
  141. let prefabRoot = blockNode;
  142. if (blockNode.name === 'Node' && blockNode.parent) {
  143. prefabRoot = blockNode.parent;
  144. }
  145. const weaponNode = prefabRoot.getChildByName('Weapon');
  146. // 保存原始材质和缩放
  147. // 优先保存customMaterial,如果没有则保存当前material
  148. const originalMaterial = sprite ? (sprite.customMaterial || sprite.material) : null;
  149. const originalBlockScale = new Vec3(blockNode.scale.x, blockNode.scale.y, blockNode.scale.z);
  150. const originalWeaponScale = weaponNode ? new Vec3(weaponNode.scale.x, weaponNode.scale.y, weaponNode.scale.z) : null;
  151. if (hasActiveEnemies) {
  152. // 有敌人时播放方块缩放动画(包括Node和Weapon子节点)
  153. const shrinkBlockScale = new Vec3(0.8, 0.8, 1);
  154. // 应用白色描边材质到Node子节点的Sprite组件
  155. // 优先使用装饰器挂载的白色材质
  156. let whiteMaterial = null;
  157. if (this.whiteMaterial) {
  158. whiteMaterial = this.whiteMaterial;
  159. } else if (BallAni.whiteMaterial) {
  160. whiteMaterial = BallAni.whiteMaterial;
  161. }
  162. if (sprite && whiteMaterial) {
  163. sprite.material = whiteMaterial;
  164. }
  165. // 对方块预制体进行缩放动画
  166. // Weapon节点需要在其原有0.4倍基础上再进行缩放动画
  167. const weaponShrinkScale = originalWeaponScale ? new Vec3(
  168. originalWeaponScale.x * 0.8, // 在0.4倍基础上再缩小到0.8倍
  169. originalWeaponScale.y * 0.8,
  170. originalWeaponScale.z
  171. ) : null;
  172. // 统一使用代码控制的 Tween 动画(不使用组件)
  173. const animationTween = tween(blockNode)
  174. .to(0.1, { scale: shrinkBlockScale }, {
  175. onStart: () => {
  176. // 开始动画时,同时缩放Weapon节点
  177. if (weaponNode && weaponShrinkScale) {
  178. tween(weaponNode)
  179. .to(0.1, { scale: weaponShrinkScale })
  180. .start();
  181. }
  182. }
  183. })
  184. .to(0.2, { scale: originalBlockScale }, {
  185. onStart: () => {
  186. // 恢复动画时,同时恢复Weapon节点
  187. if (weaponNode && originalWeaponScale) {
  188. tween(weaponNode)
  189. .to(0.2, { scale: originalWeaponScale })
  190. .start();
  191. }
  192. }
  193. })
  194. .call(() => {
  195. // 确保Weapon节点恢复到原始缩放
  196. if (weaponNode && originalWeaponScale) {
  197. weaponNode.scale = originalWeaponScale;
  198. }
  199. // 动画完成时恢复原始材质
  200. if (sprite && originalMaterial) {
  201. // 如果原始材质是customMaterial,则恢复customMaterial
  202. if (sprite.customMaterial === originalMaterial) {
  203. sprite.material = sprite.customMaterial;
  204. } else {
  205. sprite.material = originalMaterial;
  206. }
  207. } else if (sprite) {
  208. // 如果没有保存的原始材质,尝试恢复到customMaterial
  209. if (sprite.customMaterial) {
  210. sprite.material = sprite.customMaterial;
  211. } else {
  212. sprite.material = null;
  213. }
  214. }
  215. // 动画完成,从活动列表中移除
  216. this.activeBlockAnimations.delete(blockNode);
  217. })
  218. .start();
  219. // 添加到活动动画列表
  220. this.activeBlockAnimations.set(blockNode, animationTween);
  221. } else {
  222. // 没有敌人时直接结束动画,不应用材质
  223. const animationTween = tween({})
  224. .delay(0.2)
  225. .call(() => {
  226. // 动画完成,从活动列表中移除
  227. this.activeBlockAnimations.delete(blockNode);
  228. })
  229. .start();
  230. // 添加到活动动画列表
  231. this.activeBlockAnimations.set(blockNode, animationTween);
  232. }
  233. }
  234. /**
  235. * 预加载撞击特效资源
  236. */
  237. private preloadImpactEffect() {
  238. if (BallAni.impactEffectSkeleton || BallAni.isLoading) {
  239. return;
  240. }
  241. BallAni.isLoading = true;
  242. const path = 'WeaponTx/tx0005/tx0005';
  243. BundleLoader.loadSkeletonData(path).then((sData) => {
  244. BallAni.isLoading = false;
  245. if (!sData) {
  246. console.warn('[BallAni] 加载撞击特效失败: 资源为空');
  247. return;
  248. }
  249. BallAni.impactEffectSkeleton = sData;
  250. console.log('[BallAni] 撞击特效资源加载成功');
  251. }).catch((err) => {
  252. BallAni.isLoading = false;
  253. console.warn('[BallAni] 加载撞击特效失败:', err);
  254. });
  255. }
  256. /**
  257. * 预加载白色描边材质
  258. */
  259. private preloadWhiteMaterial() {
  260. if (BallAni.whiteMaterial || BallAni.isMaterialLoading) {
  261. return;
  262. }
  263. BallAni.isMaterialLoading = true;
  264. const materialPath = 'shaders/ui-sprite-white-material';
  265. resources.load(materialPath, Material, (err, material: Material) => {
  266. BallAni.isMaterialLoading = false;
  267. if (err || !material) {
  268. console.warn('[BallAni] 加载白色描边材质失败:', err);
  269. return;
  270. }
  271. BallAni.whiteMaterial = material;
  272. console.log('[BallAni] 白色描边材质加载成功');
  273. });
  274. }
  275. /**
  276. * 在指定位置播放撞击特效
  277. * @param worldPosition 世界坐标位置
  278. */
  279. public playImpactEffect(worldPosition: Vec3) {
  280. // 优先使用装饰器挂载的资源
  281. if (this.impactEffectSkeleton) {
  282. this.createAndPlayEffect(worldPosition, this.impactEffectSkeleton);
  283. return;
  284. }
  285. // 如果装饰器资源未设置,使用静态缓存
  286. if (BallAni.impactEffectSkeleton) {
  287. this.createAndPlayEffect(worldPosition, BallAni.impactEffectSkeleton);
  288. return;
  289. }
  290. // 最后才使用动态加载
  291. const path = 'WeaponTx/tx0005/tx0005';
  292. const bundleLoader = BundleLoader.getInstance();
  293. BundleLoader.loadSkeletonData(path).then((sData) => {
  294. if (!sData) {
  295. console.warn('[BallAni] 加载撞击特效失败: 资源为空');
  296. return;
  297. }
  298. BallAni.impactEffectSkeleton = sData;
  299. this.createAndPlayEffect(worldPosition, sData);
  300. }).catch((err) => {
  301. console.warn('[BallAni] 加载撞击特效失败:', err);
  302. });
  303. }
  304. /**
  305. * 创建并播放特效
  306. */
  307. private createAndPlayEffect(worldPosition: Vec3, skeletonData: sp.SkeletonData) {
  308. const effectNode = new Node('ImpactEffect');
  309. const skeleton = effectNode.addComponent(sp.Skeleton);
  310. skeleton.skeletonData = skeletonData;
  311. skeleton.premultipliedAlpha = false;
  312. skeleton.setAnimation(0, 'animation', false);
  313. skeleton.setCompleteListener(() => {
  314. this.removeEffect(effectNode);
  315. });
  316. const canvas = find('Canvas');
  317. if (canvas) {
  318. canvas.addChild(effectNode);
  319. effectNode.setWorldPosition(worldPosition);
  320. // 设置特效缩放
  321. effectNode.setScale(1.5, 1.5, 1);
  322. // 设置特效更亮 - 增加亮度
  323. const color = new Color(255, 255, 255, 255);
  324. color.r = Math.min(255, color.r * 1.5);
  325. color.g = Math.min(255, color.g * 1.5);
  326. color.b = Math.min(255, color.b * 1.5);
  327. effectNode.getComponent(sp.Skeleton).color = color;
  328. // 添加到活动特效列表
  329. this.activeEffects.push(effectNode);
  330. } else {
  331. effectNode.destroy();
  332. }
  333. }
  334. /**
  335. * 移除特效节点
  336. * @param effectNode 要移除的特效节点
  337. */
  338. private removeEffect(effectNode: Node) {
  339. // 从活动特效列表中移除
  340. const index = this.activeEffects.indexOf(effectNode);
  341. if (index !== -1) {
  342. this.activeEffects.splice(index, 1);
  343. }
  344. // 销毁节点
  345. if (effectNode && effectNode.isValid) {
  346. effectNode.destroy();
  347. }
  348. }
  349. /**
  350. * 停止指定方块的动画
  351. * @param blockNode 方块节点
  352. */
  353. public stopBlockAnimation(blockNode: Node) {
  354. const animation = this.activeBlockAnimations.get(blockNode);
  355. if (animation) {
  356. animation.stop();
  357. this.activeBlockAnimations.delete(blockNode);
  358. }
  359. // 代码控制,无需组件停止
  360. }
  361. /**
  362. * 清理所有活动的特效
  363. */
  364. public clearAllEffects() {
  365. for (const effect of this.activeEffects) {
  366. if (effect && effect.isValid) {
  367. effect.destroy();
  368. }
  369. }
  370. this.activeEffects = [];
  371. }
  372. /**
  373. * 清除所有方块动画
  374. */
  375. public clearAllBlockAnimations() {
  376. // 停止所有方块动画
  377. for (const [blockNode, animation] of this.activeBlockAnimations) {
  378. if (animation) {
  379. animation.stop();
  380. }
  381. }
  382. this.activeBlockAnimations.clear();
  383. }
  384. onDestroy() {
  385. // 解绑事件监听
  386. const eventBus = EventBus.getInstance();
  387. eventBus.off(GameEvents.ENEMY_SPAWNING_STARTED, this.onEnemySpawningStarted, this);
  388. eventBus.off(GameEvents.ENEMY_SPAWNING_STOPPED, this.onEnemySpawningStopped, this);
  389. // 组件销毁时清理所有特效和动画
  390. this.clearAllEffects();
  391. this.clearAllBlockAnimations();
  392. }
  393. /**
  394. * 获取BallAni实例
  395. * @returns BallAni实例
  396. */
  397. public static getInstance(): BallAni | null {
  398. const gameArea = find('Canvas/GameLevelUI/GameArea');
  399. if (!gameArea) {
  400. console.warn('[BallAni] 未找到GameArea节点');
  401. return null;
  402. }
  403. const ballAni = gameArea.getComponent(BallAni);
  404. if (!ballAni) {
  405. console.warn('[BallAni] GameArea节点上未找到BallAni组件');
  406. return null;
  407. }
  408. return ballAni;
  409. }
  410. }