import { _decorator, Component, Node, Vec3, input, Input, EventTouch, Camera, view, tween, Animation, Collider2D, Contact2DType, Label, Color, Canvas, UITransform, AudioSource, Sprite, director } 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(Node) bonusWuqi: Node | null = null; @property(Node) failedDialog: 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 currentDirection: number = 5; // 当前玩家朝向:3表示左/上,5表示右/下,默认为5 private hasWinTimes = 0 // 道具列表 private props: Node[] = []; private guideNode: Node | null = null; private activePopup: Node | null = null; private activePopupName: string | null = null; private pendingPopupHide: (() => void) | null = null; onLoad() { this.guideNode = this.canvas.node.getChildByName('Guide'); // 注册触摸事件 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(); // this.showBonusPopup() // this.scheduleOnce(() => { // this.showBonusPopup() // }, 5); } onDestroy() { // 移除触摸事件 input.off(Input.EventType.TOUCH_START, this.onTouchStart, this); this.clearPopupHideSchedule(); } 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.activePopup) { this.hideActivePopup(); return; } if (!this.player || !this.camera || !this.pathfinder || this.isAttacking || this.isGameOver || this.isWin) return; this.guideNode.active = false; // 获取触摸点的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) { // 水平移动为主 this.currentDirection = deltaX < 0 ? 3 : 5; return deltaX < 0 ? 'walk3' : 'walk5'; } else { // 垂直移动为主 this.currentDirection = deltaY < 0 ? 3 : 5; return deltaY < 0 ? 'walk3' : 'walk5'; // 上移用walk3,下移用walk5 } } /** * 切换动画,避免不必要的切换 */ private switchAnimation(animationName: string) { if (!this.player) { console.warn('Player节点未设置,无法切换动画'); return; } // 如果切换到站立动画,根据当前方向选择对应的站立动画 let targetAnimationName = animationName; if (animationName === 'stand') { targetAnimationName = this.currentDirection === 3 ? 'stand3' : 'stand5'; } // 如果玩家已升级,在动画名称后添加 "_2" 后缀 let finalAnimationName = targetAnimationName; if (this.isUpgraded && !targetAnimationName.endsWith('_2')) { finalAnimationName = targetAnimationName + '_2'; } if (this.currentAnimation === finalAnimationName) { return; // 已经是目标动画,不需要切换 } const animation = this.player.getChildByName('Anim').getComponent(Animation); if (animation) { // 检查动画是否存在 const state = animation.getState(finalAnimationName); if (!state) { console.warn(`动画 ${finalAnimationName} 不存在,使用默认动画`); this.currentAnimation = 'stand5'; animation.play('stand5'); 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.getChildByName('Anim').getComponent(Animation); if (monsterAnimation) { monsterAnimation.play(`${otherCollider.node.name}_attack`); } this.switchAnimation(this.currentDirection === 3 ? 'attack3' : 'attack5'); // 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('怪物已消失'); if (this.hasWinTimes === 10) { this.isWin = true this.showBonusPopup() } }, 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.isGameOver = true; console.log('游戏结束,禁止寻路'); // 显示失败弹窗 this.scheduleOnce(() => { this.showFailedDialog(); }, 1); // 延迟1秒显示失败弹窗,让玩家死亡动画播放完成 } 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 { if (!this.player || this.props.length === 0) { console.warn('玩家或道具不存在,无法创建飞行动画'); return; } console.log('创建道具飞向玩家的动画'); // 获取玩家位置 const playerPos = this.player.position.clone(); // 创建所有道具的飞行动画承诺 const flyPromises: Promise[] = []; // 为每个道具创建飞行动画 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((resolve) => { this.scheduleOnce(() => { tween(prop) .to(flyDuration, { position: new Vec3(playerPos.x, playerPos.y, playerPos.z) }, { easing: 'quadOut', onComplete: () => { prop.active = false; resolve(); } }) .start(); }, delay); }); flyPromises.push(flyPromise); }); // 等待所有道具飞行动画完成 await Promise.all(flyPromises); // 所有动画完成后,播放升级动画并设置升级状态 this.playLevelUpAnimation(); this.isUpgraded = true; this.showWeaponBonusPopup(); 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() { this.showPopupAtCameraCenter(this.bonus, '奖励弹窗'); } /** * 弹出武器奖励 */ public showWeaponBonusPopup() { // this.showPopupAtCameraCenter(this.bonusWuqi, '武器奖励'); } private showPopupAtCameraCenter(popup: Node | null, nameForLog: string) { if (!popup || !this.camera || !this.canvas) { console.warn(`${nameForLog}节点、相机或画布未设置,无法显示${nameForLog}`); return; } this.clearPopupHideSchedule(); popup.active = true; const cameraPos = this.camera.node.position; const orthoHeight = this.camera.orthoHeight; popup.setPosition(cameraPos.x, cameraPos.y, 0); const baseScale = 0.8; const standardOrthoHeight = 500; const scaleRatio = standardOrthoHeight / orthoHeight; const finalScale = baseScale * scaleRatio; popup.setScale(finalScale, finalScale, 1); this.activePopup = popup; this.activePopupName = nameForLog; this.playPopupAppearAnimation(popup, nameForLog); this.pendingPopupHide = () => { this.pendingPopupHide = null; this.hideActivePopup(); }; this.scheduleOnce(this.pendingPopupHide, 3); } /** * 播放奖励弹窗出现动画 */ private playPopupAppearAnimation(popup: Node, nameForLog: string) { const originalScale = popup.scale.clone(); popup.setScale(0.1, 0.1, 1); tween(popup) .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(`${nameForLog}显示完成`); }) .start(); } /** * 隐藏奖励弹窗 */ public hideBonusPopup() { if (!this.bonus) return; if (this.activePopup === this.bonus) { this.hideActivePopup(); return; } if (!this.bonus.active) return; this.hidePopupWithAnimation(this.bonus, '奖励弹窗'); } private hideActivePopup() { if (!this.activePopup) { return; } const popupToHide = this.activePopup; const nameForLog = this.activePopupName || '弹窗'; this.clearPopupHideSchedule(); this.activePopup = null; this.activePopupName = null; this.hidePopupWithAnimation(popupToHide, nameForLog); } private hidePopupWithAnimation(popup: Node, nameForLog: string) { tween(popup).stop(); tween(popup) .to(0.2, { scale: new Vec3(0.1, 0.1, 1) }, { easing: 'backIn' }) .call(() => { popup.active = false; console.log(`${nameForLog}已隐藏`); }) .start(); } private clearPopupHideSchedule() { if (this.pendingPopupHide) { this.unschedule(this.pendingPopupHide); this.pendingPopupHide = null; } } /** * 显示失败弹窗 */ public showFailedDialog() { this.showPopupAtCameraCenter(this.failedDialog, '失败弹窗'); this.setupRetryButtonListener(); } /** * 设置重试按钮监听器 */ private setupRetryButtonListener() { if (!this.failedDialog) { console.warn('失败弹窗节点未设置,无法监听重试按钮'); return; } // 查找Retry按钮节点 const retryButton = this.failedDialog.getChildByName('Retry'); if (!retryButton) { console.warn('未找到Retry按钮节点'); return; } // 移除之前的监听器(如果存在) retryButton.off(Node.EventType.TOUCH_END, this.onRetryButtonClick, this); // 添加新的监听器 retryButton.on(Node.EventType.TOUCH_END, this.onRetryButtonClick, this); console.log('已设置重试按钮监听器'); } /** * 重试按钮点击事件处理 */ private onRetryButtonClick() { console.log('重试按钮被点击,重新加载当前场景'); // 隐藏失败弹窗 if (this.failedDialog) { this.hidePopupWithAnimation(this.failedDialog, '失败弹窗'); } // 重新加载当前场景 this.scheduleOnce(() => { // 使用Cocos Creator的场景管理器重新加载当前场景 const sceneName = director.getScene().name; director.loadScene(sceneName); }, 0.3); // 延迟0.3秒,让弹窗消失动画完成 } }