有限状态机:数量有限的多个状态在不同的条件下相互转换的流程控制系统。
适用范围:状态,条件不确定的情况下或者不同种角色每种角色有不同的状态对应不同条件时。
使用方法:
状态机三要素:状态,条件与状态转换表。
状态转换表:表内显示当前状态,输入(条件)是什么,对应的下一个状态的输出是什么。
状态机:管理所有状态,协调组织状态的迁移。
首先是条件的枚举,存储所有的条件,赋予条件ID(FSMTriggerID)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace AI.FSM
{
/// <summary>
/// 条件的枚举,存储所有的条件,赋予条件ID
/// 后续如果条件改变或新增可修改
/// </summary>
public enum FSMTriggerID
{
//生命值为0
NoHealth,
//发现目标
SawTarget,
//目标进入攻击范围
ReachTarget,
//丢失目标
LoseTarget,
//完成巡逻
CompletePatrol,
//击杀目标
KilledTarget,
//目标离开攻击范围
WithoutAttackRange,
//.......
}
}
然后是状态的枚举,存储所有的状态,赋予状态ID(FSMStateID)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace AI.FSM
{
/// <summary>
/// 状态的枚举,存储所有的状态,赋予状态ID
/// 后续如果状态改变或新增可修改
/// </summary>
public enum FSMStateID
{
None,
//Default,
Dead,
Idel,
Pursult,
//......
}
}
再然后是所有条件的基类,之后的所有条件继承此类(FSMTrigger)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace AI.FSM
{
/// <summary>
/// 所有条件的基类,之后的所有条件继承此类
/// </summary>
public abstract class FSMTrigger
{
/// <summary>
/// 条件的编号,必不可少----------------------------1
/// </summary>
public FSMTriggerID TriggerID { get; set; }
public FSMTrigger()
{
Init();
}
/// <summary>
/// 要求子类必须初始化条件,为编号赋值--------------3
/// </summary>
public abstract void Init();
/// <summary>
/// 逻辑处理(判断结果)----------------------------2
/// 一般在判断是会用到一些变量
/// 这些变量可以写在状态机里,这里我们用这种
/// 也可以写在一个特定的类里 public abstract bool HandleTrigger(******* ******);
/// </summary>
/// <returns></returns>
public abstract bool HandleTrigger(FSMBase fsmBase);
}
}
随后是所有状态的基类,之后的所有状态继承此类(FSMState)
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace AI.FSM
{
/// <summary>
/// 所有状态的基类,之后的所有状态继承此类
/// </summary>
public abstract class FSMState
{
/// <summary>
/// 状态的编号,必不可少--------------------------------------------------------1
/// </summary>
public FSMStateID StateID { get; set; }
/// <summary>
/// 条件与状态的映射表--------------------------------------------------------------3
/// </summary>
private Dictionary<FSMTriggerID, FSMStateID> map;
/// <summary>
/// 条件集-------------------------------------------------------------------------4
/// </summary>
private List<FSMTrigger> Triggers;
public FSMState()
{
map = new Dictionary<FSMTriggerID, FSMStateID>();
Triggers = new List<FSMTrigger>();
Init();
}
/// <summary>
/// 判断当前状态的哪个条件满足,切换至下一个状态
/// </summary>
public void Reason(FSMBase fsmBase)
{
foreach (var trigger in Triggers)
{
if(trigger.HandleTrigger(fsmBase))
{
//条件满足,切换状态
FSMStateID stateID = map[trigger.TriggerID];
fsmBase.ChangeState(stateID);
return;
}
}
}
/// <summary>
/// 要求子类必须初始化状态,为编号赋值------------------------------------------------2
/// </summary>
public abstract void Init();
/// <summary>
/// 由状态机调用,添加映射表与条件集数据----------------------------------------------5
/// </summary>
/// <param name="triggerID"></param>
/// <param name="stateID"></param>
public void AddMap(FSMTriggerID triggerID, FSMStateID stateID)
{
//添加映射
map.Add(triggerID, stateID);
//创建条件对象添加至条件集
AddTrigger(triggerID);
}
/// <summary>
/// 创建条件对象添加至条件集-----------------------------------------------------------6
/// </summary>
/// <param name="triggerID"></param>
private void AddTrigger(FSMTriggerID triggerID)
{
//使用反射,创建条件对象
//反射的命名规范:AI.FSM + triggerID + Trigger
Type type = Type.GetType("AI.FSM." + triggerID + "Trigger");
FSMTrigger trigger = Activator.CreateInstance(type) as FSMTrigger;
Triggers.Add(trigger);
}
//为子类提供可选实现------------------------------------------------------------------------7
// 一般在状态里会用到一些变量
// 这些变量可以写在状态机里,这里我们用这种
// 也可以写在一个特定的类里 public virtual void OnEnterState(******* ******);
public virtual void OnEnterState(FSMBase fsmBase) { }
public virtual void OnStayState(FSMBase fsmBase) { }
public virtual void OnExitState(FSMBase fsmBase) { }
}
}
然后是状态机,这是一个脚本,可以直接挂到NPC或玩家的物体上(FSMBase)
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace AI.FSM
{
/// <summary>
/// 状态机
/// </summary>
public class FSMBase : MonoBehaviour
{
#region 状态机自身数据
/// <summary>
/// 状态列表
/// </summary>
private List<FSMState> states;
/// <summary>
/// 当前状态
/// </summary>
private FSMState currentState;
/// <summary>
/// 默认状态
/// </summary>
private FSMState defaultState;
[Tooltip("默认初始状态")]
public FSMStateID defaultStateID;
private void Start()
{
ConfigFSM();
InitComponent();
InitDefalutState();
}
/// <summary>
/// 配置状态机
/// </summary>
private void ConfigFSM()
{
states = new List<FSMState>();
//1.创建状态对象(调用FSMState.AddMap)2.设置当前状态
#region 旧的方法,通过手写添加,当状态或者条件添加或修改需要修改此代码,不好
当有具体状态类后(如IdelState与DeadState)执行
创建状态对象
//IdelState idel = new IdelState();
调用FSMState.AddMap 添加状态与条件的映射(有几种映射添加几次)
//idel.AddMap(FSMTriggerID.NoHealth, FSMStateID.Dead);
idel.AddMap(FSMTriggerID.SawTarget)
将此状态加入到状态机的状态列表内
//states.Add(idel);
添加死亡状态逻辑与上面一样
//DeadState dead = new DeadState();
由于死亡后没有其他状态所以没有映射不用AddMap
//states.Add(dead);
有其他状态继续添加
#endregion
#region 新的方法,通过读取配置文件添加,当状态或者条件添加或修改时只需修改配置文件即可,代码不用修改
//当多个NPC使用一样的配置文件时,此时会重复解析相同文件,会增加消耗,所以可以用一个新方法
//var map = new AIConfigurationReader(fileName).map;
//新方法,使用工厂存储解析过的ai文件
var map = AIConfigurationReaderFactory.GetMap(fileName);
foreach (var stateName in map)
{
//stateName.key -->当前状态
//stateName.value--->映射
Type type = Type.GetType("AI.FSM." + stateName.Key + "State");
FSMState state = Activator.CreateInstance(type) as FSMState;
foreach (var values in stateName.Value)
{
//values.key -->条件编号
//values.value --->状态编号
//string 转 enum
FSMTriggerID triggerID = (FSMTriggerID)Enum.Parse(typeof(FSMTriggerID), values.Key);
FSMStateID stateID = (FSMStateID)Enum.Parse(typeof(FSMStateID), values.Value);
//添加映射
state.AddMap(triggerID, stateID);
}
//添加状态
states.Add(state);
}
#endregion
}
/// <summary>
/// 初始化当前状态
/// </summary>
private void InitDefalutState()
{
defaultState = states.Find(s => s.StateID == defaultStateID);
currentState = defaultState;
//执行初始状态的进入
currentState.OnEnterState(this);
}
/// <summary>
/// 每帧处理的逻辑
/// </summary>
public void Update()
{
//判断当前状态的条件
currentState.Reason(this);
//执行当前状态的逻辑
currentState.OnStayState(this);
}
/// <summary>
/// 切换状态
/// </summary>
/// <param name="stateID"></param>
public void ChangeState(FSMStateID stateID)
{
//执行上一个状态的退出
currentState.OnExitState(this);
//设置当前状态(切换状态)
//如果要切换的状态ID为default,则当前状态为初始状态
//否则在状态列表内查询
if (stateID == FSMStateID.Default)
currentState = defaultState;
else
currentState = states.Find(s => s.StateID == stateID);
//执行当前状态的进入
currentState.OnEnterState(this);
}
#endregion
#region 供条件与状态使用的数据
//处理上述基本需要外,一般还会需要很多其他参数提供给条件与各具体状态使用
//例如血量
[HideInInspector]//在编辑器界面不显示
public int HP;
//例如播放待机动画等等
[HideInInspector]
public Animator animator;
//当播放动画时需要一个变量可以拿到任务的各种动画
//public ******* characterStates;
/// <summary>
/// 初始化其他参数
/// </summary>
private void InitComponent()
{
animator = GetComponentInChildren<Animator>();
}
#endregion
}
}
可以写一个配置文件,通过读取配置文件进行状态机配置(ConfigurationReader)
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEngine.Networking;
namespace Common
{
/// <summary>
/// 配置文件读取器
/// </summary>
public class ConfigurationReader
{
/// <summary>
/// 通过文件名称读取文件内容
/// </summary>
/// <param name="fileName"></param>
/// <returns></returns>
public static string GetConfigFile(string fileName)
{
string url;
#region 分平台判断StreamingAssets路径
//string url = "file://" + Application.streamingAssetsPath + "/" + fileName;
//如果在编译器或单机中
//if(Application.platform == RuntimePlatform.Android)
//Unity宏标签
#if UNITY_EDITOR || UNITY_STANDALONE
url = "file://" + Application.dataPath + "/StreamingAssets/" + fileName;
//否则如果在Iphone下
#elif UNITY_IPHONE
url = "file://" + Application.dataPath + "/Raw/" + fileName;
//否则如果在android下
#elif UNITY_ANDROID
url = "jar:file://" + Application.dataPath + "!/assets/" + fileName;
#endif
#endregion
WWW www = new WWW(url);
while(true)
{
if (www.isDone)
return www.text;
}
}
/// <summary>
/// 解析文件内容
/// </summary>
/// <param name="fileContent"></param>
/// <param name="handler"></param>
public static void Reader(string fileContent, Action<string> handler)
{
using (StringReader reader = new StringReader(fileContent))
{
//一行一行的读取
//当内容不为空时,使用特定方法解析单行信息
string line;
while((line = reader.ReadLine()) != null)
{
handler(line);
}
}
}
}
}
然后使用配置文件赌气器里的方法读取-解析AI配置文件(AIConfigurationReader)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Common;
using System;
namespace AI.FSM
{
/// <summary>
/// AI配置文件读取-解析
/// </summary>
public class AIConfigurationReader
{
//数据结构
//大字典key:当前状态,value:映射表
//小字典key:条件,value:目标状态
public Dictionary<string, Dictionary<string, string>> map { get; private set; }
//当前状态
private string mainkey;
public AIConfigurationReader(string fileName)
{
map = new Dictionary<string, Dictionary<string, string>>();
//读取配置文件
string filecontent = ConfigurationReader.GetConfigFile(fileName);
//解析配置文件
ConfigurationReader.Reader(filecontent, BuildMap);
}
/// <summary>
/// 解析配置文件
/// </summary>
/// <param name="line"></param>
private void BuildMap(string line)
{
//去除空白(如果是空行/r/n,则为空字符串)
line = line.Trim();
if (string.IsNullOrEmpty(line))
return;
//如果以[开头
if(line.StartsWith("["))
{
//[Idel]--->Idel
mainkey = line.Substring(1, line.Length - 2);
//添加map
map.Add(mainkey, new Dictionary<string, string>());
}
else
{
//映射Nohealth>Dead
string[] key_Value = line.Split('>');
map[mainkey].Add(key_Value[0], key_Value[1]);
}
}
}
}
配置文件读取器工厂(AIConfigurationReaderFactory)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace AI.FSM
{
/// <summary>
/// AI配置文件读取-解析器工厂
/// </summary>
public class AIConfigurationReaderFactory
{
//存储所有已经读取-解析过的AI配置文件,key:文件名,value:AI配置文件读取-解析器
private static Dictionary<string, AIConfigurationReader> cache = new Dictionary<string, AIConfigurationReader>();
public static Dictionary<string, Dictionary<string, string>> GetMap(string fileName)
{
if(!cache.ContainsKey(fileName))
{
cache.Add(fileName, new AIConfigurationReader(fileName));
}
return cache[fileName].map;
}
}
}
此时所有的基础代码写完,下面就是写具体的状态与条件,并完善状态机
例如待机状态(IdelState)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace AI.FSM
{
/// <summary>
/// 待机状态
/// </summary>
public class IdelState : FSMState
{
public override void Init()
{
this.StateID = FSMStateID.Idel;
}
public override void OnEnterState(FSMBase fsmBase)
{
base.OnEnterState(fsmBase);
//播放待机动画
//fsmBase.animator.SetBool();
}
public override void OnExitState(FSMBase fsmBase)
{
base.OnExitState(fsmBase);
//取消播放待机动画
//fsmBase.animator.SetBool();
}
}
}
血量为0的条件(NoHealthTrigger)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace AI.FSM
{
/// <summary>
/// 血量为0的条件
/// </summary>
public class NoHealthTrigger : FSMTrigger
{
/// <summary>
/// 判断条件是否满足
/// </summary>
/// <param name="fsmBase"></param>
/// <returns></returns>
public override bool HandleTrigger(FSMBase fsmBase)
{
//如果HP<=0则返回true,否则返回false
return fsmBase.HP <= 0;
}
public override void Init()
{
this.TriggerID = FSMTriggerID.NoHealth;
}
}
}
和死亡状态(DeadState)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace AI.FSM
{
/// <summary>
/// 死亡状态
/// </summary>
public class DeadState : FSMState
{
public override void Init()
{
this.StateID = FSMStateID.Dead;
}
/// <summary>
/// 进入死亡状态
/// </summary>
/// <param name="fsmBase"></param>
public override void OnEnterState(FSMBase fsmBase)
{
base.OnEnterState(fsmBase);
//若死亡动画以及hp这类数据在其他地方已经执行,则不在播放动画或做其他事(如技能系统中等)
//禁用状态机,以免状态机的updata一直运行,然后某种条件满足出现意外情况
fsmBase.enabled = false;
}
}
}