Unity FSM有限状态机

关于人物或敌人的多种状态之间的切换,我们平常的做法就是定义个枚举,把所需的状态放在枚举里面,我们可能要在项目很多地方调用switch方法或者是设置人物状态。当状态比较多的时候,对状态的管理和获取也会变得很麻烦。所以这个时候,我们需要一个有限状态机来统一管理我们的状态(state)。

有限状态机包括两个部分:(1)状态集 FSMState,(2)状态管理机 FSMSystem.

一. 状态集FSMState代表一组状态的集合,一个游戏中可能有多个状态集,所以我们再设计FSMState时最好将它设计为抽象类,这样就能通过继承FSMState拥有一些公共的属性和方法,同时又有自己特定的状态。

二. 一个状态管理机主要的功能有这两点:

1.能够添加和删除状态集(FSMState).

2.能够切换和获取 某个状态集(FSMState)当前的状态(state).

我们还是用个小例子来解析一下:场景有两个物体,一个NPC,一个Player,NPC平时就是巡逻的状态,当Player距离NPC多近时NPC开始追逐Player,超过多远之后就继续巡逻。

下面上脚本:

状态机脚本

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 为过度加入枚举标签,对应相应的状态
/// </summary>
public enum Transition
{
    NullTransition=0,              //用这个过渡来代表你的系统中不存在的状态
    SawPlayer,                     //这里是NPC的两个过渡
    LostPlayer

}
/// <summary>
/// 为状态加入枚举标签
/// </summary>
public enum StateID
{
    NullStateID=0,
    ChasingPlayer,              //为配合NPC添加两个状态      
    FollowingPath               
}

/// <summary>
/// 状态类
/// </summary>
public abstract class FSMState       
{
    protected Dictionary<Transition, StateID> map = new Dictionary<Transition, StateID>();

    /// <summary>
    /// 为每一个子类初始化当前的状态
    /// </summary>
    protected StateID stateID;
    public StateID ID
    {
        get { return stateID; }
    }
    /// <summary>
    /// 添加过渡
    /// </summary>
    /// <param name="trans"></param>
    /// <param name="id"></param>
    public void AddTransition(Transition trans,StateID id)  
    {
        //验证每个参数是否合法
        if(trans==Transition.NullTransition)
        {
            Debug.LogError("FSMState error:NullTransition is not allowed for a real transition");
            return;
        }
        if(id==StateID.NullStateID)
        {
            Debug.LogError("FSMState error:NullStateID is not allowed for a real stateID");
            return;
        }
        //要知道这是一个确定的有限状态机(每个过渡对应一种状态,不能产生分支)
        //检查当前的过渡是否已经在字典当中了
        if(map.ContainsKey(trans))
        {
            Debug.LogError("FSMState error:There already has transition " + trans.ToString() + "check for another");
            return;
        }
        map.Add(trans, id);
    }
    /// <summary>
    /// 这个方法在状态地图中删除transition-stateid对
    /// 如果过渡不存在地图中将打印一个错误
    /// </summary>
    /// <param name="trans"></param>
    public void DeleteTransition(Transition trans)
    {
        //check for NullTransition
        if(trans==Transition.NullTransition)
        {
            Debug.LogError("FSMState error:NullTransition is not allowed");
            return;
        }
        //删除之前确认键值对是否存在于状态地图中
        if(map.ContainsKey(trans))
        {
            map.Remove(trans);
            return;
        }
        Debug.LogError("FSMState error:Transition " + trans.ToString() + "passed to " + stateID.ToString() + "was not exist");
    }
    /// <summary>
    /// 该方法在该状态接收到一个过渡时返回状态机需要成为的新状态
    /// </summary>
    /// <param name="trans"></param>
    /// <returns></returns>
    public StateID GetOutputState(Transition trans)
    {
        //检查一下map状态字典中是否存在这个过渡
        if(map.ContainsKey(trans))
        {
            return map[trans];
        }
        return StateID.NullStateID;
    }
	/// <summary>
    /// 这个方法用来设立进入状态前的条件
    /// 在状态机分配它到当前状态之前他会被自动调用
    /// </summary>
	public virtual void DoBeforeEntering() { }
    /// <summary>
    /// 这个方法用来让一切都是必要的,例如在有限状态机变化到另一个时重置变量
    /// 在状态机切换到新的状态之前它会被自动调用
    /// </summary>
    public virtual void DoBeforeLeaving() { }
    /// <summary>
    /// 要切换状态的条件,原因
    /// NPC是被该类约束下对象的一个引用
    /// </summary>
    /// <param name="player"></param>
    /// <param name="npc"></param>
    public abstract void Reason(GameObject player, GameObject npc);
    /// <summary>
    /// 切换状态后对应的行动
    /// 表现->该方法用来控制NPC在游戏世界中的行为
    /// NPC的任何动作,移动或者交流都需要放置在这 ,
    /// NPC是被该类约束下对象的一个引用
    /// </summary>
    /// <param name="player"></param>
    /// <param name="npc"></param>
    public abstract void Act(GameObject player, GameObject npc);
}
/// <summary>
/// 该类便是有限状态机类
/// 它持有NPC的状态集合,并且有添加,删除状态的方法,以及改变当前正在执行的状态
/// </summary>
public class FSMSystem
{
    private List<FSMState> states;
    //通过预装一个过渡的唯一方式来改变状态机的状态
    //不要直接改变当前的状态

    /// <summary>
    /// 当前的状态ID
    /// </summary>
    private StateID currentStateID;
    /// <summary>
    /// 当前的状态类信息
    /// </summary>
    private FSMState currentFSMState;

    public StateID CurrentStateID
    {
        get
        {
            return currentStateID;
        }

    }

    public FSMState CurrentFSMState
    {
        get
        {
            return currentFSMState;
        }

    }

    public FSMSystem()
    {
        states = new List<FSMState>();
    }
    /// <summary>
    /// 这个方法为有限状态机置入新的状态
    /// 或者在改状态已经存在列表时打印错误信息
    /// 第一个添加的状态也是最初的状态
    /// </summary>
    public void AddState(FSMState s)
    {
        //添加前检查是否为空
        if(s==null)
        {
            Debug.LogError("FSM error:Null reference is not allowed");
        }

        //被装在的第一个状态也是初始状态
        if(states.Count==0)
        {
            states.Add(s);
            currentFSMState = s;
            currentStateID = s.ID;
            return;
        }

        //如果该状态未被添加过,则加入集合
        foreach(FSMState state in states)
        {
            if(state.ID==s.ID)
            {
                Debug.LogError("FSM error: There already exist " + s.ID.ToString());
                return;
            }

        }
        states.Add(s);
    }
    /// <summary>
    /// 删除一个已存在状态机中的状态
    /// 在它不存在时打印错误信息
    /// </summary>
    /// <param name="id"></param>
    public void DeleteState(StateID id)
    {
        //在删除前检测其是否为空
        if(id==StateID.NullStateID)
        {
            Debug.LogError("FSM error:NullStateID is not allowed for a real state");
            return;
        }
        foreach(FSMState state in states)
        {
            if(state.ID==id)
            {
                states.Remove(state);
                return;
            }
        }
        Debug.LogError("FSM error:Can't delete state " + id.ToString() + ",it was not in list");
    }
    /// <summary>
    /// 该方法基于当前状态和过渡状态是否通过来尝试改变状态机的状态,当当前状态没有目标状态用来过渡时则打印错误信息
    /// 切换当前状态到下一个要转换的状态
    /// </summary>
    /// <param name="trans"></param>
    public void PerformTransition(Transition trans)
    {
        //在改变当前状态前检测NullTransition
        if(trans==Transition.NullTransition)
        {
            Debug.LogError("FSM error:NullTransition is not allowed");
            return;
        }

        //在改变状态前检测当前状态是否可作为过渡的参数
        StateID id = currentFSMState.GetOutputState(trans);
        if(id==StateID.NullStateID)
        {
            Debug.LogError("FSM error: State " + currentFSMState.ID.ToString() + "does not allowed");
            return;
        }

        //更新当前的状态机和状态编号
        currentStateID = id;
        foreach(FSMState state in states)
        {
            if(state.ID==currentStateID)
            {
                //在状态变为新状态前执行后处理
                currentFSMState.DoBeforeLeaving();

                currentFSMState = state;

                //在状态可以使用Reason(动机)或者Act(行为)之前为它的决定条件重置它自己
                currentFSMState.DoBeforeEntering();
                break;
            }
        }
        
    }
}


扫描二维码关注公众号,回复: 17599468 查看本文章

NPC逻辑与状态集继承脚本:

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(Rigidbody))]
public class NPCController : MonoBehaviour
{
    public GameObject player;
    public Transform[] path;
    private FSMSystem fsm;

    public void SetTransition(Transition t)
    {
        //该方法用来改变有限状态机的状态,有限状态机基于当前的状态和通过的过渡状态
        //如果当前的状态没有用来通过的过度状态,则会抛出错误
        fsm.PerformTransition(t);
    }

    private void Start()
    {
        MakeFSM();
    }

    private void FixedUpdate()
    {
        fsm.CurrentFSMState.Reason(player, gameObject);
        fsm.CurrentFSMState.Act(player, gameObject);
    }
    private void MakeFSM()         //建造状态机
    {
        FollowPathState follow = new FollowPathState(path);
        follow.AddTransition(Transition.SawPlayer, StateID.ChasingPlayer);

        ChassPlayerState chass = new ChassPlayerState();
        chass.AddTransition(Transition.LostPlayer, StateID.FollowingPath);

        fsm = new FSMSystem();
        fsm.AddState(follow);
        fsm.AddState(chass);

    }
}

public class FollowPathState : FSMState
{
    private int currentWayPoint;
    private Transform[] waypoints;

    public FollowPathState(Transform[] wp)  
    {
        waypoints = wp;
        currentWayPoint = 0;
        stateID = StateID.FollowingPath;
    }

    public override void DoBeforeEntering()
    {
        Debug.Log("Followingpath before entering");
    }
    public override void DoBeforeLeaving()
    {
        Debug.Log("Followingpath before leaving");
    }
    //要切换状态的条件
    public override void Reason(GameObject player, GameObject npc)
    {
        RaycastHit hit;
        if (Physics.Raycast(npc.transform.position, npc.transform.forward, out hit, 15f))
        {
            if (hit.transform.gameObject.tag == "Player")
            {
                npc.GetComponent<NPCController>().SetTransition(Transition.SawPlayer);
            }
        }
    }
    //切换状态后的行动
    public override void Act(GameObject player, GameObject npc)
    {
        Vector3 vel = npc.GetComponent<Rigidbody>().velocity;
        Vector3 moveDir = waypoints[currentWayPoint].position - npc.transform.position;

        if(moveDir.magnitude<1)
        {
            currentWayPoint++;
            if(currentWayPoint>=waypoints.Length)
            {
                currentWayPoint = 0;
            }
        }
        else
        {
            vel = moveDir.normalized * 10;
            npc.transform.rotation = Quaternion.Slerp(npc.transform.rotation, Quaternion.LookRotation(moveDir), 5 * Time.deltaTime);
            npc.transform.eulerAngles = new Vector3(0, npc.transform.eulerAngles.y, 0);
        }
        npc.GetComponent<Rigidbody>().velocity = vel;
    }

}

public class ChassPlayerState : FSMState
{
    public ChassPlayerState()
    {
        stateID = StateID.ChasingPlayer;
    }

    public override void DoBeforeEntering()
    {
        Debug.Log("Followingpath before entering");
    }
    public override void DoBeforeLeaving()
    {
        Debug.Log("Followingpath before leaving");
    }

    //要切换状态的原因,条件
    public override void Reason(GameObject player, GameObject npc)             
    {
        if (Vector3.Distance(npc.transform.position, player.transform.position) > 3)
        {
            npc.GetComponent<NPCController>().SetTransition(Transition.LostPlayer);
        }
    }
    //切换状态后的行动
    public override void Act(GameObject player, GameObject npc)
    {
        Vector3 vel = npc.GetComponent<Rigidbody>().velocity;
        Vector3 moveDir = player.transform.position - npc.transform.position;
        npc.transform.rotation = Quaternion.Slerp(npc.transform.rotation, Quaternion.LookRotation(moveDir), 5 * Time.deltaTime);
        npc.transform.eulerAngles = new Vector3(0, npc.transform.eulerAngles.y, 0);
        vel = moveDir.normalized * 10;
        npc.GetComponent<Rigidbody>().velocity = vel;
    }
   
}

完成.