
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); }
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; 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); } }
|
实现角色移动和翻转
既然每个状态都能获取到玩家的输入,那么我们就把一系列控制移动的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

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

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

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

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

具体实现
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() { rigidBody2D = player.rigidBody2D; animator = player.animator; playerInput = player.playerInput; 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); }
public override void PhysicsUpdate() { } }
|
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); else if(Mathf.Abs(inputXY.x) > 0.5) stateMachine.ChangeState(player.runState); }
public override void PhysicsUpdate() { base.PhysicsUpdate(); } }
|
Run状态

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(); if(inputXY.x<0 && player.facingRight) stateMachine.ChangeState(player.turnState); else if(inputXY.x > 0 && !player.facingRight) stateMachine.ChangeState(player.turnState); if (animaFinTrigger) stateMachine.ChangeState(player.idleState); }
public override void PhysicsUpdate() { player.SetZeroVelocity(); } }
|
RunBreak状态的要点有两个
- 如何让动画播放完毕就转换到Idle状态
- 让玩家在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的最后一帧挂载事件

Turn状态

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();
}
public override void Exit() { base.Exit(); }
public override void LogicUpdate() { base.LogicUpdate(); if (animaFinTrigger) stateMachine.ChangeState(player.runState); }
public override void PhysicsUpdate() { player.SetZeroVelocity(); } }
|
Turn状态主要有四个问题
- Turn的过程中同样需要保持静止,但是设置速度为0,就无法实现翻转,而当角色翻转时,她的方向应该和之前相反,所以,需要在Turn动画的开头第一帧执行一个Flip的Event。
- 根据方向不同播放左右两个转向动画,解决方法和Run状态的解决方法相同
- 如何让动画播放完毕就转换到Run状态,解决方法和RunBreak中解决方法相同
- 让Turn动画播放完后直接切换到Run的动画,跳过ToRun的前摇。解决方法是直接让TrunLeft与RunLeft相联,而不是连到Exit,这样当动画播放完毕,Trun为false,Run为true,动画状态机就直接转到Run的动画了。