首页 > 其他分享 >Unity-FSM有限状态机

Unity-FSM有限状态机

时间:2023-11-14 19:26:42浏览次数:37  
标签:状态 enemy void FSM 状态机 Unity public currentState

Unity-FSM有限状态机

什么是有限状态机?

​ 在编写一些需要判断多个条件的程序时,我们常常会用到 if-else 语句,这样能够很好的帮我们解决多数问题。但在游戏开发过程中,一个角色的行为不是一成不变的,需要实时的进行修改,此时如果我们使用的是 if-else 来判断角色所处状态,就需要修改整个代码体,十分麻烦。而有限状态机很好的解决了这一些问题。

​ 有限状态机实现的方式是,将判断条件、角色状态、状态机分别封装成一个类,这样当我们需要增加或者减少角色的状态时,直接将对应的状态与相对应的条件删除即可,不会影响到所有的代码,极大的减少了开发时间。

​ 下面将介绍有限状态机该如何编写。

通常的有限状态机

​ 一般的状态机代码,会先有一个抽象的状态父类,其中包含两个抽象函数,一个是需要在进入状态的一开始就执行,一个是需要在程序执行的时候每一帧都执行,并由子类继承这个抽象父类,实现对应的两个函数。切换条件需要写在每一帧都调用的函数中。

  • 状态类:
public abstract class EnemyBaseState //状态父类
{
    public abstract void EnterState(Enemy enemy);

    public abstract void OnUpdate(Enemy enemy);
}

public class PatrolState : EnemyBaseState //巡逻子类
{
    public override void EnterState(Enemy enemy)//进入状态函数
    {
        //此处省略
    }

    public override void OnUpdate(Enemy enemy)//每帧都需要执行
    {
        //此处省略

        if(Mathf.Abs(enemy.transform.position.x - enemy.targetPoint.position.x) < 0.01f)//如果已经达到目标点
        {
            enemy.TransitionState(enemy.patrolState);//重新进入该状态
        }

        if(enemy.attackList.Count != 0)//攻击目标集合不为0
        {
            enemy.TransitionState(enemy.attackState);//切换为攻击状态
        }
    }
}

​ 在使用状态机时,就可以只创建抽象父类实例对象,在程序执行的过程中将不同的子类赋值给该对象(里氏转换),并调用其中的两个函数。

​ 当然,在执行各个状态中的函数时,不可避免的需要调用脚本中的参数,所以需要将脚本的引用传递给状态机

  • Controller(状态机使用者):
public abstract class Enemy : MonoBehaviour
{
	[Header("状态机相关")]
    
    //创建状态机对象
    EnemyBaseState currentState; 
    
    //实例化子类对象
    public PatrolState patrolState = new PatrolState();
    public AttackState attackState = new AttackState();

    //程序开始时,设置初始状态
	protected virtual void Start()
    {
        TransitionState(patrolState);
    }
    
    public void TransitionState(EnemyBaseState state)//切换状态类的函数
    {
        currentState = state;
        currentState.EnterState(this);//进入状态需要执行的函数
    }
    
    protected virtual void Update()
    {
        currentState.OnUpdate(this); //各个状态中每一帧都需要执行的函数
    }
}

将条件写成类的有限状态机

​ 如上所述,我们需要将代码变成三个部分,判断条件、角色状态、状态机,三个部分对应三个基类。这样的写法,需要将在状态类中实例化条件类的对象,从而进行条件的判断。

​ 下面将一一介绍:

判断条件(Trigger)

​ 条件类需要以下几个成员:

  1. 条件编号:属性成员代替,方便其他类查找条件
  2. 构造函数:强制要求对属性赋值
  3. 判断函数:需要传进一个状态机参数,因为条件类本身不自带参数,需要状态机的参数进行条件判断
public abstract class Trigger
{
    public TriggerID triggerID{get; set;}//属性编号
    
    public Trigger()
    {
        Init();
    }
    
    public abstract void Init();//构造函数,用于对属性赋值
    
    public abstract bool HandleTrigger(Base fsmBase);
    //判断函数,可以让别的类调用,并返回一个bool值
}

角色状态(State)

​ 状态类需要以下几个成员:

  1. 状态编号:用属性代替,方便查找对应的条件
  2. 条件集合:由于保存各种条件对象
  3. 字典集合:用于查询条件成立时对应的状态
  4. 构造函数:强制要求对属性赋值
  5. 配置字典函数:向字典集合中添加成员
  6. 判断函数:由状态机调用,判断此时有何条件成立
  7. 三个基本函数:进入状态、状态执行、退出状态
public abstract class State
    {

        private Dictionary<TriggerID,StateID> map;//字典

        public List<Trigger> Triggers;//条件集合

        public StateID stateID { get; set; }//状态属性

        public State()
        {
            map = new Dictionary<TriggerID, StateID>();
            
            Init();
        }

        public abstract void Init();//抽象化构造函数

        public void Reason(Base fsmBase)//由 状态机 调用的 检测方法
        {
            for(int i=0;i<Triggers.Count;i++)//遍历条件集合
            {
                if(Triggers[i].HandleTrigger(fsmBase))
                {
                    //切换状态
                    fsmBase.ChangeActiveState(map[Triggers[i].triggerID]);
                    //将条件对应的状态ID传引用给状态机
                    return;
                }
            }
        }

        public void AddMap(TriggerID triggerID,StateID stateID)
        {
            map.Add(triggerID,stateID);
            //创建条件对象

            Type type = Type.GetType("FSM."+triggerID+"Trigger");
            Trigger trigger = Activator.CreateInstance(type) as Trigger;
            Triggers.Add(trigger);
        }

        public virtual void EnterState(Base fsmBase){}
        public virtual void ActionState(Base fsmBase){}
        public virtual void ExitState(Base fsmBase){}
    }

状态机(Base)

​ 状态机所需成员:

  1. 默认状态ID:在Unity界面选择
  2. 状态列表:存储所有状态对象
  3. 初始化组件函数:初始化对应组件
  4. 初始化默认状态函数:根据默认状态ID设置默认状态
  5. 配置状态机函数:向条件列表与条件类中的字典集合中添加对象
  6. 切换状态函数:由状态类调用,条件满足后切换状态
public class Base : MonoBehaviour
    {
        
        [Header("角色基本组件")]
        [HideInInspector]public Animator Anim;
        [HideInInspector]public Rigidbody2D rb;
        
        [Tooltip("初始状态")]public StateID defaultStateID;
        private List<State> states;//状态列表
        public State currentState;//当前状态
        public State defaultState;

        public virtual void Start() 
        {
            InitCommponent();
            SetBase();
            InitDefaultState();
        }

        public void InitCommponent()//初始化组件
        {
            Anim = GetComponentInChildren<Animator>();
            rb = GetComponentInChildren<Rigidbody2D>();
        }


        public void InitDefaultState()//初始化默认状态
        {
            defaultState = states.Find(s => s.stateID == defaultStateID);
            currentState = defaultState;
            currentState.EnterState(this);
        }

        //配置状态机
        private void SetBase()
        {
            states = new List<State>();
            // //--创建状态对象
            // IdleState idleState = new IdleState();
            // //--添加映射
            // idleState.AddMap(TriggerID.NoHealth,StateID.Dead);
            // //--加入状态机
            // states.Add(idleState);

            // DeadState deadState = new DeadState();
            // states.Add(deadState);
        }

        //每帧处理的逻辑
        public virtual void Update() 
        {
            //判断当前状态条件
            currentState.Reason(this);
            //执行当前状态逻辑
            currentState.ActionState(this);
        }

        public void ChangeActiveState(StateID stateID)//切换状态
        {
            //离开上一个状态
            currentState.ExitState(this);
            //切换状态
            currentState = stateID == StateID.Default?defaultState:states.Find(s => s.stateID == stateID);
            //进入下一个状态
            currentState.EnterState(this);
        }
    }

标签:状态,enemy,void,FSM,状态机,Unity,public,currentState
From: https://www.cnblogs.com/MMMMrD/p/17832318.html

相关文章

  • Unity-场景的异步加载
    Unity-场景的异步加载为什么需要异步加载​ 在诸多大型游戏里,场景渲染精度都是动态的,随着场景与角色距离的增加,渲染精度也在递减,这样极大的减少了硬件性能的消耗。​ 但如果角色使用了某些传送技能,将自己传送到为渲染的地点,游戏可能就会因为需要瞬间渲染大量的场景而卡顿。此时......
  • Unity-Menu&场景切换
    Unity-Menu&场景切换开始界面1.要创建开始界面,首先要新建一个场景,用于添加游戏开始界面的内容2.新建按钮步骤:UI>画板>Button(按钮)>根据需要设置按钮3.给按钮添加代码,使得按下按钮就可以进入下一关/退出游戏(1)代码内容需要用到usingUnityEngine.SceneManagement的头文件......
  • Unity-敌人(Enemy)
    Unity-敌人(Enemy)引言​ 敌人是每个游戏中不可缺少的部分,设计得好的敌人可以给游戏增添很多乐趣,设计得差的则会非常影响我们的游戏体验。​ 经过这段时间的学习,我们已经接触了非常多的敌人代码的写法,但是就是没有系统的归类,导致每次写敌人,都要从头开始。现在是时候将他们进行一......
  • Unity-Light(含Unity2021-2d项目升级Urp渲染管线)
    Unity-Light(含Unity2021-2d项目升级Urp渲染管线)普通渲染管线(比较老旧的光效升级方式,已舍弃)​ 要使场景和角色拥有光效,那就得让他们先暗下来,给他们添加相应的材质场景材质的添加​ 选中需要添加材质的场景,在右侧框内的“材质”菜单中,选中Default-Diffuse材质人物材质的添加​......
  • Unity-单例模式
    Unity-单例模式前言​ 对于某些特殊的类,我们希望在整个程序的生命周期只创建一个该类的对象,或是希望在其他类没有持有该类的引用,就可以调用该类中的函数,我们就需要将这个类写成单例模式单例的简单实现publicclassTest(){ pubicabstractTestInstance;//创建程序中该......
  • Unity-射线
    Unity-射线前言​ 在游戏开发的过程中,许多功能的实现都需要物理检测,而发射射线是Unity中物理检测的通用方法。例如,我们需要检测玩家(Player)脚下是否是地面(图层为Ground),只需要从脚底发射一条射线,检测Player脚下GameObject的图层是否为Ground即可。​ 射线和物理检测何其重要,......
  • Unity-对象池 & 多对象池
    Unity-对象池&多对象池简介​ 在制作游戏的过程中,人物和boss的设计往往会有释放多个子弹的攻击方式。我们可以用直接创造子弹然后销毁的办法来实现这些技能的效果,但当子弹开始变多,游戏就会不断的消耗我们的内存。为了解决这个问题,开发者们就引入了状态机。普通对象池创建思......
  • Unity-协程
    Unity-协程协程的简单实现​ 一般的程序执行都是线性的,也就是必须一行一行的执行代码。​ 使用Unity提供的协程,就可以类似于开辟另一条线程,调整根据你所写的代码,调整下一行代码执行的时间。项目示例​ 下面的例子是一个U3DDemo中的代码,实现最简单的Enemy追击Player的......
  • Unity3D 如何用unity引擎然后用c#语言搭建自己的服务器
    Unity3D是一款强大的游戏开发引擎,可以用于创建各种类型的游戏。在游戏开发过程中,经常需要与服务器进行通信来实现一些功能,比如保存和加载游戏数据、实现多人游戏等。本文将介绍如何使用Unity引擎和C#语言搭建自己的服务器,并给出技术详解以及代码实现。对惹,这里有一个游戏开发交流......
  • Unity播放Hap格式视频 (大分辨下流畅播放以及帧同步解决方案)
    前言:之前对于项目上播放大分辨率视频(特别是大于4k分辨率的)常常会感觉相当的头疼,最开始使用的是Unity自带的VideoPlayer,发现效果并不理想,更换为AVPro后发现播放是流畅了但不能操作视频快进,只要一快进就会出现卡顿,最后偶然间发现了一款用于播放Hap格式视......