一个unity有限状态机框架
概述
有限状态机又称有限状态自动机,简称状态机。
- 定义:有限的多个状态在不同条件下相互转换的流程控制系统。
- 状态:物体表现出来的状况,多指行为。比如奔跑,攻击这些都属于状态。
- 条件:状态改变的依据。比如按下攻击键,按下奔跑键。
- 状态转换表:配置角色状态和条件的一张表。
- 状态机:管理所有的状态,协调组织状态的迁移。
有限状态机的适用范围:敌人AI, 玩家托管,游戏/软件整体框架,可交互教程,所有的流程控制系统
框架构建思路:
首先我们构建一个状态表如下:
当前状态 | 条件 | 目标状态 |
Idle | 按下移动键 | Run |
按下跳跃键 | Jump | |
按下技能键 | Attack | |
按下翻滚键 | Scroll | |
HP为空 | Dead | |
Run | 按下跳跃键 | Jump |
按下技能键 | Attack | |
按下翻滚键 | Scroll | |
什么都不做 | Idle | |
HP为空 | Dead |
我们发现一个角色会有多个状态,一个状态会有多个转换条件对应的状态。我们发现状态和条件均为变化点,所以我们要将状态提成不同的类,条件提成不同的类来分离变化点。
每一个状态会有若干个个条件与状态的对应,这种对应我们可以使用字典结构来实现,此字典由状态机配置时调用。这样分析那么也就是说一个状态中会有多个转换用的条件,我们还需要一个条件数组来存储条件的实例对象。
我们还需要一个类来管理一个角色上所有的状态,我们将这个类命名为状态机。
但是这里就会存在几个问题。
- 条件的实例对象从哪里来?
状态机作为管理角色状态的类,可以创建一个方法在状态机初始化的时候为这个对象的所有状态赋值初始化包括条件对象。当然我们也可以使用反射来创建条件对象。当状态机为角色状态初始化并配置状态与条件的映射关系时,由状态动态创建条件对象并添加到条件数组。
- 如何实现自动切换状态?
状态机中需要使用帧更新来每帧检查当前状态的条件表是否有满足的条件,如果满足就切换。
代码&注释
本人叙述能力实在有限下面是代码实现,包含注释详解:
FSMTriggerID:条件ID枚举
namespace CharacterControlSystem.CharacterFSM
{
public enum FSMTriggerID
{
JumpToIdleDetection,
MoveToIdleDetection,
IdleToMoveDetection,
IdleToJumpDetection,
}
}
FSMStateID:状态ID枚举
namespace CharacterControlSystem.CharacterFSM
{
public enum FSMStateID
{
Default,
Idle,
Run,
Jump,
}
}
FSMTrigger:条件基类
using System;
using UnityEngine;
namespace CharacterControlSystem.CharacterFSM
{
/// <summary>
/// 有限状态机中的条件类基类
/// </summary>
public abstract class FSMTrigger
{
/// <summary>
/// 条件ID
/// </summary>
public FSMTriggerID TriggerId { get; set; }
public FSMTrigger()
{
InitId();
}
/// <summary>
/// 子类必须实现此方法来为TriggerId赋值
/// 此方法由构造函数调用
/// </summary>
protected abstract void InitId();
/// <summary>
/// 子类必须实现此方法来处理条件逻辑,
/// 状态机通过检查所有状态的所有条件来判断需要切换至什么状态
/// 检测方法内部来判断此方法
/// </summary>
public abstract bool HandleTrigger(GameObject thisGameObject);
}
}
FSMState:状态基类
using System;
using System.Collections.Generic;
using UnityEngine;
namespace CharacterControlSystem.CharacterFSM
{
public abstract class FSMState
{
/// <summary>
/// 状态ID
/// </summary>
public FSMStateID StateId { get; set; }
/// <summary>
/// 条件状态映射表
/// </summary>
private Dictionary<FSMTriggerID, FSMStateID> _map;
/// <summary>
/// 条件表
/// </summary>
private List<FSMTrigger> _triggers;
/// <summary>
/// 进入状态时调用的委托
/// </summary>
private Action enterStateHandler;
/// <summary>
/// 状态中持续调用的委托
/// </summary>
private Action stayStateHandler;
/// <summary>
/// 退出状态时调用的委托
/// </summary>
private Action exitStateHandler;
/// <summary>
/// 当前游戏对象
/// </summary>
protected GameObject thisGameObject;
/// <summary>
/// 用作初始化的构造函数
/// </summary>
public FSMState(GameObject thisGameObject)
{
this.thisGameObject = thisGameObject;
_map = new Dictionary<FSMTriggerID, FSMStateID>();
_triggers = new List<FSMTrigger>();
InitState();
}
protected abstract void InitState();
/// <summary>
/// 添加映射关系,由状态机调用
/// </summary>
/// <param name="triggerId">条件ID</param>
/// <param name="stateId">状态ID</param>
public void AddMap(FSMTriggerID triggerId, FSMStateID stateId)
{
_map.Add(triggerId, stateId);
CreateTrigger(triggerId);
}
/// <summary>
/// 动态创建条件对象并添加到条件列表中
/// </summary>
/// <param name="triggerId">条件ID</param>
private void CreateTrigger(FSMTriggerID triggerId)
{
Type triggerType = Type.GetType("CharacterControlSystem.CharacterFSM.SubFSM.Trigger." +triggerId+ "Trigger");
if (triggerType != null)
{
FSMTrigger trigger = Activator.CreateInstance(triggerType) as FSMTrigger;
_triggers.Add(trigger);
}
}
/// <summary>
/// 状态机通过此方法来检测是否可以切换到某个状态
/// </summary>
/// <param name="fsmBase"></param>
public void Reason(FSMBase fsmBase)
{
for (int i = 0; i < _triggers.Count; i++)
{
//如果发现条件满足的条件
if (_triggers[i].HandleTrigger(fsmBase.gameObject))
{
//返回给状态机
fsmBase.ChangeState(_map[_triggers[i].TriggerId]);
return;
}
}
}
/// <summary>
/// 为进入状态委托添加回调函数
/// </summary>
/// <param name="func"></param>
public void AddEnterStateListener(Action func)
{
enterStateHandler += func;
}
/// <summary>
/// 为持续调用委托添加回调函数
/// </summary>
/// <param name="func"></param>
public void AddStayStateListener(Action func)
{
stayStateHandler += func;
}
/// <summary>
/// 为退出状态委托添加回调函数
/// </summary>
/// <param name="func"></param>
public void AddExitStateListener(Action func)
{
exitStateHandler += func;
}
/// <summary>
/// 主要用于改变角色当前状态
/// </summary>
public virtual void EnterState()
{
enterStateHandler?.Invoke();
}
/// <summary>
/// 用于角色在状态中的一些持续操作
/// </summary>
public virtual void StayState()
{
stayStateHandler?.Invoke();
}
/// <summary>
/// 主要用于退出状态时的一些操作
/// </summary>
public virtual void ExitState()
{
exitStateHandler?.Invoke();
}
}
}
FSMBase:状态机基类
using System.Collections.Generic;
using CsharpBaseModule.PublicMono;
using UnityEngine;
namespace CharacterControlSystem.CharacterFSM
{
/// <summary>
/// 状态机,表示一个角色所拥有的所有的状态
/// </summary>
public abstract class FSMBase : MonoBehaviour
{
/// <summary>
/// 状态列表
/// </summary>
protected Dictionary<FSMStateID,FSMState> _states;
/// <summary>
/// 默认状态ID
/// </summary>
[Tooltip("请选择默认状态")]
public FSMStateID defaultStateId;
/// <summary>
/// 默认状态
/// </summary>
protected FSMState _defaultState;
/// <summary>
/// 当前状态
/// </summary>
public FSMState currentState;
/// <summary>
/// 这里我将Update提到了一个公共Mono中,也可以不提
/// </summary>
protected virtual void Awake()
{
ConfigFSM();
InitDefaultState();
MonoManager.GetInstance.AddUpdateListener(BaseUpdate);
}
/// <summary>
/// 配置状态机,基类只初始化一下就好,具体配置留给子类状态机实现
/// </summary>
protected virtual void ConfigFSM()
{
_states = new Dictionary<FSMStateID, FSMState>();
}
/// <summary>
/// 初始化状态,会将默认状态赋给当前状态变量
/// </summary>
protected virtual void InitDefaultState()
{
_defaultState = _states[defaultStateId];
currentState = _defaultState;
currentState.EnterState();
}
/// <summary>
/// 每帧调用当前状态逻辑和检测是否满足其他状态的切换条件
/// </summary>
protected virtual void BaseUpdate()
{
currentState.Reason(this);
currentState.StayState();
}
/// <summary>
/// 改变当前状态,改变之前先调用条件退出的方法,之后调用新条件进入的方法
/// </summary>
/// <param name="stateId">状态ID</param>
public void ChangeState(FSMStateID stateId)
{
currentState.ExitState();
currentState = stateId == FSMStateID.Default
? _defaultState
: _states[stateId];
currentState.EnterState();
}
/// <summary>
/// 添加状态,在状态机配置中进行调用
/// </summary>
/// <param name="stateId">状态ID</param>
/// <param name="state">状态,配置时new一个</param>
/// <returns></returns>
protected FSMState AddState(FSMStateID stateId, FSMState state)
{
_states.Add(stateId, state);
return state;
}
/// <summary>
/// 寻找相应的状态
/// </summary>
/// <param name="stateId">状态ID</param>
/// <returns></returns>
protected FSMState FindState(FSMStateID stateId)
{
if (_states.ContainsKey(stateId))
{
return _states[stateId];
}
return null;
}
}
}