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

posted @   被迫吃冰淇淋的小学生  阅读(81)  评论(0编辑  收藏  举报
努力加载评论中...
点击右上角即可分享
微信分享提示