欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Unity FSM(有限状态机)

程序员文章站 2022-04-08 08:47:29
...

先看下效果:
Unity FSM(有限状态机)

FSM ,有限状态机,一个可以枚举出有限个状态,并且这些状态在特定条件下是能够来回切换的。

在游戏中经常看到的一些AI,如敌人巡逻,巡逻过程中看到玩家就追击,追上了就攻击,追不上并且有了一定的距离就返回去继续巡逻。

Unity中的Animator就是一个FSM了,不过Animator是控制角色动画播放的,什么状态的时候播放什么动画。而这里写的FSM是控制角色AI的,什么状态就做什么事。

FSM 跟 Switch case做的事一样一样,类比于 Switch 就是将case中的逻辑封装到各个State中了。
为什么要这样做呢?为了方便扩展,如果后期需要加入新状态,只需要继承基类,添加实现就好,不用修改原来的代码,对外界开放扩展,这就是开闭原则(对修改关闭,对扩展开放)。

实现过程

先来个FSMState状态类:

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

/// <summary>
/// 转换条件
/// </summary>
public enum Transition
{
    NullTransition=0,//空的转换条件
    SeePlayer,//看到玩家
    LostPlayer,//追赶过程中遗失目标玩家
}

/// <summary>
/// 当前状态
/// </summary>
public enum StateID
{
    NullState,//空的状态
    Patrol,//巡逻状态
    Chase,//追赶状态
}

public abstract class FSMState
{
    protected StateID stateID;
    public StateID StateID { get { return stateID; } }
    protected Dictionary<Transition, StateID> transitionStateDic = new Dictionary<Transition, StateID>();
    protected FSMSystem fSMSystem;

    public FSMState(FSMSystem fSMSystem)
    {
        this.fSMSystem = fSMSystem;
    }

    /// <summary>
    /// 添加转换条件
    /// </summary>
    /// <param name="trans">转换条件</param>
    /// <param name="id">转换条件满足时执行的状态</param>
    public void AddTransition(Transition trans,StateID id)
    {
        if(trans==Transition.NullTransition)
        {
            Debug.LogError("不允许NullTransition");
            return;
        }
        if(id==StateID.NullState)
        {
            Debug.LogError("不允许NullStateID");
            return;
        }
        if(transitionStateDic.ContainsKey(trans))
        {
            Debug.LogError("添加转换条件的时候" + trans + "已经存在于transitionStateDic中");
            return;
        }
        transitionStateDic.Add(trans, id);
    }
    /// <summary>
    /// 删除转换条件
    /// </summary>
    public void DeleteTransition(Transition trans)
    {
        if (trans == Transition.NullTransition)
        {
            Debug.LogError("不允许NullTransition");
            return;
        }
        if(!transitionStateDic.ContainsKey(trans))
        {
            Debug.LogError("删除转换条件的时候" + trans + "不存在于transitionStateDic中");
            return;
        }
        transitionStateDic.Remove(trans);
    }

    /// <summary>
    /// 获取当前转换条件下的状态
    /// </summary>
    public StateID GetOutputState(Transition trans)
    {
        if(transitionStateDic.ContainsKey(trans))
        {
            return transitionStateDic[trans];
        }
        return StateID.NullState;
    }
    /// <summary>
    /// 进入新状态之前做的事
    /// </summary>
    public virtual void DoBeforeEnter() { }
    /// <summary>
    /// 离开当前状态时做的事
    /// </summary>
    public virtual void DoAfterLeave() { }
    /// <summary>
    /// 当前状态所做的事
    /// </summary>
    public abstract void Act(GameObject npc);
    /// <summary>
    /// 在某一状态执行过程中,新的转换条件满足时做的事
    /// </summary>
    public abstract void Reason(GameObject npc);//判断转换条件
}

再来个FSMSystem,来管理所有的状态:

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

public class FSMSystem
{
    private Dictionary<StateID, FSMState> stateDic = new Dictionary<StateID, FSMState>();
    private StateID currentStateID;
    private FSMState currentState;

    /// <summary>
    /// 更新npc的动作
    /// </summary>
    public void Update(GameObject npc)
    {
        currentState.Act(npc);
        currentState.Reason(npc);
    }

    /// <summary>
    /// 添加新状态
    /// </summary>
    public void AddState(FSMState state)
    {
        if(state==null)
        {
            Debug.LogError("FSMState不能为空");
            return;
        }
        if(currentState==null)
        {
            currentState = state;
            currentStateID = state.StateID;
        }
        if(stateDic.ContainsKey(state.StateID))
        {
            Debug.LogError("状态" + state.StateID + "已经存在,无法重复添加");
            return;
        }
        stateDic.Add(state.StateID, state);
    }

    /// <summary>
    /// 删除状态
    /// </summary>
    public void DeleteState(StateID stateID)
    {
        if(stateID==StateID.NullState)
        {
            Debug.LogError("无法删除空状态");
            return;
        }
        if(!stateDic.ContainsKey(stateID))
        {
            Debug.LogError("无法删除不存在的状态");
            return;
        }
        stateDic.Remove(stateID);
    }

    /// <summary>
    /// 执行过渡条件满足时对应状态该做的事
    /// </summary>
    public void PerformTransition(Transition transition)
    {
        if(transition==Transition.NullTransition)
        {
            Debug.LogError("无法执行空的转换条件");
            return;
        }
        StateID id = currentState.GetOutputState(transition);
        if(id==StateID.NullState)
        {
            Debug.LogWarning("当前状态" + currentStateID + "无法根据转换条件" + transition + "发生转换");
            return;
        }
        if(!stateDic.ContainsKey(id))
        {
            Debug.LogError("在状态机里面不存在状态" + id + ",无法进行状态转换");
            return;
        }
        FSMState state = stateDic[id];
        currentState.DoAfterLeave();
        currentState = state;
        currentStateID = state.StateID;
        currentState.DoBeforeEnter();
    }
}

以上就是FSM重要的类,接下来就是使用FSM了。

先新建一个PatrolState巡逻状态:

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

/// <summary>
/// 巡逻状态
/// </summary>
public class PatrolState : FSMState
{
    private List<Transform> pathList = new List<Transform>();
    private int index = 0;
    private Transform playerTrans;
    public PatrolState(FSMSystem fSMSystem) : base(fSMSystem)
    {
        stateID = StateID.Patrol;
        Transform pathTrans = GameObject.Find("path").transform;
        Transform[] trans = pathTrans.GetComponentsInChildren<Transform>();
        foreach (Transform t in trans)
        {
            if(t!= pathTrans)
            {
                pathList.Add(t);
            }
        }
        playerTrans = GameObject.Find("Player").transform;
    }

    /// <summary>
    /// 当前状态所做的事,巡逻
    /// </summary>
    public override void Act(GameObject npc)
    {
        npc.transform.LookAt(pathList[index]);
        npc.transform.Translate(Vector3.forward * Time.deltaTime * 3);
        if(Vector3.Distance(npc.transform.position,pathList[index].position)<0.5f)
        {
            index++;
            index %= pathList.Count;
        }
    }

    /// <summary>
    /// 在某一状态执行过程中,新的转换条件满足时做的事,追赶
    /// </summary>
    public override void Reason(GameObject npc)
    {
        if(Vector3.Distance(playerTrans.position,npc.transform.position)<3)
        {
            fSMSystem.PerformTransition(Transition.SeePlayer);
        }
    }
}

再来个ChaseState追赶类:

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

/// <summary>
/// 追赶状态
/// </summary>
public class ChaseState : FSMState
{
    private Transform playerTrans;
    public ChaseState(FSMSystem fSMSystem) : base(fSMSystem)
    {
        stateID = StateID.Chase;
        playerTrans = GameObject.Find("Player").transform;
    }

    /// <summary>
    /// 当前状态所做的事,追赶
    /// </summary>
    public override void Act(GameObject npc)
    {
        npc.transform.LookAt(playerTrans);
        npc.transform.Translate(Vector3.forward * Time.deltaTime * 2);
    }

    /// <summary>
    /// 在某一状态执行过程中,新的转换条件满足时做的事,继续巡逻
    /// </summary>
    public override void Reason(GameObject npc)
    {
        if(Vector3.Distance(playerTrans.position, npc.transform.position) > 6)
        {
            fSMSystem.PerformTransition(Transition.LostPlayer);
        }
    }
}

最后我们新建一个敌人的脚本,需要挂载在敌人身上:

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

/// <summary>
/// 敌人的类,需要挂载在敌人身上
/// </summary>
public class Enemy : MonoBehaviour
{
    private FSMSystem fsm;
    void Start()
    {
        InitFSM();
    }
    private void InitFSM()
    {
        fsm = new FSMSystem();
        FSMState patrolState = new PatrolState(fsm);
        patrolState.AddTransition(Transition.SeePlayer, StateID.Chase);

        FSMState chaseState = new ChaseState(fsm);
        chaseState.AddTransition(Transition.LostPlayer, StateID.Patrol);

        fsm.AddState(patrolState);
        fsm.AddState(chaseState);
    }

    void Update()
    {
        fsm.Update(gameObject);
    }
}

路径放在path下,Enemy.cs挂载在敌人身上。名为Player的玩家一靠近就会追赶了,追不上就返回继续巡逻了。