一.初识有限状态机
以我拙见,所谓的有限状态机就是把switch case语法通过管理类+状态类的形式实现,在分支过多的情况下,使用switch case会使代码变得极其臃肿,这显然是我们要避免的,在看了一些资料后,简单的实现并使用有限状态机来写一个AI,虽然AI很适合使用FSM,但是不代表FSM只能用来做AI,实际上在学习完FSM后,立刻就想起过去写的一些代码可以使用FSM优化,比如用FSM来做卡牌游戏的回合判定状态等。
二.整理思路
上来就写代码很明显是不明智的,第一步应该先理清思路,FSM由状态类和管理类组成,我们可以写一个状态的基类,我们每添加一种状态就会写一个它的子类,然后我们用一个管理类把所有的状态管理起来,这样就可以很方便的使用状态机。思考清楚后我们可以写代码了。
三.编写代码
1.定义枚举
public enum StateId
{
NullStateId=0,
}
public enum Transtition
{
NullTranstition=0,
}
每一种状态都应该有自己唯一的标示。
2.FSMState基类
public abstract class FSMState {
protected StateId stateID;
public StateId ID
{
get { return stateID; }
}
protected Dictionary<Transtition, StateId> map = new Dictionary<Transtition, StateId>();
public void AddTranstition(Transtition trans,StateId id)
{
if (trans == Transtition.NullTranstition||id==StateId.NullStateId)
{
Debug.Log("Transtition or StateId is null");
return;
}
if(map.ContainsKey(trans))
{
Debug.Log("Transtition is exist");
return;
}
map.Add(trans, id);
}
public void DeleteTranstition(Transtition trans)
{
if(trans==Transtition.NullTranstition)
{
Debug.Log("The Transtition is null");
return;
}
if (!map.ContainsKey(trans))
{
Debug.Log("The Transtition is not exist");
return;
}
map.Remove(trans);
}
public StateId GetNextStateID(Transtition trans)
{
if (trans == Transtition.NullTranstition)
{
Debug.Log("The Transtition is null");
return default(StateId);
}
StateId stateId;
if(map.TryGetValue(trans,out stateId))
{
return stateId;
}
else
{
Debug.Log("The Transtition is not exist");
return default(StateId);
}
}
public virtual void BeforeEnterState(){}
public virtual void BeforeLeaveState(){}
public abstract Transtition CheckTranstition();
public abstract void DoUpdate();
}
不是抽象类的基类不是好基类。状态类中主要存储本状态的ID标示和装换条件和转换状态的对应表,我们一般使用字典来存储。
3.FSMSystem类
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FSMSystem {
public FSMState currentState;
private Dictionary<StateId, FSMState> dic;
public FSMSystem()
{
dic=new Dictionary<StateId,FSMState>();
}
public void AddState(FSMState state)
{
if(state==null)
{
Debug.LogError("The state is null");
return;
}
if(dic.ContainsKey(state.ID))
{
Debug.LogError("The state is exist");
return;
}
dic.Add(state.ID, state);
}
public void DeleteState(StateId id)
{
if (id == StateId.NullStateId)
{
Debug.LogError("The state you want to delete is null."); return;
}
if (!dic.ContainsKey(id))
{
Debug.LogError("The state " + id + " you want to delete is not exit."); return;
}
dic.Remove(id);
}
public void TranslateState(Transtition trans)
{
if(trans==Transtition.NullTranstition)
{
Debug.LogError("The Transtition is null");
return;
}
StateId id = currentState.GetNextStateID(trans);
if(id==default(StateId))
{
Debug.LogError("Current state is not exist Transtition");
return;
}
FSMState fs;
if (dic.TryGetValue(id, out fs))
{
currentState.BeforeLeaveState();
currentState = fs;
currentState.BeforeEnterState();
}
else
{
Debug.LogError("The FsmSystem is not contain the FsmState.");
return;
}
}
public void Start(StateId id)
{
FSMState state;
bool isGet = dic.TryGetValue(id, out state);
if (isGet)
{
state.BeforeEnterState();
currentState = state;
}
else
{
Debug.LogError("The state " + id + " is not exit in the fsm.");
}
}
public void DOUpDate()
{
Transtition trans=currentState.CheckTranstition();
if (trans != Transtition.NullTranstition)
TranslateState(trans);
currentState.DoUpdate();
}
}
代码很简单,理清思路后没有什么难理解。
四.FSM的简单使用
我们设计一个简单的敌人AI,敌人围绕着基地在巡逻,当发现玩家的时候,它会切换成追击状态,当玩家跑出它的范围后会切换回巡逻状态。
首先我们添加枚举标示,状态有巡逻和追击两种,触发条件对应发现玩家和超出范围
public enum StateId
{
NullStateId=0,
Patrol,
Pursuit
}
public enum Transtition
{
NullTranstition=0,
FindPlayer,
OutOfRange
}
编写PatrolState和Pursuit
public class PatrolState : FSMState {
private int targetIndex;
private GameObject npc;
private GameObject player;
private Transform[] wayPoint;
private Rigidbody rigid;
public PatrolState(GameObject npc,GameObject player,params Transform[] point)
{
stateID = StateId.Patrol;
targetIndex = 0;
this.npc = npc;
this.player = player;
wayPoint = point;
rigid = npc.GetComponent<Rigidbody>();
}
public override void BeforeEnterState()
{
}
public override void BeforeLeaveState()
{
}
public override void DoUpdate()
{
rigid.velocity = npc.transform.forward * 3;
Transform targetPosition = wayPoint[targetIndex];
targetPosition.position = new Vector3( targetPosition.position.x,npc.transform.position.y,targetPosition.position.z);
npc.transform.LookAt(targetPosition);
if(Vector3.Distance(npc.transform.position,targetPosition.position)<1)
{
targetIndex++;
targetIndex %= wayPoint.Length;
}
}
public override Transtition CheckTranstition()
{
if(Vector3.Distance( npc.transform.position,player.transform.position)<5)
{
return Transtition.FindPlayer;
}
return Transtition.NullTranstition;
}
}
要注意的是基类中的抽象方法是必须要在子类中实现的,虚方法可以不重写,会直接使用父类的方法。
public class PursuitState : FSMState {
private GameObject npc;
private GameObject player;
private Rigidbody rigid;
public PursuitState(GameObject npc,GameObject player)
{
stateID = StateId.Pursuit;
this.npc = npc;
this.player = player;
rigid = npc.GetComponent<Rigidbody>();
}
public override void BeforeEnterState()
{
}
public override void BeforeLeaveState()
{
}
public override Transtition CheckTranstition()
{
if(Vector3.Distance( npc.transform.position,player.transform.position)>10)
{
return Transtition.OutOfRange;
}
return Transtition.NullTranstition;
}
public override void DoUpdate()
{
rigid.velocity = npc.transform.forward * 3;
Vector3 targetPostion = player.transform.position;
targetPostion.y = npc.transform.position.y;
npc.transform.LookAt(targetPostion);
}
}
编写挂载的AI类,使用FSMSystem
public class NPCAI : MonoBehaviour {
public Transform[] wayPoint;
public GameObject player;
public FSMSystem fsm;
void Start()
{
player = GameObject.FindGameObjectWithTag("Player");
InitFSM();
}
void Update()
{
fsm.DOUpDate();
}
private void InitFSM()
{
fsm = new FSMSystem();
FSMState patrol = new PatrolState(this.gameObject, player, wayPoint);
patrol.AddTranstition(Transtition.FindPlayer, StateId.Pursuit);
fsm.AddState(patrol);
FSMState pursuit = new PursuitState(this.gameObject, player);
pursuit.AddTranstition(Transtition.OutOfRange, StateId.Patrol);
fsm.AddState(pursuit);
fsm.Start(StateId.Patrol);
}
}
完成演示