【Unity】 有限状态机 FSM
游戏引擎:Unity
版本:2019.4.6f1 【2017版本以上均可】
编译平台:Visual Studio 2019
一、什么是FSM
FSM 全名 [Finite State Machine]。中文名 [有限状态机],又称 [有限状态自动机],简称 [状态机]。
1.1 了解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);
}
map
在public 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 等