FSM状态机及C#反射实现逻辑
零、大致逻辑
1.初始化 Start
组件->状态->状态内部初始化->进入初始状态
2.运行时 Update
遍历状态的所有条件->
不满足所有条件对象->执行当前状态运行时逻辑->进行一次玩家搜索
满足某一个条件对象->执行当前状态退出逻辑->执行状态改变->执行新状态进入逻辑->执行新状态运行时逻辑->进行一次玩家搜索
3.销毁时 OnDestroy
清空所有状态内部数据->清空状态机数据->清空所有状态
一、状态机配置文件和它的工厂
状态机需要知道当前对象会有哪些状态,且状态需要知道有哪些条件
并以Dictionary<状态, Dictionary<条件, 目标状态>>存到状态机内部
状态机填一个文件名,数据以txt方式存在于StreamAsset文件夹下
使用反射,将这一个个字符串变成状态类和条件类
那么,该如何去拿到这些字符串呢
我们使用一个工厂,状态机传字符串给工厂
工厂看看自己的缓存中有没有,有就给你,没有就new
下方的类就和一个字符串相匹配Dictionary<string, FSMConfigFlie>
状态机配置文件初始化
public class FSMConfigFlie { public Dictionary<string, Dictionary<string, string>> Map; private string current = ""; //这是供工厂使用的文件读取方法 public FSMConfigFlie(string fileName) { Map = new Dictionary<string, Dictionary<string, string>>(); BuildMap(ConfigFile.GetConfigFile(fileName), MyBuildMap); } //这是其他地方写的享元函数,但由于篇幅太大,就直接提过来了 private void BuildMap(string paths,Action<string> handler) { using (StringReader reader = new StringReader(paths)) { string line; while ((line = reader.ReadLine()) != null) { handler(line); } } //StringReader是个数据流,在使用完毕后要关闭,倘若中途出错,就不会关了 //但是用using括起来,那么只要它运行完毕, 肯定关了。 } //详细解析 private void MyBuildMap(string line) { if (line.StartsWith("[")) { current = line.Substring(1, line.Length - 2); Map.Add(current, new Dictionary<string, string>()); } else if (line.Contains(">")) { string[] keyValue = line.Split('>'); Map[current].Add(keyValue[0], keyValue[1]); } } }
配置文件Map缓存
public class FSMConfigFlieFactory { //缓存 public static Dictionary<string, FSMConfigFlie> OMap; //静态变量 static FSMConfigFlieFactory() { OMap = new Dictionary<string, FSMConfigFlie>(); } //供外部使用的状态数据获取方法 public static Dictionary<string, Dictionary<string, string>> GetMap(string file) { if (!OMap.ContainsKey(file)) { OMap.Add(file, new FSMConfigFlie(file)); } return OMap[file].Map; } }
此时,我们就获得了
Dictionary<状态, Dictionary<条件, 目标状态>>
二、状态抽象类
然后,就是状态类
状态机拿到Dictionary<状态, Dictionary<条件, 目标状态>>
遍历其中的Key,反射生成状态类后,foreach将其中的Dictionary<条件, 目标状态>传给状态类
状态类再使用反射区生成条件类和目标条件类
此外,状态类还需要固定的属性:条件类List、Dictionary<条件, 目标状态>、StateID
固定的方法构造函数、初始化方法、添加条件方法、遍历条件方法、进入状态方法、执行状态方法、退出状态方法
状态父类
public abstract class FSMState { //编号 public FSMStateID StateID { get; set; } //映射表 private Dictionary<FSMTriggerID, FSMStateID> map; //条件列表 private List<FSMTrigger> triggers; public FSMState() { map = new Dictionary<FSMTriggerID, FSMStateID>(); triggers = new List<FSMTrigger>(); } //检测当前状态的条件是否满足 public void Reason(FSMBase fsm) { for (int i = 0; i < triggers.Count; i++) { //发现满足条件 if (triggers[i].HandleTrigger(fsm)) { //从映射表中获取目标状态 FSMStateID stateID = map[triggers[i].TriggerID]; //切换状态 fsm.ChangeActiveState(stateID); return; } } } //要求子类必须初始化状态,为编号赋值 public abstract void Init(FSMBase fsm); public void AddMap(FSMTriggerID triggerID, FSMStateID stateID) { //条件映射 map.Add(triggerID, stateID); //创建条件对象(命名规则:"FSM." + 条件枚举 + "Trigger") Type type = Type.GetType("FSM." + triggerID + "Trigger"); if (type == null) Debug.Log("FSM." + triggerID + "Trigger"); FSMTrigger trigger = Activator.CreateInstance(type) as FSMTrigger; triggers.Add(trigger); } //为子类提供可选提供 public virtual void EnterState(FSMBase fsm) { } public virtual void ActionState(FSMBase fsm) { } public virtual void ExitState(FSMBase fsm) { } }
其中,XXXID主要是为了让枚举对应上类,存枚举,然后根据枚举值找类
OK,现在就差条件类了,即triggers[i].HandleTrigger(fsm)是什么
三、条件抽象类
都知道是由状态类去创建的条件类,即状态类中的
FSMTrigger trigger = Activator.CreateInstance(type) as FSMTrigger;
条件父类
public abstract class FSMTrigger { //编号 public FSMTriggerID TriggerID { get; set; } public FSMTrigger() { Init(); } //要求子类必须初始化条件,为编号赋值 public abstract void Init(); //要求子类必须有逻辑处理 public abstract bool HandleTrigger(FSMBase fsm); }
条件类去判断状态机中的某个参数符不符合当前状态
如果不符合,就会返回false,告诉状态类该切换状态了
状态类拿到条件类的ID,去Dictionary<FSMTriggerID, FSMStateID>中找到目标状态
把目标状态ID传给状态机,状态机执行切换状态方法
某一条件类:false->状态类拿到对应状态ID传到状态机->状态机切换状态
四、FSMBase代码
查看代码
using System; using System.Collections; using System.Collections.Generic; using UnityEngine.AI; using UnityEngine; using Character; using Common; namespace FSM { /// <summary> /// 状态机 /// </summary> public class FSMBase : MonoBehaviour { List<FSMState> states;//状态列表 [Tooltip("默认状态ID")] public FSMStateID defaultID; [HideInInspector]//当前状态,默认状态 public FSMState currentState, defaultState; [HideInInspector]//动画机 public Animator animator; [HideInInspector]//状态类 public CharacterStatus characterStatus; [HideInInspector]//寻路系统 public NavMeshAgent nav; //跑步,走路 public float runSpeed = 2, walkSpeed = 1; public Transform fatherPoint; public string configFileName = "AI_01.txt"; void Start() { InitComponent(); ConfigFSM(); InitDefaultState(); } private void InitComponent() { if (GetComponent<NavMeshAgent>()) nav = GetComponent<NavMeshAgent>(); animator = GetComponentInChildren<Animator>(); characterStatus = GetComponent<CharacterStatus>(); } //配置状态机 private void ConfigFSM() { states = new List<FSMState>(); var Map = FSMConfigFlieFactory.GetMap(configFileName); foreach (var state in Map) { //创建状态对象 Type type = Type.GetType("FSM." + state.Key + "State"); FSMState s = Activator.CreateInstance(type) as FSMState; s.Init(this); states.Add(s);//加入状态机 foreach (var dic in state.Value) { //设置状态(AddMap) FSMTriggerID triggerID = (FSMTriggerID)Enum.Parse(typeof(FSMTriggerID), dic.Key); FSMStateID stateID = (FSMStateID)Enum.Parse(typeof(FSMStateID), dic.Value); s.AddMap(triggerID, stateID); } } } //为默认状态赋值 private void InitDefaultState() { //查找默认状态,并赋值 defaultState = states.Find(s => s.StateID == defaultID); currentState = defaultState; //进入状态 currentState.EnterState(this); } //检测状态,每帧处理逻辑 void Update() { //执行当前状态条件 currentState.Reason(this); //执行当前状态逻辑 currentState.ActionState(this); //查找目标 SawTarget(); } //切换状态 public void ChangeActiveState(FSMStateID stateID) { //如果需要切换的状态ID是默认ID,这赋默认状态值 //if (stateID == FSMStateID.Default) currentState = defaultState; //否则查找目标状态 //else currentState = states.Find(s => s.StateID == stateID); currentState.ExitState(this);//离开旧->切换->进入新 currentState = stateID == FSMStateID.Default ? defaultState : states.Find(s => s.StateID == stateID); currentState.EnterState(this); } string[] testTag = new string[1] { "Player" }; public float dis; float ang = 360; Transform[] tarTFs; [HideInInspector] public Transform tarTF; //寻找目标 private void SawTarget() { tarTFs = TransformHelper.FindAllObj(testTag, dis, ang, transform); tarTF = tarTFs.Length > 0 ? tarTFs[0] : null; if (tarTF != null) { if (tarTF.GetComponent<CharacterStatus>().HP <= 0) tarTF = null; } } public void MoveTotarget(Vector3 pos, float stopDis, float speed) { if (nav) { nav.SetDestination(pos); nav.stoppingDistance = stopDis - 1; nav.speed = speed; } else { StartCoroutine(FlyToTarget(pos, stopDis, speed)); } } private IEnumerator FlyToTarget(Vector3 pos, float stopDis, float speed) { while (Vector3.Distance(transform.position, pos) > stopDis) { transform.position = Vector3.MoveTowards(transform.position, pos, Time.deltaTime * speed); transform.rotation = Quaternion.RotateTowards(transform.rotation, Quaternion.LookRotation(pos - transform.position), 3f); if (currentState.StateID == FSMStateID.Pursuit) break; yield return null; } } } } /* *程序执行流程 *状态机每帧检测当前状态的条件--->状态类遍历所有条件对象---> *如果某个条件达成--->状态机切换目标状态 */
反射
简单介绍
由上述案例就可以看出,使用反射技术可以创建一个类,该类可以读取其他类的数据,其他类也能使用该类
相当于new了一个对象,但该对象是通过字符串去决定的
C# 反射(Reflection) | 菜鸟教程 (runoob.com)
优点
1.反射提高了程序的灵活性和扩展性。
2.降低耦合性,提高自适应性
3.它允许程序创建和控制任何类的对象,无需提前硬编码目标类。
缺点
1.性能问题:使用反射基本上是一种解释操作,用于字段和方法接入时要远慢于直接代码。因此反射机制主要应用在对灵活性和拓展性要求很高的系统框架上,普通程序不建议使用。
2.维护问题:使用反射会模糊程序内部逻辑;程序员希望在源代码中看到程序的逻辑,反射却绕过了源代码的技术,因而会带来维护的问题,反射代码比相应的直接代码更复杂。
使用方法
父类 instance = Activator.CreateInstance("子类") as 父类;
实际上instance内部储存的就是子类,这是继承的基础功能
底层原理
C#不是计算机能读懂语言,因此需要编译,而反射,就是对编译过后的.dll文件进行的操作
.dll文件分为两部分
metadata:生成的描述,它可能是把命名空间、类名、属性名记录了一下,包括特性。是一个数据结构。
IL:我们写的实际代码,类、方法、属性等
在编译代码的时候,元数据表就根据代码把类的所有信息都记录在了它里面。
而反射的过程刚好相反,就是类通过元数据里记录的关于目标类的详细信息找到该类的成员,并能使它“复活”
(因为元数据里所记录的信息足够详细,以致于可以根据metadata里面记录的信息找到关于该类的IL code并加以利用)。
0
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步