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 @ 2024-03-16 11:40  被迫吃冰淇淋的小学生  阅读(182)  评论(0)    收藏  举报