Files
climb/assets/scripts/PlayerController.ts
2025-09-22 17:34:28 +08:00

421 lines
13 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 } from 'cc';
import { TiledMapPathfinder } from './TiledMapPathfinder';
const { ccclass, property } = _decorator;
@ccclass('PlayerController')
export class PlayerController extends Component {
@property(Node)
player: 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(); // 上一个目标位置,用于方向判断
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);
}
}
onDestroy() {
// 移除触摸事件
input.off(Input.EventType.TOUCH_START, this.onTouchStart, this);
}
start() {
if (this.player) {
this.originalPosition.set(this.player.position);
}
}
private onTouchStart(event: EventTouch) {
if (!this.player || !this.camera || !this.pathfinder || this.isAttacking) 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 cameraPos = this.camera.node.position;
// 计算世界坐标
const worldX = normalizedX + cameraPos.x;
const worldY = normalizedY + 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;
}
if (this.currentAnimation === animationName) {
return; // 已经是目标动画,不需要切换
}
const animation = this.player.getComponent(Animation);
if (animation) {
// 检查动画是否存在
const state = animation.getState(animationName);
if (!state) {
console.warn(`动画 ${animationName} 不存在,使用默认动画`);
this.currentAnimation = 'stand';
animation.play('stand');
return;
}
this.currentAnimation = animationName;
animation.play(animationName);
console.log(`切换动画: ${animationName}`);
} else {
console.warn('未找到Animation组件无法播放动画');
}
}
/**
* 移动到路径中的下一个路径点
*/
private moveToNextWaypoint() {
if (this.currentAnimation === 'attack') {
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(() => {
// 比较生命值,判断输赢
console.log('判定攻击结果玩家HP:', playerHp, '怪物HP:', monsterHp);
if (playerHp >= monsterHp) {
// 玩家获胜
console.log('玩家获胜!更新玩家生命值为:', playerHp + monsterHp);
// 玩家生命值增加怪物生命值
const newPlayerHp = playerHp + monsterHp;
playerLabel.string = newPlayerHp.toString();
// 播放生命值标签的强调动画
this.playLabelEmphasisAnimation(playerLabel);
// 播放怪物死亡动画
if (monsterAnimation) {
monsterAnimation.play(`${otherCollider.node.name}_die`);
}
// 1秒后怪物消失
this.scheduleOnce(() => {
otherCollider.node.destroy();
console.log('怪物已消失');
}, 1);
this.switchAnimation('stand'); // 玩家站立
} else {
// 怪物获胜
console.log('怪物获胜玩家生命值变为0');
// 玩家生命值变为0
playerLabel.string = '0';
// 播放生命值标签的失败动画
this.playLabelFailAnimation(playerLabel);
// 玩家死亡动画
this.switchAnimation('die');
// 怪物站立动画
if (monsterAnimation) {
monsterAnimation.play(`${otherCollider.node.name}_stand`);
}
}
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();
}
}