BallAni.ts 16 KB

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