什么是状态和状态机
想象一下你正在编写一个玩家控制脚本:人物在默认状态下是站立在地面上的,当你按下方向键时,人物会移动,按下空格键时,人物会先跳起进入半空中,之后受重力影响又回到地面。
可以用类似流程图的方式表示上述过程,图中的方框表示人物拥有的各种状态,箭头表示状态之间的切换以及对应的条件。在任何时候,人物只会处于一种状态中,当满足预定的条件时会触发状态的切换。
在游戏开发中,可以利用有限状态机来管理游戏中的物体或角色的内部状态,Unity中的Animator其实就是有限状态机的体现。
经典的FSM通常使用switch语句实现,而状态设计模式定义了一个状态接口和一个实现该接口的状态基类,更加面向对象。
经典的状态机实现
要实现一个基本的有限状态机,可以使用一种简单的方式,就是利用枚举和switch
语句。
首先需要定义一个枚举类,它包含角色的各个状态。
CSharp
public enum PlayerControllerState {
Idle, Move, Jump
}
然后在MonoBehaviour
的Update
函数中使用switch
语句,执行不同状态下对应的逻辑。
CSharp
public class PlayerController : MonoBehaviour
{
private PlayerControllerState currentState;
private void Update()
{
CheckInput();
switch (currentState)
{
case PlayerControllerState.Idle:
Idle();
break;
case PlayerControllerState.Move:
Walk();
break;
case PlayerControllerState.Jump:
Jump();
break;
}
}
public void CheckInput() { ... }
public void Idle() { ... }
public void Walk() { ... }
public void Jump() { ... }
}
虽然这种方式的实现可以正常工作,但随着角色状态越来越多,PlayerController
这个脚本很快就会变得混乱不堪,代码难以理解和维护。所以对于比较复杂的项目来说,使用switch语句并不是一种最佳做法。
这时候状态模式就派上用场了,它可以让你的代码更加模块化,易扩展,使添加新状态和管理状态切换更加简单。
使用状态模式优化
状态模式主要解决两个问题:
- 一个对象应该在它的内部状态改变时,改变它的行为。
- 特定状态的行为应该被独立地定义,添加新的状态不应该影响现有状态的行为。
在前面的实现中,PlayerController
可以切换自己的状态来改变行为,但还不太满足第二点需求。
为了尽可能减小添加新状态对已有状态的影响,可以把状态封装成一个个对象。
IState接口
首先需要创建一个接口IState
,每一个具体的状态都要实现这个接口。接口中包含三个方法:Enter
,Update
和Exit
,分别对应进入状态时、处于状态中、退出状态时需要执行的方法。
CSharp
public interface IState
{
// 进入状态时执行的逻辑
public void Enter();
// 处于状态时每帧执行的逻辑,包括处理状态的切换
public void Update();
// 退出状态时执行的逻辑
public void Exit();
}
StateMachine类
StateMachine
类为了管理各个状态以及处理状态之间的切换,会持有各个状态的引用。另外,每个状态都需要用到PlayerController
脚本,所以要将PlayerController
对象传入构造函数。
CSharp
[Serializable]
public class StateMachine
{
public IState CurrentState { get; private set; }
// 状态对象的引用
public WalkState walkState;
public JumpState jumpState;
public IdleState idleState;
// 构造函数
public StateMachine(PlayerController player)
{
// 实例化各个状态
this.walkState = new WalkState(player);
this.jumpState = new JumpState(player);
this.idleState = new IdleState(player);
}
// 初始化状态机,设置初始状态
public void Initialize(IState state)
{
CurrentState = state;
state.Enter();
}
// 切换状态
public void TransitionTo(IState nextState)
{
CurrentState.Exit();
CurrentState = nextState;
nextState.Enter();
}
// 状态更新
public void Update()
{
if (CurrentState != null)
{
CurrentState.Update();
}
}
}
状态类
以IdleState
为例,在状态更新函数中,检查是否满足切换到移动状态或跳跃状态的条件,通过PlayerController
引用获取到状态机进行状态切换。
CSharp
public class IdleState : IState {
private PlayerController player;
public IdleState(PlayerController player)
{
this.player = player;
}
public void Enter()
{
//Debug.Log("Entering Idle State");
}
public void Update()
{
if (!player.IsGrounded)
{
player.PlayerStateMachine.
TransitionTo(player.PlayerStateMachine.jumpState);
}
if (Mathf.Abs(player.CharController.velocity.x) > 0.1f
|| Mathf.Abs(player.CharController.velocity.z) > 0.1f)
{
player.PlayerStateMachine
.TransitionTo(player.PlayerStateMachine.walkState);
}
}
public void Exit()
{
//Debug.Log("Exiting Idle State");
}
}
参考文章:How to Develop a Modular & Flexible Codebase With the State Programming Pattern | Unity