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])?