【Unity 笔记】有限状态机 FSM

【Unity】 有限状态机 FSM

游戏引擎:Unity
版本:2019.4.6f1 【2017版本以上均可】
编译平台:Visual Studio 2019

一、什么是FSM

FSM 全名 [Finite State Machine]。中文名 [有限状态机],又称 [有限状态自动机],简称 [状态机]

1.1 了解Animator组件

Unity为我们提供了Animator组件,便利开发者更加自主的实现更多动画的控制方式。
Unity Animator组件
参考:Unity Animator官方文档介绍

2.2 有限状态机的基础认识

释义举例
定义有限的多个状态在不同条件下,相互转换的流程控制系统
状态物体表现的行为状况行走、奔跑、自疗、射击等
条件状态改变的依据受攻击、中毒、休息等
状态转换表例如:中毒 - (吃药) ->健康
状态机管理所有状态,协调组织状态的迁移

二、为什么选择FSM

  由于游戏内容日益的多元化、复杂化,游戏角色的行为逻辑也不断的更新更变。从最初始的行走、奔跑、跳跃,增加了受伤、治疗、拾取等诸多新的行为。亦或是调整更边以往的行走方式,增加了不同生命状态下的行为表现。或是删除了一些效果欠佳的行为表现。致使了开发人员在复杂的动画状态机面前出现更新修改工作量增大。
  FSM 有限状态机,意在独立状态与条件之间的联系,使其之间互不影响,便利开发人员在应对此类问题,减少工作量,以及后期行为扩展、修改等开发中的便利化。

三、FSM的实现

3.0 二类一基类

二类 一基类解释
FSM Base
FSM State代表 具体[状态],隔离状态机与状态的变化
FSM Trigger代表 具体[条件],隔离状态机与条件的变化

3.1 状态类 FSMState

FSMState类主要完成以下工作

  • 存储条件的列表
  • 存储条件与状态之间的映射关系
  • 实现传入条件,返回指定状态的逻辑方法
  • 状态的进入,进行时,退出的抽象方法【服务于子类】
3.1.1 代码块
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace FSM
{
    public class FsmState<T>
    {   
        private List<FsmTrigger<T>> triggerList;  //条件列表     
        private Dictionary<string, string> map;   //策划配置的映射表(条件 --> 状态)

        [HideInInspector] public T Fsm;

        public FsmState()
        {
            triggerList = new List<FsmTrigger<T>>();
            map = new Dictionary<string, string>();
        }

        /// <summary>
        /// 添加映射
        /// </summary>
        /// <param name="triggerName"></param>
        /// <param name="stateName"></param>
        public void AddMap(string triggerName,string stateName)
        {
            map.Add(triggerName, stateName);

            Type type = Type.GetType("AI.FSM." + triggerName + "Trigger");
            FsmTrigger<T> trigger = Activator.CreateInstance(type) as FsmTrigger<T>;
            //为条件提供状态机引用
            trigger.Fsm = Fsm;
            triggerList.Add(trigger);
        }

        /// <summary>
        /// 检查条件:遍历条件,判断谁满足逻辑
        /// </summary>
        /// <returns></returns>
        public string Check()
        {
            for (int i = 0; i < triggerList.Count; i++)
            {
                //发现满足的条件
                if (triggerList[i].OnTriggerHandler())
                {                 
                    string triggerClassName = triggerList[i].GetType().Name;         //获取条件对象类名
                    string stateName = map[triggerClassName.Replace("Trigger","")];  //策划配置:NoHealth -程序类名:NoHealthTrigger
                    return stateName;  
                }
            }
            return null;
        }

        /// <summary>
        /// 当状态进入时逻辑
        /// </summary>
        public virtual void OnStateEnter() { }
        /// <summary>
        /// 当状态停留时逻辑
        /// </summary>
        public virtual void OnStateStay() { }
        /// <summary>
        /// 当状态离开时逻辑
        /// </summary>
        public virtual void OnStateExit() { } 
3.1.2 关于AddMap( )的说明

AddMap():负责添加 Trigger 与 State 之间的映射联系。

public void AddMap(string triggerName,string stateName)
{
    map.Add(triggerName, stateName);

    Type type = Type.GetType("AI.FSM." + triggerName + "Trigger");
    FsmTrigger<T> trigger = Activator.CreateInstance(type) as FsmTrigger<T>;
    //为条件提供状态机引用
    trigger.Fsm = Fsm;
    triggerList.Add(trigger);
}
  • mappublic FsmState(){}已分配存储空间
  • type指获取指定Trigger的触发器
  • Activator.CreateInstance()为创建实例对象,添加至triggerList;的条件列表中。

有关于Activator.CreateInstance()的解释,请参考
[作者]黑火石科技—C# Activator.CreateInstance()方法使用
[作者]W低小调W—Unity 关于Activator.CreateInstance使用

3.1.3 关于Check( )的说明

Check():检查条件,即遍历。如下图源码:

public string Check()
{
    for (int i = 0; i < triggerList.Count; i++)
    {
        if (triggerList[i].OnTriggerHandler())
        {
            string triggerClassName = triggerList[i].GetType().Name;
            string stateName = map[triggerClassName.Replace("Trigger","")];
            return stateName;
        }
    }
    return null;
}

其中关于下行代码内容的解释:

string stateName = map[triggerClassName.Replace("Trigger","")]

由于策划给予的条件命名可能并不严谨与适应开发人员的编程习惯。

例如:策划给予NoHealth的命名习惯,作为触发角色死亡条件,我们通常选择命名NoHealthTrigger来直接表明其为死亡的触发条件,在查询状态时,我们会切换为正常命名的状态类,当然若不嫌麻烦,可自行调整资源命名方式,忽略.Replace()这一操作。

3.1.4 选择Virtual而非Abstract的原因
// 当状态进入时逻辑
public virtual void OnStateEnter() { }
// 当状态停留时逻辑
public virtual void OnStateStay() { }
// 当状态离开时逻辑
public virtual void OnStateExit() { } 

  我们在FSMState添加OnStateEnter/OnStateStay/OnStateExit是为了利于应对动画切换的多元化。因为不清楚各状态之间以何种方式、条件来回切换。选择了 virtual 声明方法。

virtual与abstract区别

  • virtual的声明既可以出现在抽象类中,也能在非抽象类中出现,默认可以实现,也可以不用实现。
  • abstract的声明,严格于抽象类中,且子类继承中必须实现abstract方法。

  有关详细的说明,请参考 [作者]编码者频道—C#中的interface、virtual和abstract

3.2 条件类 FSMTrigger

3.2.1 代码块
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace FSM
{
    /// <summary>
    /// 条件基类:代表具体[条件],隔离状态与条件的变化。
    /// </summary>
    public abstract class FsmTrigger<T>
    {
        //子类可以直接使用的成员
        public T Fsm;
		//条件处理 
        public abstract bool OnTriggerHandler();
    }
}
3.2.2 关于选择abstract的说明

作为必须判断条件是否处理,以及何种方式处理。即:[不清楚如何处理方法,选择 Abstract 抽象方法]


3.3 状态基类 FSMBase

FSMBase作为FSM的基类,不同于前两类,将绑定于GameObject对象上。

3.3.1 代码块
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace FSM
{
    /// <summary>
    /// 有限状态机基类
    /// </summary>
    public class FsmBase<T> : MonoBehaviour where T:class
    {
        private List<FsmState<T>> stateList;  //存储状态列表
        private FsmState<T> currentState;  //当前状态赋值
        private FsmState<T> defaultState;  //默认状态赋值
        [Tooltip("配置文件名称")] public string configName;
        [Tooltip("默认状态")] public string defaultStateName;
        
        protected void Awake()
        {
            ConfigFSM();
            InitDefaultState();
        }
                
        protected void Update()
        {
            currentState.OnStateStay();
            string nextStateName = currentState.Check();
            if (nextStateName != null)
            {
                ChangeState(nextStateName);
            }
        }


        private void ConfigFSM()
        {
            //读取配置文件
            //形成数据结构
            var map = new FSMConfigReader(configName).map;
            //配置有限状态机
            stateList = new List<FsmState<T>>();
            foreach (string mainKey in map.Keys)
            {
                Type type = Type.GetType("AI.FSM." + mainKey + "State");
                FsmState<T> state = Activator.CreateInstance(type) as FsmState<T>;
                state.Fsm = this  as T ;
                foreach (var subMap in map[mainKey])
                { 
                    state.AddMap(subMap.Key, subMap.Value);
                }
                stateList.Add(state);
            }
        }
        
        //初始化默认状态
        private void InitDefaultState()
        {
            //在状态列表中查找默认状态
            defaultState = stateList.Find(e => e.GetType().Name == defaultStateName + "State");
            currentState = defaultState;
            currentState.OnStateEnter();
        }

		//状态切换
        private void ChangeState(string stateName)
        {           
            currentState.OnStateExit();  //离开之前状态
            //切换
            if (stateName == "Default")
                currentState = defaultState;
            else
               currentState = stateList.Find(e => e.GetType().Name == stateName+"State");
            
            currentState.OnStateEnter();  //进入当前状态
        }
    }
}
3.3.2 关于InitDefaultState( )的说明

InitDefaultState():初始化默认状态。

private void InitDefaultState()
{
    //在状态列表中查找默认状态
    defaultState = stateList.Find(e => e.GetType().Name == defaultStateName + "State");
    currentState = defaultState;
    currentState.OnStateEnter();
}

  我们在开头public string defaultStateName;声明了一个尚未填写的defaultStateName,它可以是站立idle,也可以是跳跃jump等。需要的是传入一个动画命名。使得我们以更灵活的设置默认动画状态。其中一行:

defaultState = stateList.Find(e => e.GetType().Name == defaultStateName + "State");
  • 从状态列表中查找所需求的默认状态,我们使用这行代码是为了避免在状态列表获取动画信息前,我们查找不到对应的动画状态。即使确实存在,但我们的查找先于状态列表添加动画状态。在确认找到指定动画后,进入对应的动画状态。

补充:FSMConfigReader FSM状态机配置获取

致谢水管工人玛丽奥 纠正提醒

public class FSMConfigReader
{
    //外层字典:key 状态名称   value 字典
    //内层字典:key 条件名称   value 状态名称
    public Dictionary<string, Dictionary<string, string>> map;
    private string mainKey;

    public FSMConfigReader(string fileName)
    {
        map = new Dictionary<string, Dictionary<string, string>>();
        string content = ConfigurationReader.GetConfigFile(fileName);
        ConfigurationReader.ReadConfigFile(content, LineHandler);
    }

    private void LineHandler(string line)
    {
        line = line.Trim(); //剔除字符串空格
        if (line == "") return;
        if (line.StartsWith("["))
        {
            //如果该行以[开始,表示状态  [Idle]
            mainKey = line.Substring(1, line.Length - 2);
            map.Add(mainKey, new Dictionary<string, string>());
        }
        else
        {
            //表示条件  NoHealth>Dead
            string[] keyValue = line.Split('>');
            map[mainKey].Add(keyValue[0], keyValue[1]);
        } 
    }
}

LineHandler():笔者印象中是对命名文字的处理,指某一状态到另一状态的过程
例如一些动作的变化: Health -> Hurt 这一过程通过字符分割得到状态的映射进行注册。
譬如:获取 [Idle] 那么以此的映射有 idle -> run / idle -> walk 等

  • 0
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值