| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293 |
- import { _decorator, Component, Node, Label, Vec3, tween, Tween, UITransform, UIOpacity, instantiate, resources, Prefab, Color, find } from 'cc';
- import { EnemyController } from '../CombatSystem/EnemyController';
- const { ccclass, property } = _decorator;
- /**
- * 伤害数字动画组件
- * 实现伤害数字从小变大再缩小消失的动画效果
- * 可挂载到EnemyController节点上,通过装饰器获取EnemyController组件
- */
- @ccclass('DamageNumberAni')
- export class DamageNumberAni extends Component {
-
- @property({ type: EnemyController, tooltip: 'EnemyController组件引用' })
- private enemyController: EnemyController = null;
-
- @property({ type: Node, tooltip: 'GameArea节点引用' })
- private gameArea: Node = null;
-
- @property({ type: Prefab, tooltip: 'DamageNumber预制体引用' })
- private damageNumberPrefab: Prefab = null;
-
- @property({ type: Prefab, tooltip: 'EnemyLabel预制体引用' })
- private enemyLabelPrefab: Prefab = null;
- // 为频繁触发的伤害数字提供左右交替的偏移,避免重叠
- @property({ tooltip: '伤害数字左右偏移距离(像素)' })
- private spawnOffsetX: number = 24;
- private alternatingSide: number = 1; // 1: 向右, -1: 向左
-
- onLoad() {
- // 自动获取同节点上的EnemyController组件
- if (!this.enemyController) {
- this.enemyController = this.getComponent(EnemyController);
- }
-
- // 只有在需要使用EnemyController功能时才报错
- // 动态创建的伤害数字节点不需要EnemyController组件
- if (!this.enemyController && this.damageNumberPrefab) {
- console.warn('[DamageNumberAni] 未找到EnemyController组件,某些功能可能不可用');
- }
- }
-
- /**
- * 创建并显示伤害数字
- * @param damage 伤害值
- * @param worldPosition 世界坐标位置(敌人头顶)
- * @param isCritical 是否暴击
- */
- public showDamageNumber(damage: number, worldPosition: Vec3, isCritical: boolean = false) {
- // 如果伤害为0,显示格挡文字而不是数字0
- if (damage === 0) {
- this.showBlockText(worldPosition);
- return;
- }
-
- // 检查预制体引用
- if (!this.damageNumberPrefab) {
- console.error('[DamageNumberAni] DamageNumber预制体引用未设置');
- return;
- }
-
- // 实例化伤害数字节点
- const damageNode = instantiate(this.damageNumberPrefab);
-
- // 使用装饰器引用的GameArea节点
- if (!this.gameArea) {
- console.error('[DamageNumberAni] GameArea节点引用未设置');
- damageNode.destroy();
- return;
- }
-
- // 添加到GameArea
- this.gameArea.addChild(damageNode);
-
- // 转换世界坐标到GameArea的本地坐标
- const gameAreaTransform = this.gameArea.getComponent(UITransform);
- if (gameAreaTransform) {
- const localPos = gameAreaTransform.convertToNodeSpaceAR(worldPosition);
- // 稍微向上偏移,显示在敌人头顶
- localPos.y += 30;
- // 为频繁伤害提供左右交替偏移,避免重叠在同一点
- const offset = isCritical ? this.spawnOffsetX * 1.2 : this.spawnOffsetX;
- localPos.x += this.alternatingSide * offset;
- // 切换下次生成方向
- this.alternatingSide = -this.alternatingSide;
- damageNode.position = localPos;
- }
-
- // 设置伤害数字文本
- const label = damageNode.getComponent(Label);
- if (label) {
- // 显示精确的伤害值,保留一位小数(如果有小数部分)
- const damageText = damage % 1 === 0 ? damage.toString() : damage.toFixed(1);
- label.string = damageText;
-
- // 暴击时使用不同颜色
- if (isCritical) {
- label.color = Color.YELLOW; // 暴击显示黄色
- label.fontSize = 30; // 暴击字体更大
- } else {
- label.color = Color.WHITE; // 普通伤害白色
- label.fontSize = 25;
- }
- }
-
- // 将DamageNumberAni组件添加到伤害数字节点
- const aniComponent = damageNode.addComponent(DamageNumberAni);
- aniComponent.playAnimation(isCritical);
- }
-
- /**
- * 静态方法保持兼容性,但推荐使用实例方法
- * @param damage 伤害值
- * @param worldPosition 世界坐标位置(敌人头顶)
- * @param isCritical 是否暴击
- */
- public static showDamageNumber(damage: number, worldPosition: Vec3, isCritical: boolean = false) {
- // 尝试找到EnemyController节点上的DamageNumberAni组件
- const enemyControllerNode = find('Canvas/GameLevelUI/EnemyController');
- if (enemyControllerNode) {
- const damageAni = enemyControllerNode.getComponent(DamageNumberAni);
- if (damageAni) {
- damageAni.showDamageNumber(damage, worldPosition, isCritical);
- return;
- }
- }
-
- console.warn('[DamageNumberAni] 未找到挂载的DamageNumberAni组件,请确保组件已挂载到EnemyController节点');
- }
-
- /**
- * 播放伤害数字动画
- * @param isCritical 是否暴击
- */
- private playAnimation(isCritical: boolean = false) {
- // 初始状态:缩放为0,透明度为1
- this.node.setScale(0, 0, 1);
- const uiTransform = this.node.getComponent(UITransform);
- const uiOpacity = this.node.getComponent(UIOpacity);
- if (uiOpacity) {
- uiOpacity.opacity = 255;
- }
-
- // 动画参数
- const maxScale = isCritical ? 1.3 : 1.0; // 暴击时放大更多
- const growDuration = 0.2; // 放大阶段持续时间
- const stayDuration = 0.3; // 停留时间
- const shrinkDuration = 0.4; // 缩小消失时间
- const floatDistance = 50; // 向上漂浮距离
-
- // 第一阶段:从0放大到最大尺寸(从底部开始放大)
- tween(this.node)
- .to(growDuration, {
- scale: new Vec3(maxScale, maxScale, 1)
- }, {
- easing: 'backOut' // 弹性效果
- })
- .call(() => {
- // 第二阶段:保持大小,开始向上漂浮并逐渐缩小透明
- const startPos = this.node.position.clone();
- const endPos = startPos.clone();
- endPos.y += floatDistance;
-
- // 位置动画
- tween(this.node)
- .to(stayDuration + shrinkDuration, {
- position: endPos
- }, {
- easing: 'sineOut'
- })
- .start();
-
- // 延迟后开始缩小和淡出
- tween(this.node)
- .delay(stayDuration)
- .to(shrinkDuration, {
- scale: new Vec3(0.3, 0.3, 1)
- }, {
- easing: 'sineIn'
- })
- .start();
-
- // 透明度动画
- if (uiOpacity) {
- tween(uiOpacity)
- .delay(stayDuration)
- .to(shrinkDuration, {
- opacity: 0
- }, {
- easing: 'sineIn'
- })
- .call(() => {
- // 动画完成,销毁节点
- if (this.node && this.node.isValid) {
- this.node.destroy();
- }
- })
- .start();
- }
- })
- .start();
- }
-
- /**
- * 显示格挡文字
- * @param worldPosition 世界坐标位置(敌人头顶)
- */
- public showBlockText(worldPosition: Vec3) {
- // 检查预制体引用
- if (!this.enemyLabelPrefab) {
- console.error('[DamageNumberAni] EnemyLabel预制体引用未设置');
- return;
- }
-
- // 实例化格挡文字节点
- const blockTextNode = instantiate(this.enemyLabelPrefab);
-
- // 使用装饰器引用的GameArea节点
- if (!this.gameArea) {
- console.error('[DamageNumberAni] GameArea节点引用未设置');
- blockTextNode.destroy();
- return;
- }
-
- // 添加到GameArea
- this.gameArea.addChild(blockTextNode);
-
- // 转换世界坐标到GameArea的本地坐标
- const gameAreaTransform = this.gameArea.getComponent(UITransform);
- if (gameAreaTransform) {
- const localPos = gameAreaTransform.convertToNodeSpaceAR(worldPosition);
- // 稍微向上偏移,显示在敌人头顶
- localPos.y += 30;
- blockTextNode.position = localPos;
- }
-
- // 设置文字内容
- const label = blockTextNode.getComponent(Label);
- if (label) {
- label.string = "格挡";
- label.color = Color.CYAN; // 使用青色显示格挡文字
- label.fontSize = 22;
- }
-
- // 将DamageNumberAni组件添加到格挡文字节点
- const aniComponent = blockTextNode.addComponent(DamageNumberAni);
- console.log("格挡",aniComponent);
- aniComponent.playBlockAnimation();
- }
-
- /**
- * 播放格挡文字动画
- */
- private playBlockAnimation(): void {
- // 初始状态:缩放为0,透明度为1
- this.node.setScale(0, 0, 1);
- const uiOpacity = this.node.getComponent(UIOpacity);
-
- if (uiOpacity) {
- uiOpacity.opacity = 255;
- }
-
- // 创建动画序列
- tween(this.node)
- .to(0.2, { scale: new Vec3(1.2, 1.2, 1) }) // 放大
- .to(0.1, { scale: new Vec3(1, 1, 1) }) // 回到正常大小
- .delay(0.5) // 停留0.5秒
- .parallel( // 同时执行淡出和向上移动
- tween().to(0.3, { scale: new Vec3(0.8, 0.8, 1) }),
- tween().by(0.3, { position: new Vec3(0, 20, 0) }),
- uiOpacity ? tween(uiOpacity).to(0.3, { opacity: 0 }) : tween()
- )
- .call(() => {
- if (this.node && this.node.isValid) {
- this.node.destroy(); // 动画结束后销毁节点
- }
- })
- .start();
- }
-
- onDestroy() {
- // 清理所有tween动画
- Tween.stopAllByTarget(this.node);
- const uiTransform = this.node.getComponent(UITransform);
- const uiOpacity = this.node.getComponent(UIOpacity);
- if (uiTransform) {
- Tween.stopAllByTarget(uiTransform);
- }
- if (uiOpacity) {
- Tween.stopAllByTarget(uiOpacity);
- }
- }
- }
|