创建状态类
状态基类选择
共有两个类使用了IState
接口,另外其中一个类还衍生出了一个专门供角色使用的子类
State
:非常纯粹的使用IState
,无其他任何操作。适用于一些简单的FSM。
StateBehaviour
:除了使用Istate
外,还继承了MonoBehaviour
。适合需要在Inspector上序列化的FSM。
CharacterState
:继承自StateBehaviour
,因为角色的状态大多数都无法打断自己(无法从Idle再次进入Idle),该类针对这个问题做了特殊处理
如果需要创建角色FSM的话,毫无疑问的就需要继承CharacterState
了
代码示例
状态类里的逻辑越简单越好,只需要做状态类的逻辑判断,不要做状态转换
使用方法
1 2 3 4 5 6 7
| public class MoveState : CharacterState { private void OnEnable() { Debug.Log("播放Move动画"); } }
|
最好不要做切换逻辑
状态类只用处理自己状态内的逻辑,不要做转换逻辑(转换逻辑应该在Brain中判断)。除非是特殊的事件状态(如攻击、死亡)结束后可以使用TrySetDefaultState
返回默认状态。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public class IdleState : CharacterState { private void OnEnable() { Debug.Log("播放Idle动画"); }
private void Update() { } }
|
特殊事件
优先级与强制转换使用场景
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| public class AttackState : CharacterState { [SerializeField] private PlayerBrain1 _brain; private int _animationTime = 1000; private void OnEnable() { Debug.Log("播放Attack动画"); AnimationDelay(); }
public override CharacterStatePriority Priority => CharacterStatePriority.Medium;
private async void AnimationDelay() { await Task.Delay(_animationTime); _brain.StateMachine.ForceSetDefaultState(); } }
|
创建Brain类
用来控制Player在应该进入什么状态
StateMachine基类选择
有两个可供选择,分别对应着后缀为1和2的文件
StateMachine1
:在进入状态时需传入状态类这个对象实例。
StateMachine2
:在进入状态时只用输入对应的枚举。对序列化友好,但是需要额外的精力去维护。
对于Player状态机这两个都可以
StateMachine1示例
注意事项:在Update中转换时,必须要使用if等逻辑判断处理好进入状态的顺序,不要出现成功进入A状态后继续转换,导致进入了B状态。
错误示范:
1 2 3 4 5 6 7
| private void Update() { if (在空中) FSM.TrySetState(State.Air); FSM.TrySetState(State.Idle); }
|
正确示范:
1 2 3 4 5 6 7
| private void Update() { if (在空中) FSM.TrySetState(State.Air); else FSM.TrySetState(State.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 29 30 31 32 33 34 35 36 37 38 39 40 41
| public sealed class PlayerBrain1 : MonoBehaviour { public StateMachine<CharacterState>.WithDefault StateMachine = new();
[SerializeField] private CharacterState _idleState; [SerializeField] private CharacterState _moveState; [SerializeField] private CharacterState _attackState;
private void Start() { StateMachine.InitializeAfterDeserialize(_idleState); }
private void Update() { UpdateMovement(); UpdateAttackAction(); }
private void UpdateMovement() { if (Input.GetAxis("Horizontal") != 0) { StateMachine.TrySetState(_moveState); } else { StateMachine.TrySetState(_idleState); } }
private void UpdateAttackAction() { if (!Input.GetMouseButtonDown(0)) return; StateMachine.TrySetState(_attackState); } }
|
StateMachine2示例
状态机的使用方法上面已经说的很清楚了,这里就只说一下使用枚举与类实例的区别
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
| public sealed class PlayerBrain2 : MonoBehaviour { private enum State { Idle, Move }
[SerializeField] private StateMachine<State, CharacterState>.WithDefault _stateMachine = new();
[SerializeField] private CharacterState _idleState; [SerializeField] private CharacterState _moveState;
private void Awake() { _stateMachine.AddRange( new [] { State.Idle , State.Move }, new [] { _idleState, _moveState}); }
private void Start() { _stateMachine.InitializeAfterDeserialize(State.Idle); }
private void Update() { UpdateMovement(); }
private void UpdateMovement() { if (Input.GetAxis("Horizontal") > .1f) _stateMachine.TrySetState(State.Move); else _stateMachine.TrySetState(State.Idle); } }
|