feat(player): 改进玩家动画系统,支持四方向动画

- 添加新的动画文件支持上下左右四个方向的站立和行走动画
- 重构PlayerDirection枚举,支持左上、左下、右上、右下四个象限
- 优化动画切换逻辑,增加动画候选机制和兜底策略
- 改进方向判断算法,基于移动增量精确计算朝向
- 移除BonusWuqi相关资源和节点引用
- 更新场景文件,添加新动画剪辑引用
This commit is contained in:
richarjiang
2025-10-16 09:18:22 +08:00
parent 0e803bc5f0
commit 3908bb6935
219 changed files with 14387 additions and 270 deletions

View File

@@ -6,6 +6,13 @@ const { ccclass, property } = _decorator;
// EPhysics2DDrawFlags.Aabb |
// EPhysics2DDrawFlags.Shape;
enum PlayerDirection {
LeftUp = 'LeftUp',
LeftDown = 'LeftDown',
RightUp = 'RightUp',
RightDown = 'RightDown',
}
@ccclass('PlayerController')
export class PlayerController extends Component {
@property(Canvas)
@@ -38,12 +45,12 @@ export class PlayerController extends Component {
private currentPath: Vec3[] = [];
private currentPathIndex: number = 0;
private originalPosition: Vec3 = new Vec3();
private currentAnimation: string = 'stand'; // 当前播放的动画
private currentAnimation: string | null = null; // 当前播放的动画剪辑名称
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 currentDirection: PlayerDirection = PlayerDirection.RightDown; // 当前玩家朝向:四象限(左上/左下/右上/右下)
// 平滑移动相关变量
private moveTween: any = null; // 当前移动的tween对象
@@ -279,20 +286,13 @@ export class PlayerController extends Component {
// 如果移动距离很小,保持当前动画
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
if (distance < 1) {
return this.currentAnimation.includes('walk') ? 'walk' : this.currentAnimation;
if (this.isCurrentAction('walk')) {
return 'walk';
}
return 'stand';
}
// 计算主要移动方向
const absX = Math.abs(deltaX);
const absY = Math.abs(deltaY);
if (absX > absY) {
// 水平移动为主
this.currentDirection = deltaX < 0 ? 3 : 5;
} else {
// 垂直移动为主
this.currentDirection = deltaY < 0 ? 3 : 5;
}
this.currentDirection = this.resolveDirectionFromDelta(deltaX, deltaY);
// 只返回基础动画名称,不包含方向信息
return 'walk';
@@ -302,63 +302,65 @@ export class PlayerController extends Component {
* 切换动画,避免不必要的切换
* 根据 this.currentDirection 来决定 player 的 scale 是否要取反,不再需要通过动画名称进行区分
*/
private switchAnimation(animationName: string) {
private switchAnimation(actionName: string) {
if (!this.player) {
console.warn('Player节点未设置无法切换动画');
return;
}
// 根据动画类型获取基础动画名称,不再包含方向信息
let baseAnimationName = animationName;
if (animationName === 'stand') {
baseAnimationName = 'stand';
} else if (animationName.startsWith('walk')) {
baseAnimationName = 'walk';
} else if (animationName.startsWith('attack')) {
baseAnimationName = 'attack';
} else if (animationName.startsWith('die')) {
baseAnimationName = 'die';
const animNode = this.player.getChildByName('Anim');
if (!animNode) {
console.warn('未找到Anim子节点无法切换动画');
return;
}
// 如果玩家已升级,在动画名称后添加 "_2" 后缀
let finalAnimationName = baseAnimationName;
if (this.isUpgraded && !baseAnimationName.endsWith('_2')) {
finalAnimationName = baseAnimationName + '_2';
const animation = animNode.getComponent(Animation);
if (!animation) {
console.warn('未找到Animation组件无法播放动画');
return;
}
const candidates = this.getAnimationNameCandidates(actionName);
let finalAnimationName: string | null = null;
for (const candidate of candidates) {
if (!candidate) {
continue;
}
const state = animation.getState(candidate);
if (state) {
finalAnimationName = candidate;
break;
}
}
if (!finalAnimationName) {
const fallback = this.isUpgraded ? 'stand_2' : 'stand5';
if (!this.currentAnimation || this.currentAnimation !== fallback) {
console.warn(`未找到可用的动画 ${actionName},使用兜底动画 ${fallback}`);
}
const fallbackState = animation.getState(fallback);
if (!fallbackState) {
console.error('无法找到兜底动画,动画切换失败');
return;
}
finalAnimationName = fallback;
}
// 检查是否需要切换动画
if (this.currentAnimation === finalAnimationName) {
// 即使动画名称相同也需要检查方向是否改变可能需要调整scale
this.updatePlayerScale();
return;
}
const animation = this.player.getChildByName('Anim').getComponent(Animation);
if (animation) {
// 检查动画是否存在
const state = animation.getState(finalAnimationName);
if (!state) {
console.warn(`动画 ${finalAnimationName} 不存在,使用默认动画`);
this.currentAnimation = 'stand_2';
animation.play('stand_2');
this.updatePlayerScale();
return;
}
this.currentAnimation = finalAnimationName;
animation.play(finalAnimationName);
console.log(`切换动画: ${finalAnimationName}`);
// 根据当前方向更新玩家scale
this.updatePlayerScale();
} else {
console.warn('未找到Animation组件无法播放动画');
}
animation.play(finalAnimationName);
this.currentAnimation = finalAnimationName;
console.log(`切换动画: ${finalAnimationName}`);
this.updatePlayerScale();
}
/**
* 根据当前方向更新玩家Anim子节点的scale
* 当 currentDirection 为 3 时,需要翻转Anim节点scale.x 取反)
* 左向LeftUp/LeftDown需要翻转Anim节点scale.x 取反)
*/
private updatePlayerScale() {
if (!this.player) return;
@@ -371,28 +373,127 @@ export class PlayerController extends Component {
}
const currentScale = animNode.scale.clone();
const faceLeft = this.currentDirection === PlayerDirection.LeftUp || this.currentDirection === PlayerDirection.LeftDown;
const desiredScaleX = faceLeft ? -Math.abs(currentScale.x) : Math.abs(currentScale.x);
// 根据方向决定是否需要翻转
// currentDirection: 3表示左/上5表示右/下
if (this.currentDirection === 3) {
// 需要翻转确保scale.x为负值
if (currentScale.x > 0) {
animNode.setScale(-currentScale.x, currentScale.y, currentScale.z);
}
} else {
// 不需要翻转确保scale.x为正值
if (currentScale.x < 0) {
animNode.setScale(-currentScale.x, currentScale.y, currentScale.z);
}
if (currentScale.x !== desiredScaleX) {
animNode.setScale(desiredScaleX, currentScale.y, currentScale.z);
}
}
private normalizeAnimationAction(animationName: string): string {
if (!animationName) {
return animationName;
}
if (animationName.startsWith('walk')) {
return 'walk';
}
if (animationName.startsWith('attack')) {
return 'attack';
}
if (animationName.startsWith('stand')) {
return 'stand';
}
if (animationName.startsWith('die')) {
return 'die';
}
return animationName;
}
private isCurrentAction(action: string): boolean {
if (!this.currentAnimation) {
return false;
}
return this.normalizeAnimationAction(this.currentAnimation) === action;
}
private resolveDirectionFromDelta(
deltaX: number,
deltaY: number,
overrides?: { horizontal?: 'Left' | 'Right', vertical?: 'Up' | 'Down' }
): PlayerDirection {
const horizontalThreshold = 0.5;
const verticalThreshold = 0.5;
let horizontal: 'Left' | 'Right' = overrides?.horizontal ?? (this.isFacingLeft() ? 'Left' : 'Right');
if (!overrides?.horizontal && Math.abs(deltaX) > horizontalThreshold) {
horizontal = deltaX < 0 ? 'Left' : 'Right';
}
let vertical: 'Up' | 'Down' = overrides?.vertical ?? (this.isFacingUp() ? 'Up' : 'Down');
if (!overrides?.vertical && Math.abs(deltaY) > verticalThreshold) {
vertical = deltaY > 0 ? 'Up' : 'Down';
}
return this.composeDirection(horizontal, vertical);
}
private composeDirection(horizontal: 'Left' | 'Right', vertical: 'Up' | 'Down'): PlayerDirection {
if (horizontal === 'Left') {
return vertical === 'Up' ? PlayerDirection.LeftUp : PlayerDirection.LeftDown;
}
return vertical === 'Up' ? PlayerDirection.RightUp : PlayerDirection.RightDown;
}
private isFacingLeft(): boolean {
return this.currentDirection === PlayerDirection.LeftUp || this.currentDirection === PlayerDirection.LeftDown;
}
private isFacingUp(): boolean {
return this.currentDirection === PlayerDirection.LeftUp || this.currentDirection === PlayerDirection.RightUp;
}
private getVerticalAnimationSuffix(): '3' | '5' {
return this.isFacingUp() ? '3' : '5';
}
private getAnimationNameCandidates(requestedName: string): string[] {
const action = this.normalizeAnimationAction(requestedName);
const suffix = this.getVerticalAnimationSuffix();
const oppositeSuffix = suffix === '3' ? '5' : '3';
const candidates: string[] = [];
const pushUnique = (name?: string) => {
if (!name) {
return;
}
if (candidates.indexOf(name) === -1) {
candidates.push(name);
}
};
if (action === 'walk' || action === 'attack') {
if (this.isUpgraded) {
pushUnique(`${action}${suffix}_2`);
pushUnique(`${action}_2`);
}
pushUnique(`${action}${suffix}`);
pushUnique(action);
pushUnique(`${action}${oppositeSuffix}`);
} else if (action === 'stand') {
if (this.isUpgraded) {
pushUnique(`stand${suffix}_2`);
pushUnique('stand_2');
}
pushUnique(`stand${suffix}`);
pushUnique(`stand${oppositeSuffix}`);
pushUnique('stand');
} else {
if (this.isUpgraded) {
pushUnique(`${action}_2`);
}
pushUnique(action);
}
return candidates;
}
/**
* 移动到路径中的下一个路径点
*/
private moveToNextWaypoint() {
if (this.currentAnimation === 'attack' || this.isAttacking) {
return
if (this.isCurrentAction('attack') || this.isAttacking) {
return;
}
if (!this.player || this.currentPath.length === 0 || this.currentPathIndex >= this.currentPath.length) {
@@ -551,20 +652,7 @@ export class PlayerController extends Component {
return;
}
// 梦幻西游方向判断逻辑:
// 1. 优先判断水平方向:左上、左下、左都算左边;右上、右下、右都算右边
// 2. 只有当水平方向不明显时(接近垂直),才判断垂直方向
// 设置一个阈值,当水平方向的绝对值大于这个阈值时,优先判断水平方向
const horizontalThreshold = 0.5; // 可以根据需要调整这个值
if (Math.abs(deltaX) > horizontalThreshold) {
// 水平方向明显,优先判断左右
this.currentDirection = deltaX < 0 ? 3 : 5; // 3表示左5表示右
} else {
// 水平方向不明显,判断垂直方向
this.currentDirection = deltaY < 0 ? 3 : 5; // 3表示上5表示下
}
this.currentDirection = this.resolveDirectionFromDelta(deltaX, deltaY);
// 切换到对应的动画(只传递基础动画名称)
this.switchAnimation('walk');
@@ -657,9 +745,23 @@ export class PlayerController extends Component {
const distance = Vec3.distance(currentLocalPos, targetLocalPos);
const targetPlayerCenterY = targetWorldPos.y + playerOffsetY;
const monsterCenterY = monsterWorldPos.y + monsterOffsetY;
const verticalDeltaToMonster = monsterCenterY - targetPlayerCenterY;
const verticalOverride = Math.abs(verticalDeltaToMonster) <= 0.5 ? undefined : (verticalDeltaToMonster > 0 ? 'Up' : 'Down');
const desiredDirection = this.resolveDirectionFromDelta(
targetWorldPos.x - playerWorldPos.x,
targetWorldPos.y - playerWorldPos.y,
{
horizontal: standOnLeft ? 'Right' : 'Left',
vertical: verticalOverride,
}
);
this.currentDirection = desiredDirection;
if (distance < 1) {
playerNode.setPosition(targetLocalPos);
this.currentDirection = standOnLeft ? 5 : 3;
this.updatePlayerScale();
resolve();
return;
}
@@ -675,7 +777,8 @@ export class PlayerController extends Component {
easing: 'smooth',
onComplete: () => {
playerNode.setPosition(targetLocalPos);
this.currentDirection = standOnLeft ? 5 : 3;
this.currentDirection = desiredDirection;
this.updatePlayerScale();
resolve();
}
})