初学有限状态机

想象一下


想象一个场景,你有一个ARPG游戏里的角色,你的角色有很多骚气的动作:蹬墙跳、滑铲、二段跳、滑翔、过肩摔、格挡反击、咸鱼冲刺、吹口哨……

但是,你的角色并不能在滑铲的时候使出咸鱼冲刺,在滑翔的时候对着敌人使出过肩摔,在挨打的时候对着怪吹口哨,否则其他看到的玩家就会直呼:卧槽,有挂

如果你是Unity菜鸟,你会怎样设计角色控制代码呢,正好我就是菜鸟,我会告诉你应该这样做:声明一溜布尔值来判断角色所处的状态

bool isGround;// 在地面上
bool isHuaXiang;// 正在滑翔
bool isHurt;// 正在挨打
……

当按下动作按键时用if来判断

if(!isHurt)
	ChuiKouShao();// 如果角色没有在挨打,就对着怪吹口哨嘲讽

当然,学过有限状态机的你对着我的嘴就是一巴掌:如果角色的状态和行为不断变得复杂,慢慢的,你会创建114514个条件变量,慢慢的,if层级会越来越多。。然后你交给了我一个新的方法——有限状态机:

有限状态机


FSM,有限状态机,可以枚举出有限多个状态,当满足特定的条件时可以在这些条件中来回切换

有限状态机的核心思想:

  • 拥有有限个的多种状态
  • 当前处于其中一个状态
  • 状态之间可以互相切换

比如游戏中的敌人AI,正常情况下敌人会在特定的路线上来回走动进行巡逻,当玩家发出动静或者首次进入视野时会警觉,这时候玩家再次发出动静或者暴露在视野中敌人就会追击玩家,直到玩家消失在视野中

Unity当中的Animator就是一个FSM:

只不过每个状态里存放的是动画,我们的FSM也会沿用这个思想,只不过状态里存放的是逻辑代码

FSM可以说是一个强化版的 switch case ,判断处于哪个状态,执行对应的逻辑,FSM可以很方便地进行扩展,加入新状态只需继承基类,不用修改原来的代码

一个最简单的有限状态机


FSM思想一:拥有有限个的多种状态

我们首先思考一下敌人有哪些状态,这里我只使用了简单的两种状态:

  • 巡逻
  • 追赶

使用枚举 enum 类型来存储所有的状态便于使用:

    public enum StateType
    {
        Patrol,// 巡逻状态
        Chase// 追赶状态
    }

FSM思想二:当前处于其中一个状态

接下来需要知道敌人当前正处于哪种状态,使用一个 StateType 类型的变量来存储

private StateType currentState;

FSM思想三:状态之间可以互相切换

在Update中,做状态之间切换的判断,当前正处于哪种状态,就执行对应状态的响应函数

        private void Update()
        {
            switch (currentState)
            {
                case StateType.Patrol:
                    OnPatrol();// 巡逻状态的响应函数
                    break;
                case StateType.Chase:
                    OnChase();// 追击状态的响应函数
                    break;
                default:
                    break;
            }
        }

接下来只要补充两个响应函数中的代码逻辑:

private void OnPatrol()
        {
            if (Vector2.Distance(transform.position, targetPos) < 0.1f)
            {
                targetPos = (targetPos == patrolPos1) ? patrolPos2 : patrolPos1;
            }

            transform.position = Vector2.MoveTowards(transform.position, targetPos, speed * Time.deltaTime);

            if (Vector2.Distance(player.position, transform.position) < dangerDistance)
            {
                transform.GetComponentInChildren<Text>().text = "Enemy(Chasing)";
                enemyMaterial.color = Color.red;
                currentState = StateType.Chase;
            }
        }
private void OnChase()
        {
            transform.position = Vector2.MoveTowards(transform.position, player.position, speed * Time.deltaTime);

            if (Vector2.Distance(transform.position, player.position) > dangerDistance)
            {
                transform.GetComponentInChildren<Text>().text = "Enemy(patrolling)";
                enemyMaterial.color = Color.green;
                currentState = StateType.Patrol;
            }
        }

更复杂点的有限状态机


简单版本的状态机的代码耦合性太大,不便于更改,这时候需要加入一个中间类 状态机管理类 来控制、调用所有的状态;同时为了便于管理,所有的状态都继承自同一个 状态基类 ,这里也可以用接口来实现同样的效果,总体来说就是下面的框架:

  • 每个独立的敌人对象都有一个自己的状态机管理器,这个管理器中存放着敌人的所有状态,通过这个管理器来实现自己当前状态的切换和运行

状态枚举

便于规范和使用,用一个枚举来存放游戏中的所有状态

    public enum StateEnum
    {
        Patrol,
        Chase
    }

状态机管理器类

用字典来存放单个敌人的状态名字和实例,并创建 AddState 函数便于在单个敌人类中对自己拥有状态进行管理

        public Dictionary<StateEnum, BaseState> stateDic;
        public GameObject aIObject; // 当前状态机管理器的拥有者
        public BaseState currentState; // 当前所处于的状态

        /// <summary>
        /// 向字典中添加State的方法
        /// </summary>
        /// <param name="stateEnum">State名</param>
        public void AddState(StateEnum state)
        {
            switch (state)
            {
                case StateEnum.Patrol:
                    stateDic.Add(state, new PatrolState(this));
                    break;
                case StateEnum.Chase:
                    stateDic.Add(state, new ChaseState(this));
                    break;
                default: break;
            }
        }

状态的基类

抽象类,只能被继承。所有状态都具有的共同行为

OnEnter、OnUpdate、OnExit

单个状态类

每个状态单独一个类,存放这个状态拥有的逻辑代码,继承于状态基类

角色对象类

创建StateMachineManager类的实例,每个角色都有自己专属的状态机,因此StateMachineManager类不能是抽象类

posted @ 2024-05-29 15:13  Caromeow  阅读(32)  评论(0)    收藏  举报