3.1 有限状态机
请简述有限状态机是什么,并尝试实现二段技能(超时会影响技能衔接)的状态机。
问题分析
有限状态机的英文缩写为FSM(Finite State Machine),也可称为状态机,是表示有限个状态以及在这些状态之间的转移和动作等行为的模型。所谓有限,指的是状态有限,所有的状态可枚举出来。它在游戏中运用非常广泛,最常见的就是用于控制角色的状态。
在动手编写FSM之前,首先要明确以下两个基本概念。
◎ 状态(State):每一个状态都是稳定的,如果不满足转移条件,则状态机会一直处于状态中。
◎ 转换(Transition):状态之间通过转换连接,每个转换中可以包含不同的条件。
接下来就可以开始动手制作状态机了。状态机的设计是一个逻辑梳理的过程,建议拿出纸笔画图制作。先将人物所有的状态列出来,然后再用转换连接它们,一个角色的状态机可能如图3.1所示。
图3.1
在图3.1中,角色正常会处于等待状态。当按下键盘中的B键时,角色进入躲闪状态,超时后返回。当按下键盘上的A键时,释放技能A,如果在未超时的状态下再次按下A键,则触发二段技能B。在技能超时后,自动回到等待。
代码实现
下面通过代码实现图3.1的核心功能。创建FSMTest.cs脚本,在脚本中编写如下代码:
public enum TRAN_INPUT { BUTTON_A, BUTTON_B, TIME_OUT, } public interface State { State HandleInput(TRAN_INPUT input); void EnterState(); }
其中,TRAN_INPUT为转换的条件类型,HandleInput是转换判断函数,EnterState是进入State时需要触发的处理。
接下来就分别实现状态。每个状态都是一个实现State接口的子类。对照状态图,添加如下代码:
//站立状态 public class IdleState: State { public State HandleInput(TRAN_INPUT input) { if(input==TRAN_INPUT.BUTTON_A) { return new SkillA(); } else if (input==TRAN_INPUT.BUTTON_B) { return new DodgeState(); } return null; } public void EnterState() { Debug.Log("To Idle State"); } } //翻滚状态 public class DodgeState: State { public State HandleInput(TRAN_INPUT input) { if(input==TRAN_INPUT.TIME_OUT) { return new IdleState(); } return null; } public void EnterState() { Debug.Log("To Dodge State"); } } //技能B public class SkillA: State { public State HandleInput(TRAN_INPUT input) { if(input==TRAN_INPUT.BUTTON_A) { return new SkillB(); } else if(input==TRAN_INPUT.TIME_OUT) { return new IdleState(); } return null; } public void EnterState() { Debug.Log("To SkillA State"); } } //技能B public class SkillB: State { public State HandleInput(TRAN_INPUT input) { if(input==TRAN_INPUT.TIME_OUT) { return new IdleState(); } return null; } public void EnterState() { Debug.Log("To SkillB State"); } }
实现了状态转换后,再编写一个FSM对外的接口类,将复杂的状态对外隐藏起来,添加如下代码:
public class FSM { State CurrentState; public FSM (){ CurrentState=new IdleState(); CurrentState.EnterState(); } public void HandleInput(TRAN_INPUT input) { State newState=CurrentState.HandleInput(input); if(newState !=null) { CurrentState=newState; CurrentState.EnterState(); } } }
通过这样的封装,外部只需调用HandleInput,并将输入信号量传入即可,无须关心具体的跳转逻辑。
最后,我们还要编写测试类,新建FSMTest.cs文件,添加如下代码:
public class FSMTest : MonoBehaviour { public FSM fsm ; void Start() { fsm=new FSM(); } void Update() { if(Input.GetKeyUp(KeyCode.J)){ fsm.HandleInput(TRAN_INPUT.BUTTON_A); StopAllCoroutines(); StartCoroutine(autoTimeOut(2)); } else if(Input.GetKeyUp(KeyCode.Space)){ fsm.HandleInput(TRAN_INPUT.BUTTON_B); StopAllCoroutines(); StartCoroutine(autoTimeOut(1)); } } IEnumerator autoTimeOut(float sec){ yield return new WaitForSeconds(sec); fsm.HandleInput(TRAN_INPUT.TIME_OUT); } }
在这个类中,通过Input.GetKeyUp获取键盘的输入,控制状态机的转换。另外,笔者通过协程实现了一个简易的超时判断,在实际项目中,超时判断通常会更加复杂。
将脚本挂载到场景的GameObject中,运行游戏可以看到状态进入Idle的输出,按下对应按键即可看到状态转换的输出,如图3.2所示。
图3.2
Animator
在Unity 3D中,对于动画控制可以使用Animator。在这种编辑模式下,我们可以在图形化界面中灵活地更改状态机的连接方式,控制转换条件。Animator还支持子状态机等整理形式,可以简化状态图的复杂程度。另外,在游戏运行的过程中,也可以看到Animator的运行变化,是强大的图形化工具。例如,角色的击飞、击退、击倒,以及空中连击的动画状态图等,可以按照图3.3的方式连接。
图3.3
总结
本篇通过代码实现了一个简单的技能状态机,讲解了FSM的常见用法。另外,还简单介绍了其在Animator中的应用。由于Animator的使用比较简单,因此很容易找到讲解资料,另外去看官网文档也是个不错的选择([:animatordoc]),这里就不过多介绍了。
扩展问题
既然有了Animator,还需要实现逻辑的状态机吗([:logicfsm])?