Files
climb/assets/scripts/PlayerController.ts
2025-09-26 10:49:23 +08:00

706 lines
21 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { _decorator, Component, Node, Vec3, input, Input, EventTouch, Camera, view, tween, Animation, Collider2D, Contact2DType, Label, Color, Canvas, UITransform } 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({ 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);
// 播放攻击动画
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;
}, 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();
}
}