有限状态机中有两个核心类,FSMSystem和FSMState
前者是管理状态,每一个npc身上都要有一个FSMSystem的实例对象。
后者是各种状态的基类,用abstract修饰,由它会派生出其他类,有多少种状态就会派生出多少个类。
另外有两个枚举类型,转换条件Transition和状态编号StateID。StateID的每一个值会对应一个FSMState的一个子类
下面举一个例子,一个npc有三种状态,巡逻,追逐,攻击,所以FSMState会有三个派生类
有四种转换条件,不同的状态下可以触发其中的某些转换条件
//转换状态的条件,看见玩家,失去玩家(跟丢了),靠近玩家,玩家死亡,以及一个空状态
public enum Transition
{
NullTransition,
SeePlayer,
LosePlayer,
CloseToPlayer,
PlayerDie
}
//状态编号,巡逻,追逐,攻击,以及一个空编号,三种状态对应FSMState的三个派生类
public enum StateID
{
NullStateID,
Patrol,
Chase,
Attack
}
/// <summary>
/// 状态基类,抽象类,只能继承不能实例化
/// </summary>
public abstract class FSMState
{
protected StateID stateID; //每一个状态都会对应一个id
public StateID iD
{
get
{
return stateID;
}
}
protected Dictionary<Transition, StateID> map; //在该状态下能进行哪几种转换,每一个转换条件对应一个id,也就是触发该条件时会转换到哪种状态
protected FSMSystem fsmSystem; //一个FSMSystem的引用,表示该状态是由哪个状态机来管理,构造函数中赋值
public FSMState(FSMSystem system)
{
fsmSystem = system;
map = new Dictionary<Transition, StateID>();
}
//给字典添加内容,该状态下能进行哪几种转换,传入每种转换条件以及对应的状态id
public void AddTransition(Transition trans, StateID id)
{
if(trans == Transition.NullTransition || id == StateID.NullStateID)
{
Debug.LogError("transition or id is null");
return;
}
if (map == null)
Debug.Log("kkkkkk");
if(map.ContainsKey(trans))
{
Debug.LogError("transition is already in dictionary");
return;
}
map.Add(trans, id);
}
//删除某种转换条件,也就是在该状态下不能再进行这种转换
public void DeleteTransition(Transition trans)
{
if(map.ContainsKey(trans) == false)
{
Debug.LogWarning("this transition did not exist");
return;
}
map.Remove(trans);
}
//传递过来的转换条件,判断能否发生转换
public StateID GetStateID(Transition trans)
{
if (map.ContainsKey(trans))
return map[trans];
return StateID.NullStateID;
}
//进状态之前要做的事
public virtual void DoWhileEntering() { }
//出状态之前要做的事
public virtual void DoWhileLeaving() { }
//状态机处于当前状态时会一直调用
public abstract void Act();
//判断转换条件
public abstract void Reason();
}
npc有三种状态,所以上面的FSMState有三个派生类,PatrolState,ChaseState,AttackState
三个子类都必须重写上面的Act和Reason方法,前者需要放在Update里面每一帧里调用,后者用来检查是否需要转换状态,也是需要每一帧调用,一般可以放在Act方法里面。
DoWhileEntering和DoWhileLeaving可以选择性的重写,因为在进出某个状态时不一定要执行,也可能什么事都不做
/// <summary>
/// 巡逻状态,继承自FSMState
/// </summary>
public class PatrolState : FSMState
{
private Transform[] transforms; //巡逻时的几个目标点
private GameObject npc; //NPC对象
private GameObject player; //游戏玩家对象
private Rigidbody npcRigidbody; //NPC身上的刚体
private int targetPoint = 0; //巡逻目标点的指针
public PatrolState(Transform[] tr, GameObject obj, GameObject player, FSMSystem fsmSystem) : base (fsmSystem)
{
stateID = StateID.Patrol;
transforms = tr;
npc = obj;
npcRigidbody = npc.GetComponent<Rigidbody>();
this.player = player;
}
//这个方法将会放在Update里面每一帧调用,需要做两件事,检查是否要转换状态,以及追逐时的逻辑控制
public override void Act()
{
Reason();
Patrol();
}
//控制追逐时的逻辑
private void Patrol()
{
//控制它的运动,让它一直向自己的正前方运动然后每时每刻控制它的方向
npcRigidbody.velocity = npc.transform.forward * 3; //必须要每帧调用,否则不会动
Transform targetTr = transforms[targetPoint];
Vector3 targetVect = targetTr.position;
npc.transform.LookAt(targetTr);
if (Vector3.Distance(npc.transform.position, targetVect) < 1)
{
targetPoint++;
targetPoint %= transforms.Length; //到了数组最大值时再赋值为零
}
}
//检查状态是否需要发生转换,这里是和玩家距离小于5米时触发”看见玩家“的条件,会换成另一种状态
public override void Reason()
{
if (Vector3.Distance(player.transform.position, npc.transform.position) < 5)
{
fsmSystem.PerformTransition(Transition.SeePlayer);
}
}
}
/// <summary>
/// 追逐状态,继承自FSMState
/// </summary>
public class ChaseState : FSMState
{
private GameObject npc;
private GameObject player;
private Rigidbody npcRigidbody;
public ChaseState(GameObject npc, GameObject player, FSMSystem fsmSystem) : base (fsmSystem)
{
stateID = StateID.Chase;
this.npc = npc;
this.player = player;
npcRigidbody = npc.GetComponent<Rigidbody>();
}
//同理,检查是否需要切换状态以及控制追逐玩家的逻辑
public override void Act()
{
Reason();
ChasingPlayer();
}
public override void Reason()
{
if (Vector3.Distance(npc.transform.position, player.transform.position) > 15)
fsmSystem.PerformTransition(Transition.LosePlayer);
}
private void ChasingPlayer()
{
npcRigidbody.velocity = npc.transform.forward * 3;
npc.transform.LookAt(player.transform);
}
}
/// <summary>
/// 攻击状态,继承自FSMState,这个类这里没有写什么,需要的话可以自行添加
/// </summary>
public class AttackState : FSMState
{
public AttackState(FSMSystem fsmSystem) : base(fsmSystem)
{
stateID = StateID.Attack;
}
public override void Act()
{
//do something
}
public override void Reason()
{
//check the transition
}
}
上述三个子类,分别控制进出以及处于当前状态时要做的事,不同的情况实现就不一样
下面是管理状态的类
/// <summary>
/// 状态管理,每一个npc身上都会持有一个该类的实例
/// </summary>
public class FSMSystem
{
private FSMState currentstate; //记录该npc当前状态
public FSMState currentState
{
get
{
return currentstate;
}
}
//所有状态的集合字典,该npc的所有能够到达的状态,状态有很多种,并不是每一个npc都能处于任何状态
//本例的巡逻追逐攻击三种状态比较简单,另外举个例子,动物有走,跑,跳,飞四种状态,但是如果这个npc是一直猫,那就不可能处于飞的状态,所以它的状态集合里就不会出现飞
private Dictionary<StateID, FSMState> statesDic;
public FSMSystem()
{
statesDic = new Dictionary<StateID, FSMState>();
}
//往字典里添加状态
public void AddState(FSMState state)
{
if(state == null)
{
Debug.LogError("the state you want to add is null");
return;
}
if(statesDic.ContainsKey(state.iD))
{
Debug.LogError("the state you want to add is already exist");
return;
}
statesDic.Add(state.iD, state);
}
//删除状态
public void DeleteState(FSMState state)
{
if (state == null)
{
Debug.LogError("the state you want to delete is null");
return;
}
if (statesDic.ContainsKey(state.iD) == false)
{
Debug.LogError("the state you want to delete is not exist");
return;
}
statesDic.Remove(state.iD);
}
/// <summary>
/// 控制状态的转换
/// </summary>
/// <param name="trans">转换条件</param>
public void PerformTransition(Transition trans)
{
//首先判断传入的转换条件是否为空,为空直接返回
if (trans == Transition.NullTransition)
{
Debug.LogError("the transition is null");
return;
}
//不为空的话,查找此转换条件下能转换成什么状态,返回状态的id
StateID id = currentstate.GetStateID(trans);
//如果是空id则返回,说明该状态不能进行这种转换
if (id == StateID.NullStateID)
{
Debug.LogError("this transition will not happen");
return;
}
//以上异常情况都排除,在状态集合中找到相应的状态,并赋值
FSMState state;
statesDic.TryGetValue(id, out state);
currentstate.DoWhileLeaving();
currentstate = state;
currentstate.DoWhileEntering();
}
/// <summary>
/// 状态机开始工作,也就是赋予该npc一个初状态
/// </summary>
/// <param name="id">状态机的id</param>
public void Start(StateID id)
{
if(id == StateID.NullStateID)
{
Debug.LogError("id is null");
return;
}
FSMState state;
if(statesDic.TryGetValue(id, out state))
{
currentstate = state;
state.DoWhileEntering();
return;
}
Debug.LogError("no such state");
}
}
下面是一个控制npc的脚本,这个需要挂在npc身上
public class NPCcontrol : MonoBehaviour
{
//一个状态管理类的实例,这里面记录的有这个npc当前处于何种状态
private FSMSystem fsm;
public Transform[] transforms; //巡逻状态时需要用到的几个巡逻点的位置
public GameObject npc;
public GameObject player;
//以上三个变量都是处于某种状态时要用到的,作为参数传递给某个状态
void Start()
{
Init();
}
//初始化
private void Init()
{
fsm = new FSMSystem(); //生成一个状态机
//生成一个巡逻状态的实例,并给追巡逻态添加转换条件,在看见玩家时可以切换到追逐状态
PatrolState patrolState = new PatrolState(this.transforms, npc, player,fsm);
patrolState.AddTransition(Transition.SeePlayer, StateID.Chase);
//生成一个追逐状态实例,并添加转换条件,三种情况下会切换到对应的新状态
ChaseState chaseState = new ChaseState(npc,player, fsm);
chaseState.AddTransition(Transition.CloseToPlayer, StateID.Attack);
chaseState.AddTransition(Transition.LosePlayer, StateID.Patrol);
chaseState.AddTransition(Transition.PlayerDie, StateID.Patrol);
//同理,生成攻击状态的实例
AttackState attackState = new AttackState(fsm);
attackState.AddTransition(Transition.PlayerDie, StateID.Patrol);
attackState.AddTransition(Transition.LosePlayer, StateID.Patrol);
//将上述三种状态添加到状态管理系统的字典里
fsm.AddState(attackState);
fsm.AddState(patrolState);
fsm.AddState(chaseState);
//给一个初始状态,启动状态机
fsm.Start(StateID.Patrol);
}
private void Update()
{
fsm.currentState.Act(); //每时每刻调用当前状态下的Act函数
}
}