PlayerState

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
public class PlayerState
{
protected PlayerStateMachine stateMachine;
protected Player player;

protected Rigidbody2D rigidBody;
protected PlayerInput playerInput;
protected Animator animator;
protected PhysicsCheck physicsCheck;

private string animBoolName;
private string animTriggerName;

protected Vector2 inputXY;

public bool animFinTrigger;


public PlayerState(Player player,PlayerStateMachine stateMachine,string animBoolName, string animTriggerName)
{
this.player = player;
this.stateMachine = stateMachine;
this.animBoolName = animBoolName;
this.animTriggerName = animTriggerName;
}

public virtual void Enter()
{
Debug.Log("Enter"+animBoolName);
rigidBody = player.rigidBody;
animator = player.animator;
playerInput = player.playerInput;
physicsCheck = player.physicsCheck;
animFinTrigger = false;


inputXY = playerInput.GamePlay.Move.ReadValue<Vector2>();
if (!string.IsNullOrEmpty(animBoolName) && HasAnimatorParameter(animator, animBoolName, AnimatorControllerParameterType.Bool))
{
animator.SetBool(animBoolName, true);
}

// 检查 animTriggerName 是否存在
if (!string.IsNullOrEmpty(animTriggerName) && HasAnimatorParameter(animator, animTriggerName, AnimatorControllerParameterType.Trigger))
{
animator.SetTrigger(animTriggerName);
}
}


public virtual void Exit()
{
if (!string.IsNullOrEmpty(animBoolName) && HasAnimatorParameter(animator, animBoolName, AnimatorControllerParameterType.Bool))
{
animator.SetBool(animBoolName, false);
}
Debug.Log("Exit" + animBoolName);
}
public virtual void LogicUpdate()
{
inputXY = playerInput.GamePlay.Move.ReadValue<Vector2>();
animator.SetBool("FacingRight", player.facingRight);


}
public virtual void PhysicsUpdate()
{
player.SetVelocity(inputXY);
}
private bool HasAnimatorParameter(Animator animator, string paramName, AnimatorControllerParameterType type)
{
foreach (var param in animator.parameters)
{
if (param.name == paramName && param.type == type)
return true;
}
return false;
}
}

PlayerStateMachine

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class PlayerStateMachine
{
public PlayerState currentState { get; private set; }

public void Initialize(PlayerState startState)
{
currentState = startState;
currentState.Enter();
}

public void ChangeState(PlayerState newState)
{
currentState.Exit();
currentState = newState;
currentState.Enter();
}
}

Player

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
public class Player : MonoBehaviour
{
[Header("组件")]
public PlayerStateMachine stateMachine { get; private set; } //状态机
public PlayerInputMap inputMap { get; private set; } //输入读取组件

[Header("状态")]
#region
public PlayerIdleState idleState { get; private set; }
public PlayerMoveState moveState { get; private set; }
#endregion

[Header("运动参数")]
public float moveSpeed;

protected override void Awake()
{
base.Awake();

#region 初始化各种状态
stateMachine = new PlayerStateMachine();
idleState = new PlayerIdleState(this,stateMachine,"Idle");
moveState = new PlayerMoveState(this, stateMachine, "Move");
#endregion

inputMap = new PlayerInputMap();
}

protected override void Start()
{

stateMachine.Initialize(idleState);
inputMap.GamePlay.Dash.started += CheckDashInput;
}

private void OnEnable()
{
inputMap.Enable();
}

//关闭输入
private void OnDisable()
{
inputMap.Disable();
}

protected override void Update()
{
if(dashCoolDown>0)
dashCoolDown -= Time.deltaTime;
stateMachine.currentState.LogicUpdate();
}

protected override void FixedUpdate()
{
stateMachine.currentState.PhysicsUpdate();
}
}

获取玩家的输入

我们将获取玩家输入的逻辑写在PlayerState中,这样所有的子状态都能获取到玩家的输入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class PlayerState
{
protected PlayerInput playerInput;
protected Vector2 inputXY;

public PlayerState(Player player, PlayerStateMachine stateMachine, string animBoolName)
{
this.player = player;
this.stateMachine = stateMachine;
this.animBoolName = animBoolName;
}

public virtual void Enter()
{
playerInput = player.playerInput; //组件的创建是在Player的Awake中
inputXY = playerInput.GamePlay.Move.ReadValue<Vector2>();//Enter时获取一下输入,避免PhysicsUpdate()发生在LogicUpdate()之后,导 //丢掉开始的输入
animator.SetBool(animBoolName, true);
}

public virtual void LogicUpdate()
{
inputXY = playerInput.GamePlay.Move.ReadValue<Vector2>(); //接受玩家输入
}

public virtual void PhysicsUpdate()
{
player.SetVelocity(inputXY); //根据输入控制角色,默认是设置左右移动速度
}

public virtual void Exit()
{
animator.SetBool(animBoolName, false);
}
}

实现角色移动和翻转

​ 既然每个状态都能获取到玩家的输入,那么我们就把一系列控制移动的Player方法开放给PlayState,让State根据输入决定如何控制角色。

​ 对于有特殊需求,比如需要接受冲刺,跳跃指令或者速度为0的状态,单独修改就好。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public void SetVelocity(Vector2 inputXY)
{
FlipController(inputXY.x); //每一次改变速度都检查是否要翻转
rigidBody2D.velocity = new Vector2(moveSpeed * inputXY.x * Time.deltaTime, rigidBody2D.velocity.y);
}

public void SetZeroVelocity()
{

rigidBody2D.velocity = new Vector2(0, 0);
}

public void FlipController(float x)
{
if (x > 0.05 && !facingRight) { Debug.Log(x); Flip(); }
else if (x < -0.05 && facingRight) {Debug.Log(x); Flip(); };
}

public void Flip()
{
Debug.Log("Flip");
facingDir *= -1;
facingRight = !facingRight;
transform.Rotate(0, 180, 0);
}

设计人物在地面移动的FSM

image-20250514111356503

我设计的移动逻辑是这样的

  • 当玩家输入>0.1,也就是轻推摇杆时,从Idle进入到Walk,播放走路动画,并且有一个起步的前摇
  • 玩家走路途中松开摇杆就从Walk返回Idle,并且有一个停止的后摇

image-20250514111505238

  • 在Walk状态下如果大幅推摇杆,就从Walk进入到Run,并且Walk的前摇后摇都可以取消,直接进入Run
  • Run的动画有一个起步的前摇,并且根据玩家朝向不同,分别有向左向右奔跑的动画

image-20250514111658729

  • Run的过程中如果松开摇杆,就进入到RunBreak状态,播放一个急停的动画
  • 如果RunBreak动画播放完,就回到Idle状态
  • 如果在RunBreak动画播放过程中往角色面朝方向反方向拉摇杆,就进入Turn状态

image-20250514111957832

  • Turn状态播放人物转向的动画,动画播放完回到Run状态,播放人物朝反方向跑的动画

image-20250514112212387

具体实现

PlayerState基类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public class PlayerState
{
//组件用于改变角色的状态,如速度,旋转,动画等
#region 组件
protected Rigidbody2D rigidBody2D;
protected Animator animator;
protected PlayerInput playerInput; //获取输入
#endregion

protected Player player; //玩家类的引用,用于获取一些参数还有玩家的其他状态
protected PlayerStateMachine stateMachine; //状态机
public bool animaFinTrigger; //一个触发器,用来指示当前状态某些动画已经播放完毕
private string animBoolName; //用来触发与状态同名的动画状态机参数
protected Vector2 inputXY; //存储玩家输入

public PlayerState(Player player, PlayerStateMachine stateMachine, string animBoolName)
{
this.player = player;
this.stateMachine = stateMachine;
this.animBoolName = animBoolName;
}

public virtual void Enter()
{
//组件在Player类中初始化,这里获取引用,可以不用多写player.
rigidBody2D = player.rigidBody2D;
animator = player.animator;
playerInput = player.playerInput;
//进入状态时默认触发器为false
animaFinTrigger = false;
Debug.Log("Enter: "+animBoolName);
inputXY = playerInput.GamePlay.Move.ReadValue<Vector2>(); //由于物理相关的更新和逻辑更新分开,有可能刚进入状态时,逻辑更新在物理更新
//之后,导致丢失玩家输入,因此在刚进入状态时获取一下输入
animator.SetBool(animBoolName, true);
}

public virtual void LogicUpdate()
{
inputXY = playerInput.GamePlay.Move.ReadValue<Vector2>(); //读取输入
}

public virtual void PhysicsUpdate()
{
player.SetVelocity(inputXY); //默认根据输入执行的操作是左右移动和翻转
}

public virtual void Exit()
{
animator.SetBool(animBoolName, false);
Debug.Log("Exit: " + animBoolName);
}
}

Idle状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class PlayerIdleState : PlayerState
{
public PlayerIdleState(Player player, PlayerStateMachine stateMachine, string animBoolName) : base(player, stateMachine, animBoolName)
{
}

public override void Enter()
{
base.Enter();
}

public override void Exit()
{
base.Exit();
}

public override void LogicUpdate()
{
base.LogicUpdate();
if (Mathf.Abs(inputXY.x) > 0.1) stateMachine.ChangeState(player.walkState); //转入Walk

}

public override void PhysicsUpdate() //分开写的话,PhysicsUpdate获取的输出会滞后
{
//我希望Idle状态下不对输入做出反应,因此重写为不执行操作
}
}

Walk状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class PlayerWalkState : PlayerState
{
public PlayerWalkState(Player player, PlayerStateMachine stateMachine, string animBoolName) : base(player, stateMachine, animBoolName)
{
}

public override void Enter()
{
base.Enter();
}

public override void Exit()
{
base.Exit();
}

public override void LogicUpdate()
{
base.LogicUpdate();
if(Mathf.Abs(inputXY.x)<0.1)stateMachine.ChangeState(player.idleState); //回到Idle
else if(Mathf.Abs(inputXY.x) > 0.5) stateMachine.ChangeState(player.runState); //进入Run
}

public override void PhysicsUpdate()
{
base.PhysicsUpdate();
}
}

Run状态

image-20250514115205827

Run状态比起码两个状态稍微复杂一些,主要是有前摇和左右两个跑动动画。前摇用一个ExitTime为1的动画即可,而左右跑动的切换,用一个空节点结合玩家的方向解决,为此需要在PlayerState中增加Animator中FacingRight参数的更新。

1
2
3
4
5
public virtual void LogicUpdate()
{
inputXY = playerInput.GamePlay.Move.ReadValue<Vector2>(); //读取输入
animator.SetBool("FacingRight", player.facingRight); //
}

RunBreak状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class PlayerRunBreakState : PlayerState
{
public PlayerRunBreakState(Player player, PlayerStateMachine stateMachine, string animBoolName) : base(player, stateMachine, animBoolName)
{
}

public override void Enter()
{
base.Enter();
}

public override void Exit()
{
base.Exit();
}

public override void LogicUpdate()
{
base.LogicUpdate();
//动画播放过程中转向就进入Turn
if(inputXY.x<0 && player.facingRight) stateMachine.ChangeState(player.turnState);
else if(inputXY.x > 0 && !player.facingRight) stateMachine.ChangeState(player.turnState);
//如果动画播放完毕就回到Idle
if (animaFinTrigger) stateMachine.ChangeState(player.idleState);
}

public override void PhysicsUpdate()
{
player.SetZeroVelocity(); //RunBreak中设置速度为0
}
}

RunBreak状态的要点有两个

  1. 如何让动画播放完毕就转换到Idle状态
  2. 让玩家在RunBreak状态中保持静止

第二点只需要重写PhysicsUpdate方法就好。第三点可以用动画事件解决,新建一个类用于提供各种动画触发事件。注意这个类要和Animator组件挂载在同一个GameObject上,否则Animation添加事件时找不到这个类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class PlayerAnimationEvents : MonoBehaviour
{
private Player player;

private void Awake()
{
player = GetComponentInParent<Player>();
}

public void AnimationFlip()
{
player.Flip();
}

public void AnimationTrigger()
{
player.stateMachine.currentState.animaFinTrigger = true;
}
}

为RunBreak的最后一帧挂载事件

image-20250514121119768

Turn状态

image-20250514121912760

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class PlayerTurnState : PlayerState
{
public PlayerTurnState(Player player, PlayerStateMachine stateMachine, string animBoolName) : base(player, stateMachine, animBoolName)
{
}

public override void Enter()
{
base.Enter();
//player.Flip();

}

public override void Exit()
{
base.Exit();
//player.Flip();
}

public override void LogicUpdate()
{
base.LogicUpdate();
if (animaFinTrigger) stateMachine.ChangeState(player.runState);
}

public override void PhysicsUpdate()
{
player.SetZeroVelocity();
}
}

Turn状态主要有四个问题

  1. Turn的过程中同样需要保持静止,但是设置速度为0,就无法实现翻转,而当角色翻转时,她的方向应该和之前相反,所以,需要在Turn动画的开头第一帧执行一个Flip的Event
  2. 根据方向不同播放左右两个转向动画,解决方法和Run状态的解决方法相同
  3. 如何让动画播放完毕就转换到Run状态,解决方法和RunBreak中解决方法相同
  4. 让Turn动画播放完后直接切换到Run的动画,跳过ToRun的前摇。解决方法是直接让TrunLeft与RunLeft相联,而不是连到Exit,这样当动画播放完毕,Trun为false,Run为true,动画状态机就直接转到Run的动画了。