727 lines
22 KiB
TypeScript
727 lines
22 KiB
TypeScript
import { _decorator, Component, Node, Vec3, input, Input, EventTouch, Camera, view, tween, Animation, Collider2D, Contact2DType, Label, Color, Canvas, UITransform, AudioSource } from 'cc';
|
||
import { TiledMapPathfinder } from './TiledMapPathfinder';
|
||
const { ccclass, property } = _decorator;
|
||
|
||
@ccclass('PlayerController')
|
||
export class PlayerController extends Component {
|
||
@property(Canvas)
|
||
canvas: Canvas | null = null;
|
||
|
||
@property(Node)
|
||
player: Node | null = null; // 玩家节点
|
||
|
||
@property(Node)
|
||
bonus: Node | null = null;
|
||
|
||
@property(Camera)
|
||
camera: Camera | null = null; // 主摄像机
|
||
|
||
@property(TiledMapPathfinder)
|
||
pathfinder: TiledMapPathfinder | null = null; // 寻路组件
|
||
|
||
@property(Node)
|
||
attackAudio: Node | null = null; // 攻击音效节点
|
||
|
||
@property({ range: [1, 300] })
|
||
moveSpeed: number = 300; // 移动速度(像素/秒)
|
||
|
||
@property
|
||
mapWidth: number = 1080; // 地图宽度
|
||
|
||
@property
|
||
mapHeight: number = 2560; // 地图高度
|
||
|
||
private isMoving: boolean = false;
|
||
private isAttacking: boolean = false;
|
||
private currentPath: Vec3[] = [];
|
||
private currentPathIndex: number = 0;
|
||
private originalPosition: Vec3 = new Vec3();
|
||
private currentAnimation: string = 'stand'; // 当前播放的动画
|
||
private lastTargetPosition: Vec3 = new Vec3(); // 上一个目标位置,用于方向判断
|
||
private isUpgraded: boolean = false; // 玩家是否已升级
|
||
private isGameOver: boolean = false; // 游戏是否结束(玩家死亡)
|
||
private isWin: boolean = false; // 游戏是否胜利(到达终点)
|
||
|
||
private hasWinTimes = 0
|
||
|
||
// 道具列表
|
||
private props: Node[] = [];
|
||
|
||
onLoad() {
|
||
// 注册触摸事件
|
||
input.on(Input.EventType.TOUCH_START, this.onTouchStart, this);
|
||
|
||
let collider = this.player.getComponent(Collider2D);
|
||
if (collider) {
|
||
// 监听碰撞事件
|
||
collider.on(Contact2DType.BEGIN_CONTACT, this.onBeginContact, this);
|
||
}
|
||
|
||
this.initProps();
|
||
}
|
||
|
||
onDestroy() {
|
||
// 移除触摸事件
|
||
input.off(Input.EventType.TOUCH_START, this.onTouchStart, this);
|
||
}
|
||
|
||
start() {
|
||
if (this.player) {
|
||
this.originalPosition.set(this.player.position);
|
||
}
|
||
}
|
||
|
||
initProps() {
|
||
if (!this.canvas) {
|
||
console.warn('Canvas未设置,无法初始化道具');
|
||
return;
|
||
}
|
||
|
||
// 查找Canvas下的Props节点
|
||
const propsNode = this.canvas.node.getChildByName('Props');
|
||
if (!propsNode) {
|
||
console.warn('未找到Props节点,请确保Canvas下有名为"Props"的节点');
|
||
return;
|
||
}
|
||
|
||
// 清空现有的道具列表
|
||
this.props.length = 0;
|
||
|
||
// 获取Props节点下的所有子节点
|
||
for (let i = 0; i < propsNode.children.length; i++) {
|
||
const child = propsNode.children[i];
|
||
this.props.push(child);
|
||
console.log(`添加道具: ${child.name}`);
|
||
|
||
// 为每个道具添加悬浮动画
|
||
this.addFloatingAnimation(child);
|
||
}
|
||
|
||
console.log(`初始化道具完成,共找到 ${this.props.length} 个道具`);
|
||
}
|
||
|
||
/**
|
||
* 为道具添加悬浮动画
|
||
*/
|
||
private addFloatingAnimation(propNode: Node) {
|
||
if (!propNode) return;
|
||
|
||
// 保存原始位置
|
||
const originalY = propNode.position.y;
|
||
const floatHeight = 10; // 悬浮高度(像素)
|
||
const floatDuration = 2; // 悬浮周期(秒)
|
||
|
||
// 创建上下浮动的动画
|
||
tween(propNode)
|
||
.to(floatDuration / 2, { position: new Vec3(propNode.position.x, originalY + floatHeight, propNode.position.z) }, {
|
||
easing: 'sineInOut'
|
||
})
|
||
.to(floatDuration / 2, { position: new Vec3(propNode.position.x, originalY, propNode.position.z) }, {
|
||
easing: 'sineInOut'
|
||
})
|
||
.union() // 将动画串联起来
|
||
.repeatForever() // 无限重复
|
||
.start();
|
||
|
||
console.log(`为道具 ${propNode.name} 添加悬浮动画`);
|
||
}
|
||
|
||
private onTouchStart(event: EventTouch) {
|
||
if (!this.player || !this.camera || !this.pathfinder || this.isAttacking || this.isGameOver || this.isWin) return;
|
||
|
||
// 获取触摸点的UI坐标
|
||
const touchLocation = event.getUILocation();
|
||
|
||
// 将UI坐标转换为世界坐标
|
||
const worldPos = this.screenToWorldPoint(touchLocation);
|
||
|
||
console.log(`触摸UI坐标: (${touchLocation.x}, ${touchLocation.y})`);
|
||
console.log(`转换后世界坐标: (${worldPos.x.toFixed(2)}, ${worldPos.y.toFixed(2)})`);
|
||
|
||
this.moveToPositionWithPathfinding(worldPos);
|
||
}
|
||
|
||
private screenToWorldPoint(screenPos: { x: number, y: number }): Vec3 {
|
||
if (!this.camera) {
|
||
console.error('Camera未设置,无法进行坐标转换');
|
||
return new Vec3(screenPos.x, screenPos.y, 0);
|
||
}
|
||
|
||
// 获取可见区域大小
|
||
const visibleSize = view.getVisibleSize();
|
||
|
||
// 计算屏幕中心点
|
||
const centerX = visibleSize.width * 0.5;
|
||
const centerY = visibleSize.height * 0.5;
|
||
|
||
// 将屏幕坐标转换为以屏幕中心为原点的坐标
|
||
const normalizedX = screenPos.x - centerX;
|
||
const normalizedY = screenPos.y - centerY;
|
||
|
||
// 获取相机的正交高度
|
||
const orthoHeight = this.camera.orthoHeight;
|
||
|
||
// 计算屏幕坐标到世界坐标的缩放比例
|
||
// 屏幕高度的一半对应 orthoHeight 的世界单位
|
||
const scaleY = orthoHeight / (visibleSize.height * 0.5);
|
||
const scaleX = scaleY; // 保持宽高比一致
|
||
|
||
// 将屏幕坐标转换为世界坐标(相对于相机中心)
|
||
const worldOffsetX = normalizedX * scaleX;
|
||
const worldOffsetY = normalizedY * scaleY;
|
||
|
||
// 考虑相机的位置偏移
|
||
const cameraPos = this.camera.node.position;
|
||
|
||
// 计算最终的世界坐标
|
||
const worldX = worldOffsetX + cameraPos.x;
|
||
const worldY = worldOffsetY + cameraPos.y;
|
||
|
||
return new Vec3(worldX, worldY, 0);
|
||
}
|
||
|
||
|
||
private moveToPositionWithPathfinding(worldPos: Vec3) {
|
||
if (!this.player || !this.pathfinder) return;
|
||
|
||
// 停止当前移动
|
||
this.stopMovement();
|
||
|
||
// 限制目标位置在地图边界内
|
||
const clampedPos = this.clampPlayerPosition(worldPos);
|
||
|
||
// 检查目标位置是否可行走
|
||
if (!this.pathfinder.isWorldPositionWalkable(clampedPos)) {
|
||
console.log('目标位置不可行走,寻找最近的可行走位置');
|
||
const closestWalkable = this.pathfinder.getClosestWalkablePosition(clampedPos);
|
||
if (!closestWalkable) {
|
||
console.warn('找不到可行走的位置');
|
||
return;
|
||
}
|
||
clampedPos.set(closestWalkable);
|
||
}
|
||
|
||
// 使用寻路算法计算路径
|
||
const startPos = this.player.position;
|
||
this.currentPath = this.pathfinder.findPath(startPos, clampedPos);
|
||
|
||
if (this.currentPath.length === 0) {
|
||
console.warn('无法找到路径');
|
||
return;
|
||
}
|
||
|
||
console.log(`找到路径,包含${this.currentPath.length}个点`);
|
||
|
||
// 开始沿路径移动
|
||
this.currentPathIndex = 0;
|
||
this.isMoving = true;
|
||
this.moveToNextWaypoint();
|
||
}
|
||
|
||
// 限制玩家位置在地图边界内
|
||
private clampPlayerPosition(position: Vec3): Vec3 {
|
||
// 计算地图边界(地图锚点为0.5,0.5,所以范围是-mapWidth/2到+mapWidth/2)
|
||
const mapHalfWidth = this.mapWidth * 0.5;
|
||
const mapHalfHeight = this.mapHeight * 0.5;
|
||
|
||
// 限制玩家位置
|
||
const clampedPosition = position.clone();
|
||
clampedPosition.x = Math.max(-mapHalfWidth, Math.min(mapHalfWidth, position.x));
|
||
clampedPosition.y = Math.max(-mapHalfHeight, Math.min(mapHalfHeight, position.y));
|
||
|
||
return clampedPosition;
|
||
}
|
||
|
||
/**
|
||
* 根据移动方向获取对应的动画名称
|
||
*/
|
||
private getAnimationNameByDirection(currentPos: Vec3, targetPos: Vec3): string {
|
||
const deltaX = targetPos.x - currentPos.x;
|
||
const deltaY = targetPos.y - currentPos.y;
|
||
|
||
// 如果移动距离很小,保持当前动画
|
||
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
||
if (distance < 1) {
|
||
return this.currentAnimation;
|
||
}
|
||
|
||
// 计算主要移动方向
|
||
const absX = Math.abs(deltaX);
|
||
const absY = Math.abs(deltaY);
|
||
|
||
// 添加角度判断,更精确地确定方向
|
||
const angle = Math.atan2(deltaY, deltaX) * 180 / Math.PI; // 转换为角度
|
||
|
||
if (absX > absY) {
|
||
// 水平移动为主
|
||
return deltaX > 0 ? 'walk3' : 'walk5';
|
||
} else {
|
||
// 垂直移动为主
|
||
return deltaY > 0 ? 'walk3' : 'walk5'; // 上移用walk3,下移用walk5
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 切换动画,避免不必要的切换
|
||
*/
|
||
private switchAnimation(animationName: string) {
|
||
if (!this.player) {
|
||
console.warn('Player节点未设置,无法切换动画');
|
||
return;
|
||
}
|
||
|
||
// 如果玩家已升级,在动画名称后添加 "_2" 后缀
|
||
let finalAnimationName = animationName;
|
||
if (this.isUpgraded && !animationName.endsWith('_2')) {
|
||
finalAnimationName = animationName + '_2';
|
||
}
|
||
|
||
if (this.currentAnimation === finalAnimationName) {
|
||
return; // 已经是目标动画,不需要切换
|
||
}
|
||
|
||
|
||
const animation = this.player.getComponent(Animation);
|
||
if (animation) {
|
||
// 检查动画是否存在
|
||
const state = animation.getState(finalAnimationName);
|
||
if (!state) {
|
||
console.warn(`动画 ${finalAnimationName} 不存在,使用默认动画`);
|
||
this.currentAnimation = 'stand';
|
||
animation.play('stand');
|
||
return;
|
||
}
|
||
|
||
this.currentAnimation = finalAnimationName;
|
||
animation.play(finalAnimationName);
|
||
console.log(`切换动画: ${finalAnimationName}`);
|
||
} else {
|
||
console.warn('未找到Animation组件,无法播放动画');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 移动到路径中的下一个路径点
|
||
*/
|
||
private moveToNextWaypoint() {
|
||
if (this.currentAnimation === 'attack' || this.isAttacking) {
|
||
return
|
||
}
|
||
|
||
if (!this.player || this.currentPath.length === 0 || this.currentPathIndex >= this.currentPath.length) {
|
||
this.isMoving = false;
|
||
this.switchAnimation('stand');
|
||
console.log('路径移动完成');
|
||
return;
|
||
}
|
||
|
||
|
||
|
||
const targetPos = this.currentPath[this.currentPathIndex];
|
||
const currentPos = this.player.position;
|
||
|
||
// 根据移动方向选择动画
|
||
const animationName = this.getAnimationNameByDirection(currentPos, targetPos);
|
||
|
||
// 切换到对应的动画
|
||
this.switchAnimation(animationName);
|
||
|
||
// 计算移动距离和时间
|
||
const distance = Vec3.distance(currentPos, targetPos);
|
||
const moveTime = distance / this.moveSpeed;
|
||
|
||
console.log(`移动到路径点${this.currentPathIndex}: (${targetPos.x.toFixed(2)}, ${targetPos.y.toFixed(2)})`);
|
||
|
||
// 记录目标位置用于方向判断
|
||
this.lastTargetPosition.set(targetPos);
|
||
|
||
// 使用缓动移动到目标位置
|
||
tween(this.player)
|
||
.to(moveTime, { position: targetPos }, {
|
||
onComplete: () => {
|
||
this.currentPathIndex++;
|
||
this.moveToNextWaypoint();
|
||
}
|
||
})
|
||
.start();
|
||
}
|
||
|
||
/**
|
||
* 停止当前移动
|
||
*/
|
||
private stopMovement() {
|
||
if (this.player) {
|
||
tween(this.player).stop();
|
||
}
|
||
this.isMoving = false;
|
||
this.currentPath = [];
|
||
this.currentPathIndex = 0;
|
||
|
||
// 停止移动时播放站立动画
|
||
this.switchAnimation('stand');
|
||
}
|
||
|
||
update(deltaTime: number) {
|
||
// 更新逻辑现在主要由缓动系统处理
|
||
// 这里可以添加其他需要每帧更新的逻辑
|
||
}
|
||
|
||
onBeginContact(selfCollider: Collider2D, otherCollider: Collider2D) {
|
||
if (otherCollider.node.name.startsWith('guai_')) {
|
||
this.handleAttack(otherCollider);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处理攻击逻辑
|
||
*/
|
||
private handleAttack(otherCollider: Collider2D) {
|
||
this.isAttacking = true;
|
||
console.log('开始攻击,怪物名称:', otherCollider.node.name);
|
||
|
||
// 获取玩家和怪物的生命值
|
||
const playerHpLabel = this.player.getChildByName('hp');
|
||
const monsterHpLabel = otherCollider.node.getChildByName('hp');
|
||
|
||
if (!playerHpLabel || !monsterHpLabel) {
|
||
console.warn('未找到生命值标签,玩家hp:', playerHpLabel, '怪物hp:', monsterHpLabel);
|
||
this.isAttacking = false;
|
||
return;
|
||
}
|
||
|
||
// 获取生命值数值
|
||
const playerLabel = playerHpLabel.getComponent(Label);
|
||
const monsterLabel = monsterHpLabel.getComponent(Label);
|
||
|
||
if (!playerLabel || !monsterLabel) {
|
||
console.warn('未找到Label组件');
|
||
this.isAttacking = false;
|
||
return;
|
||
}
|
||
|
||
const playerHp = parseInt(playerLabel.string) || 0;
|
||
const monsterHp = parseInt(monsterLabel.string) || 0;
|
||
console.log('玩家生命值:', playerHp, '怪物生命值:', monsterHp);
|
||
|
||
// 播放攻击音效
|
||
if (this.attackAudio) {
|
||
const audioSource = this.attackAudio.getComponent(AudioSource);
|
||
if (audioSource) {
|
||
audioSource.play();
|
||
console.log('播放攻击音效');
|
||
}
|
||
}
|
||
|
||
// 播放攻击动画
|
||
const monsterAnimation = otherCollider.node.getComponent(Animation);
|
||
if (monsterAnimation) {
|
||
monsterAnimation.play(`${otherCollider.node.name}_attack`);
|
||
}
|
||
this.switchAnimation('attack');
|
||
|
||
// 2秒后判定攻击结果
|
||
this.scheduleOnce(async () => {
|
||
// 比较生命值,判断输赢
|
||
console.log('判定攻击结果,玩家HP:', playerHp, '怪物HP:', monsterHp);
|
||
if (playerHp >= monsterHp) {
|
||
this.hasWinTimes++
|
||
// 玩家获胜
|
||
console.log('玩家获胜!更新玩家生命值为:', playerHp + monsterHp);
|
||
|
||
// 玩家生命值增加怪物生命值
|
||
const newPlayerHp = playerHp + monsterHp;
|
||
playerLabel.string = newPlayerHp.toString();
|
||
|
||
// 播放生命值标签的强调动画
|
||
this.playLabelEmphasisAnimation(playerLabel);
|
||
|
||
// 播放怪物死亡动画
|
||
if (monsterAnimation) {
|
||
monsterAnimation.play(`${otherCollider.node.name}_die`);
|
||
}
|
||
|
||
// 如果是攻击 guai_2 并且成功,创建道具飞向 player 的动画
|
||
if (otherCollider.node.name === 'guai_2') {
|
||
await this.createPropsFlyToPlayerAnimation()
|
||
}
|
||
|
||
// 1秒后怪物消失
|
||
this.scheduleOnce(() => {
|
||
otherCollider.node.destroy();
|
||
console.log('怪物已消失');
|
||
}, 1);
|
||
|
||
this.switchAnimation('stand'); // 玩家站立
|
||
|
||
if (this.hasWinTimes === 10) {
|
||
this.isWin = true
|
||
this.showBonusPopup()
|
||
}
|
||
} else {
|
||
// 怪物获胜
|
||
console.log('怪物获胜!玩家生命值变为0');
|
||
|
||
// 玩家生命值变为0
|
||
playerLabel.string = '0';
|
||
|
||
// 播放生命值标签的失败动画
|
||
this.playLabelFailAnimation(playerLabel);
|
||
|
||
// 玩家死亡动画
|
||
this.switchAnimation('die');
|
||
|
||
// 怪物站立动画
|
||
if (monsterAnimation) {
|
||
monsterAnimation.play(`${otherCollider.node.name}_stand`);
|
||
}
|
||
|
||
// 设置游戏结束标志,禁止后续寻路
|
||
this.isGameOver = true;
|
||
console.log('游戏结束,禁止寻路');
|
||
}
|
||
|
||
this.isAttacking = false;
|
||
|
||
// 停止攻击音效
|
||
if (this.attackAudio) {
|
||
const audioSource = this.attackAudio.getComponent(AudioSource);
|
||
if (audioSource) {
|
||
audioSource.stop();
|
||
console.log('停止攻击音效');
|
||
}
|
||
}
|
||
}, 2);
|
||
}
|
||
|
||
/**
|
||
* 播放生命值标签强调动画(成功时)
|
||
*/
|
||
private playLabelEmphasisAnimation(label: Label) {
|
||
if (!label) return;
|
||
|
||
const originalScale = label.node.scale.clone();
|
||
const originalColor = label.color.clone();
|
||
|
||
// 创建强调动画序列
|
||
tween(label.node)
|
||
.to(0.1, { scale: new Vec3(originalScale.x * 1.2, originalScale.y * 1.2, originalScale.z) })
|
||
.to(0.1, { scale: originalScale })
|
||
.to(0.1, { scale: new Vec3(originalScale.x * 1.1, originalScale.y * 1.1, originalScale.z) })
|
||
.to(0.1, { scale: originalScale })
|
||
.start();
|
||
|
||
// 颜色闪烁效果
|
||
tween(label)
|
||
.to(0.1, { color: new Color(255, 255, 0) }) // 黄色
|
||
.to(0.1, { color: new Color(0, 255, 0) }) // 绿色
|
||
.to(0.1, { color: originalColor })
|
||
.start();
|
||
}
|
||
|
||
/**
|
||
* 播放生命值标签失败动画(失败时)
|
||
*/
|
||
private playLabelFailAnimation(label: Label) {
|
||
if (!label) return;
|
||
|
||
const originalScale = label.node.scale.clone();
|
||
const originalColor = label.color.clone();
|
||
|
||
// 创建失败动画序列 - 震动效果
|
||
tween(label.node)
|
||
.to(0.05, { position: new Vec3(label.node.position.x - 5, label.node.position.y, label.node.position.z) })
|
||
.to(0.05, { position: new Vec3(label.node.position.x + 5, label.node.position.y, label.node.position.z) })
|
||
.to(0.05, { position: new Vec3(label.node.position.x - 5, label.node.position.y, label.node.position.z) })
|
||
.to(0.05, { position: new Vec3(label.node.position.x + 5, label.node.position.y, label.node.position.z) })
|
||
.to(0.05, { position: label.node.position })
|
||
.start();
|
||
|
||
// 颜色变红效果
|
||
tween(label)
|
||
.to(0.1, { color: new Color(255, 0, 0) }) // 红色
|
||
.to(0.1, { color: new Color(128, 0, 0) }) // 暗红色
|
||
.to(0.1, { color: originalColor })
|
||
.start();
|
||
}
|
||
|
||
/**
|
||
* 创建道具飞向玩家的动画(同步方法)
|
||
*/
|
||
private async createPropsFlyToPlayerAnimation(): Promise<void> {
|
||
if (!this.player || this.props.length === 0) {
|
||
console.warn('玩家或道具不存在,无法创建飞行动画');
|
||
return;
|
||
}
|
||
|
||
console.log('创建道具飞向玩家的动画');
|
||
|
||
// 获取玩家位置
|
||
const playerPos = this.player.position.clone();
|
||
|
||
// 创建所有道具的飞行动画承诺
|
||
const flyPromises: Promise<void>[] = [];
|
||
|
||
// 为每个道具创建飞行动画
|
||
this.props.forEach((prop, index) => {
|
||
if (!prop || !prop.isValid) return;
|
||
|
||
// 保存道具原始位置
|
||
const originalPos = prop.position.clone();
|
||
|
||
// 计算飞行时间(根据距离调整)
|
||
const distance = Vec3.distance(originalPos, playerPos);
|
||
const flyDuration = Math.max(0.5, distance / 500); // 最少0.5秒,速度500像素/秒
|
||
|
||
// 添加延迟,让道具依次飞向玩家
|
||
const delay = index * 0.1;
|
||
|
||
// 停止道具的悬浮动画
|
||
tween(prop).stop();
|
||
|
||
// 创建飞行动画的承诺
|
||
const flyPromise = new Promise<void>((resolve) => {
|
||
this.scheduleOnce(() => {
|
||
tween(prop)
|
||
.to(flyDuration, {
|
||
position: new Vec3(playerPos.x, playerPos.y, playerPos.z)
|
||
}, {
|
||
easing: 'quadOut',
|
||
onComplete: () => {
|
||
// 道具到达玩家位置后消失
|
||
prop.destroy();
|
||
console.log(`道具 ${prop.name} 已到达玩家位置并消失`);
|
||
resolve();
|
||
}
|
||
})
|
||
.start();
|
||
}, delay);
|
||
});
|
||
|
||
flyPromises.push(flyPromise);
|
||
});
|
||
|
||
// 等待所有道具飞行动画完成
|
||
await Promise.all(flyPromises);
|
||
|
||
// 所有动画完成后,播放升级动画并设置升级状态
|
||
this.playLevelUpAnimation();
|
||
this.isUpgraded = true;
|
||
console.log('所有道具飞行动画完成,玩家已升级,后续动画将使用升级版本');
|
||
}
|
||
|
||
/**
|
||
* 播放玩家升级动画
|
||
*/
|
||
private playLevelUpAnimation() {
|
||
if (!this.player) {
|
||
console.warn('玩家节点不存在,无法播放升级动画');
|
||
return;
|
||
}
|
||
|
||
// 查找levelUp子节点
|
||
const levelUpNode = this.player.getChildByName('levelUp');
|
||
if (!levelUpNode) {
|
||
console.warn('未找到levelUp子节点');
|
||
return;
|
||
}
|
||
|
||
// 获取动画组件
|
||
const levelUpAnimation = levelUpNode.getComponent(Animation);
|
||
if (!levelUpAnimation) {
|
||
console.warn('levelUp节点未找到Animation组件');
|
||
return;
|
||
}
|
||
|
||
console.log('播放玩家升级动画');
|
||
|
||
// 播放升级动画
|
||
levelUpAnimation.play('levelUp');
|
||
}
|
||
|
||
/**
|
||
* 显示奖励弹窗
|
||
* 根据当前镜头位置和正交高度,将奖励节点正确缩放并移动到画面正中间
|
||
*/
|
||
public showBonusPopup() {
|
||
if (!this.bonus || !this.camera || !this.canvas) {
|
||
console.warn('奖励节点、相机或画布未设置,无法显示奖励弹窗');
|
||
return;
|
||
}
|
||
|
||
// 确保奖励节点是激活状态
|
||
this.bonus.active = true;
|
||
|
||
// 获取相机位置,相机所在的世界坐标就是当前屏幕的中心
|
||
const cameraPos = this.camera.node.position;
|
||
const orthoHeight = this.camera.orthoHeight;
|
||
|
||
// 直接将弹窗设置到相机位置(屏幕中心)
|
||
this.bonus.setPosition(cameraPos.x, cameraPos.y, 0);
|
||
|
||
// 计算合适的缩放比例,确保弹窗在不同正交高度下都能正确显示
|
||
// 基础缩放比例
|
||
const baseScale = 1.0;
|
||
|
||
// 根据正交高度调整缩放,确保弹窗大小合适
|
||
// 假设标准正交高度为500,以此为基准进行缩放
|
||
const standardOrthoHeight = 500;
|
||
const scaleRatio = standardOrthoHeight / orthoHeight;
|
||
const finalScale = baseScale * scaleRatio;
|
||
|
||
// 设置奖励节点的缩放
|
||
this.bonus.setScale(finalScale, finalScale, 1);
|
||
|
||
// 添加弹窗出现动画
|
||
this.playBonusPopupAnimation();
|
||
}
|
||
|
||
/**
|
||
* 播放奖励弹窗出现动画
|
||
*/
|
||
private playBonusPopupAnimation() {
|
||
if (!this.bonus) return;
|
||
|
||
// 保存原始缩放
|
||
const originalScale = this.bonus.scale.clone();
|
||
|
||
// 初始状态设置为很小
|
||
this.bonus.setScale(0.1, 0.1, 1);
|
||
|
||
// 创建弹窗弹出动画
|
||
tween(this.bonus)
|
||
.to(0.3, {
|
||
scale: new Vec3(originalScale.x * 1.2, originalScale.y * 1.2, originalScale.z)
|
||
}, {
|
||
easing: 'backOut'
|
||
})
|
||
.to(0.1, {
|
||
scale: originalScale
|
||
}, {
|
||
easing: 'sineInOut'
|
||
})
|
||
.call(() => {
|
||
console.log('奖励弹窗显示完成');
|
||
})
|
||
.start();
|
||
}
|
||
|
||
/**
|
||
* 隐藏奖励弹窗
|
||
*/
|
||
public hideBonusPopup() {
|
||
if (!this.bonus) return;
|
||
|
||
// 创建弹窗消失动画
|
||
tween(this.bonus)
|
||
.to(0.2, {
|
||
scale: new Vec3(0.1, 0.1, 1)
|
||
}, {
|
||
easing: 'backIn'
|
||
})
|
||
.call(() => {
|
||
this.bonus.active = false;
|
||
console.log('奖励弹窗已隐藏');
|
||
})
|
||
.start();
|
||
}
|
||
} |