DamageNumberAni.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. import { _decorator, Component, Node, Label, Vec3, tween, Tween, UITransform, UIOpacity, instantiate, resources, Prefab, Color, find } from 'cc';
  2. import { EnemyController } from '../CombatSystem/EnemyController';
  3. const { ccclass, property } = _decorator;
  4. /**
  5. * 伤害数字动画组件
  6. * 实现伤害数字从小变大再缩小消失的动画效果
  7. * 可挂载到EnemyController节点上,通过装饰器获取EnemyController组件
  8. */
  9. @ccclass('DamageNumberAni')
  10. export class DamageNumberAni extends Component {
  11. @property({ type: EnemyController, tooltip: 'EnemyController组件引用' })
  12. private enemyController: EnemyController = null;
  13. @property({ type: Node, tooltip: 'GameArea节点引用' })
  14. private gameArea: Node = null;
  15. @property({ type: Prefab, tooltip: 'DamageNumber预制体引用' })
  16. private damageNumberPrefab: Prefab = null;
  17. @property({ type: Prefab, tooltip: 'EnemyLabel预制体引用' })
  18. private enemyLabelPrefab: Prefab = null;
  19. onLoad() {
  20. // 自动获取同节点上的EnemyController组件
  21. if (!this.enemyController) {
  22. this.enemyController = this.getComponent(EnemyController);
  23. }
  24. // 只有在需要使用EnemyController功能时才报错
  25. // 动态创建的伤害数字节点不需要EnemyController组件
  26. if (!this.enemyController && this.damageNumberPrefab) {
  27. console.warn('[DamageNumberAni] 未找到EnemyController组件,某些功能可能不可用');
  28. }
  29. }
  30. /**
  31. * 创建并显示伤害数字
  32. * @param damage 伤害值
  33. * @param worldPosition 世界坐标位置(敌人头顶)
  34. * @param isCritical 是否暴击
  35. */
  36. public showDamageNumber(damage: number, worldPosition: Vec3, isCritical: boolean = false) {
  37. // 如果伤害为0,显示格挡文字而不是数字0
  38. if (damage === 0) {
  39. this.showBlockText(worldPosition);
  40. return;
  41. }
  42. // 检查预制体引用
  43. if (!this.damageNumberPrefab) {
  44. console.error('[DamageNumberAni] DamageNumber预制体引用未设置');
  45. return;
  46. }
  47. // 实例化伤害数字节点
  48. const damageNode = instantiate(this.damageNumberPrefab);
  49. // 使用装饰器引用的GameArea节点
  50. if (!this.gameArea) {
  51. console.error('[DamageNumberAni] GameArea节点引用未设置');
  52. damageNode.destroy();
  53. return;
  54. }
  55. // 添加到GameArea
  56. this.gameArea.addChild(damageNode);
  57. // 转换世界坐标到GameArea的本地坐标
  58. const gameAreaTransform = this.gameArea.getComponent(UITransform);
  59. if (gameAreaTransform) {
  60. const localPos = gameAreaTransform.convertToNodeSpaceAR(worldPosition);
  61. // 稍微向上偏移,显示在敌人头顶
  62. localPos.y += 30;
  63. damageNode.position = localPos;
  64. }
  65. // 设置伤害数字文本
  66. const label = damageNode.getComponent(Label);
  67. if (label) {
  68. // 显示精确的伤害值,保留一位小数(如果有小数部分)
  69. const damageText = damage % 1 === 0 ? damage.toString() : damage.toFixed(1);
  70. label.string = damageText;
  71. // 暴击时使用不同颜色
  72. if (isCritical) {
  73. label.color = Color.YELLOW; // 暴击显示黄色
  74. label.fontSize = 24; // 暴击字体更大
  75. } else {
  76. label.color = Color.WHITE; // 普通伤害白色
  77. label.fontSize = 20;
  78. }
  79. }
  80. // 将DamageNumberAni组件添加到伤害数字节点
  81. const aniComponent = damageNode.addComponent(DamageNumberAni);
  82. aniComponent.playAnimation(isCritical);
  83. }
  84. /**
  85. * 静态方法保持兼容性,但推荐使用实例方法
  86. * @param damage 伤害值
  87. * @param worldPosition 世界坐标位置(敌人头顶)
  88. * @param isCritical 是否暴击
  89. */
  90. public static showDamageNumber(damage: number, worldPosition: Vec3, isCritical: boolean = false) {
  91. // 尝试找到EnemyController节点上的DamageNumberAni组件
  92. const enemyControllerNode = find('Canvas/GameLevelUI/EnemyController');
  93. if (enemyControllerNode) {
  94. const damageAni = enemyControllerNode.getComponent(DamageNumberAni);
  95. if (damageAni) {
  96. damageAni.showDamageNumber(damage, worldPosition, isCritical);
  97. return;
  98. }
  99. }
  100. console.warn('[DamageNumberAni] 未找到挂载的DamageNumberAni组件,请确保组件已挂载到EnemyController节点');
  101. }
  102. /**
  103. * 播放伤害数字动画
  104. * @param isCritical 是否暴击
  105. */
  106. private playAnimation(isCritical: boolean = false) {
  107. // 初始状态:缩放为0,透明度为1
  108. this.node.setScale(0, 0, 1);
  109. const uiTransform = this.node.getComponent(UITransform);
  110. const uiOpacity = this.node.getComponent(UIOpacity);
  111. if (uiOpacity) {
  112. uiOpacity.opacity = 255;
  113. }
  114. // 动画参数
  115. const maxScale = isCritical ? 1.3 : 1.0; // 暴击时放大更多
  116. const growDuration = 0.2; // 放大阶段持续时间
  117. const stayDuration = 0.3; // 停留时间
  118. const shrinkDuration = 0.4; // 缩小消失时间
  119. const floatDistance = 50; // 向上漂浮距离
  120. // 第一阶段:从0放大到最大尺寸(从底部开始放大)
  121. tween(this.node)
  122. .to(growDuration, {
  123. scale: new Vec3(maxScale, maxScale, 1)
  124. }, {
  125. easing: 'backOut' // 弹性效果
  126. })
  127. .call(() => {
  128. // 第二阶段:保持大小,开始向上漂浮并逐渐缩小透明
  129. const startPos = this.node.position.clone();
  130. const endPos = startPos.clone();
  131. endPos.y += floatDistance;
  132. // 位置动画
  133. tween(this.node)
  134. .to(stayDuration + shrinkDuration, {
  135. position: endPos
  136. }, {
  137. easing: 'sineOut'
  138. })
  139. .start();
  140. // 延迟后开始缩小和淡出
  141. tween(this.node)
  142. .delay(stayDuration)
  143. .to(shrinkDuration, {
  144. scale: new Vec3(0.3, 0.3, 1)
  145. }, {
  146. easing: 'sineIn'
  147. })
  148. .start();
  149. // 透明度动画
  150. if (uiOpacity) {
  151. tween(uiOpacity)
  152. .delay(stayDuration)
  153. .to(shrinkDuration, {
  154. opacity: 0
  155. }, {
  156. easing: 'sineIn'
  157. })
  158. .call(() => {
  159. // 动画完成,销毁节点
  160. if (this.node && this.node.isValid) {
  161. this.node.destroy();
  162. }
  163. })
  164. .start();
  165. }
  166. })
  167. .start();
  168. }
  169. /**
  170. * 显示格挡文字
  171. * @param worldPosition 世界坐标位置(敌人头顶)
  172. */
  173. public showBlockText(worldPosition: Vec3) {
  174. // 检查预制体引用
  175. if (!this.enemyLabelPrefab) {
  176. console.error('[DamageNumberAni] EnemyLabel预制体引用未设置');
  177. return;
  178. }
  179. // 实例化格挡文字节点
  180. const blockTextNode = instantiate(this.enemyLabelPrefab);
  181. // 使用装饰器引用的GameArea节点
  182. if (!this.gameArea) {
  183. console.error('[DamageNumberAni] GameArea节点引用未设置');
  184. blockTextNode.destroy();
  185. return;
  186. }
  187. // 添加到GameArea
  188. this.gameArea.addChild(blockTextNode);
  189. // 转换世界坐标到GameArea的本地坐标
  190. const gameAreaTransform = this.gameArea.getComponent(UITransform);
  191. if (gameAreaTransform) {
  192. const localPos = gameAreaTransform.convertToNodeSpaceAR(worldPosition);
  193. // 稍微向上偏移,显示在敌人头顶
  194. localPos.y += 30;
  195. blockTextNode.position = localPos;
  196. }
  197. // 设置文字内容
  198. const label = blockTextNode.getComponent(Label);
  199. if (label) {
  200. label.string = "格挡";
  201. label.color = Color.CYAN; // 使用青色显示格挡文字
  202. label.fontSize = 22;
  203. }
  204. // 将DamageNumberAni组件添加到格挡文字节点
  205. const aniComponent = blockTextNode.addComponent(DamageNumberAni);
  206. console.log("格挡",aniComponent);
  207. aniComponent.playBlockAnimation();
  208. }
  209. /**
  210. * 播放格挡文字动画
  211. */
  212. private playBlockAnimation(): void {
  213. // 初始状态:缩放为0,透明度为1
  214. this.node.setScale(0, 0, 1);
  215. const uiOpacity = this.node.getComponent(UIOpacity);
  216. if (uiOpacity) {
  217. uiOpacity.opacity = 255;
  218. }
  219. // 创建动画序列
  220. tween(this.node)
  221. .to(0.2, { scale: new Vec3(1.2, 1.2, 1) }) // 放大
  222. .to(0.1, { scale: new Vec3(1, 1, 1) }) // 回到正常大小
  223. .delay(0.5) // 停留0.5秒
  224. .parallel( // 同时执行淡出和向上移动
  225. tween().to(0.3, { scale: new Vec3(0.8, 0.8, 1) }),
  226. tween().by(0.3, { position: new Vec3(0, 20, 0) }),
  227. uiOpacity ? tween(uiOpacity).to(0.3, { opacity: 0 }) : tween()
  228. )
  229. .call(() => {
  230. if (this.node && this.node.isValid) {
  231. this.node.destroy(); // 动画结束后销毁节点
  232. }
  233. })
  234. .start();
  235. }
  236. onDestroy() {
  237. // 清理所有tween动画
  238. Tween.stopAllByTarget(this.node);
  239. const uiTransform = this.node.getComponent(UITransform);
  240. const uiOpacity = this.node.getComponent(UIOpacity);
  241. if (uiTransform) {
  242. Tween.stopAllByTarget(uiTransform);
  243. }
  244. if (uiOpacity) {
  245. Tween.stopAllByTarget(uiOpacity);
  246. }
  247. }
  248. }