前言
在游戏开发中,有限状态机(Finite State Machine,FSM)是一种强大的设计模式,用于描述对象在不同状态下的行为以及状态之间的转换,对于有限的状态,每个状态有自己独立的实现逻辑和过渡逻辑,每个状态可以切换至零-多个状态。通过描述对象在不同状态下的行为以及状态之间的转换,为游戏开发者提供了一种清晰而有效的方式来管理复杂的游戏逻辑。
一、FSM有限状态机简介
有限状态机是一种数学模型,用于描述一个系统在不同状态之间的转换。在游戏开发中,这个系统可以是任何具有多种行为的实体,比如角色、敌人、NPC等。每个状态代表一个特定的行为或状态,而状态之间的转换则取决于一些条件或触发事件。
FSM的基本元素
一个典型的FSM包含以下几个基本元素:
1. 状态(State)
状态是FSM的基本单元,代表对象在某一时刻的行为。在游戏中,一个状态可以对应角色的站立、行走、攻击等动作。
2. 转换(Transition)
转换定义了状态之间的关系,描述了在何种条件下从一个状态切换到另一个状态。条件可以是一些变量的取值、用户输入、时间等。
3. 动作(Action)
动作是与状态关联的具体行为,包括但不限于播放动画、改变属性、触发事件等。一个状态可能有多个关联的动作。
4. 状态机控制器(Controller)
状态机控制器是FSM的管理者,负责监控当前状态、添加状态、过渡状态。
二、应用场景
如下图:
对于传统的AI在空闲、巡逻、追逐、攻击等状态的切换,我们需要这样去实现:
if (空闲条件)
{
//进入空闲状态
}
else if (巡逻条件)
{
//进入巡逻状态
}
else if (追逐条件)
{
//进入追逐状态
}
else if (攻击条件)
{
//进入攻击状态
}
这样写需要频繁使用if else 或者switch语句,这样不仅代码的可读性和维护性差,还会导致每个状态耦合在一起,违背了高内聚低耦合的设计思想。
那么状态机是如何实现如上图几种状态之间的逻辑呢?
首先,我们需要定义一个状态的抽象类或者接口,也就是状态基类。里面需要包含三个核心方法,这里我使用抽象类来实现。
/// <summary>
/// 状态进入时执行
/// </summary>
public abstract void OnEnter();
/// <summary>
/// 每一帧调用
/// </summary>
public abstract void OnUpdate();
/// <summary>
/// 状态退出时执行
/// </summary>
public abstract void OnExit();
这三个方法分别表示一个状态在进入、执行中、退出,当某个状态进入时,将会调用一次OnEnter方法,可以用于处理该状态的初始化任务,当这个状态退出时调用一次OnExit方法,适合用于处理一些资源释放的操作,而OnUpdate方法是状态在执行的过程中每一帧都会调用,适合处理一些需要每帧做判断的逻辑。
然后我们还需要一个状态枚举,里面用于列举所需要的状态:
public enum StateType
{
IDLE = 0,
PATROL,
CHASE,
ATTACK
}
完整的状态基类如下:
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public abstract class StateBase
{
protected StateType _state;
protected Action<StateType> Transition;
protected Animator _animator;
protected Transform _target;
public StateType State
{
get => _state;
}
//防止外部实例化
public StateBase(Animator animator, Transform target)
{
_animator = animator;
_target = target;
}
/// <summary>
/// 状态进入时执行
/// </summary>
public abstract void OnEnter();
/// <summary>
/// 每一帧调用
/// </summary>
public abstract void OnUpdate();
/// <summary>
/// 状态退出时执行
/// </summary>
public abstract void OnExit();
/// <summary>
/// 设置状态过渡的回调
/// </summary>
/// <param name="action"></param>
public void SetTransitionAction(Action<StateType> action)
{
Transition = action;
}
}
public enum StateType
{
IDLE = 0,
PATROL,
CHASE,
ATTACK
}
我们还需要一个用于管理状态的状态机管理类:
用于保存状态、添加状态、设置默认状态、切换状态等的管理。
状态机管理类代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class StateMachineSystem
{
private StateBase _currentState;
private Dictionary<StateType, StateBase> _allStates;
public StateBase CurrentState
{
get => _currentState;
}
public StateMachineSystem()
{
_allStates = new Dictionary<StateType, StateBase>();
}
/// <summary>
/// 构造函数设置默认状态
/// </summary>
/// <param name="stateType"></param>
/// <param name="defaultState"></param>
public StateMachineSystem(StateType stateType, StateBase defaultState)
{
_allStates = new Dictionary<StateType, StateBase>();
AddState(stateType,defaultState);
TransitionState(stateType);
}
public void OnUpdate()
{
_currentState.OnUpdate();
}
/// <summary>
/// 添加状态
/// </summary>
/// <param name="stateType"></param>
/// <param name="newState"></param>
public void AddState(StateType stateType, StateBase newState)
{
if (_allStates.ContainsKey(stateType)) return;
_allStates.Add(stateType,newState);
_allStates[stateType].SetTransitionAction(TransitionState);
}
/// <summary>
/// 切换状态
/// </summary>
/// <param name="stateType"></param>
public void TransitionState(StateType stateType)
{
if (_currentState == _allStates[stateType]) return;
if (!_allStates.ContainsKey(stateType))
{
Debug.LogErrorFormat("{0} does not exist!",stateType.ToString());
return;
}
_currentState?.OnExit();
_currentState = _allStates[stateType];
_currentState?.OnEnter();
}
}
然后分别创建空闲、巡逻、追逐、攻击等状态类:
空闲状态:
public class IdleState : StateBase
{
private float time = 0f;
public IdleState(Animator animator, Transform target) : base(animator, target)
{
_state = StateType.IDLE;
}
public override void OnEnter()
{
Debug.Log("进入Idle状态");
}
public override void OnUpdate()
{
Debug.Log("正在执行Idle状态");
time += Time.deltaTime;
if (time >= 2f)
{
Transition(StateType.PATROL);
time = 0f;
}
}
public override void OnExit()
{
Debug.Log("退出Idle状态");
}
}
巡逻状态:
public class PatrolState : StateBase
{
private float time = 0f;
public PatrolState(Animator animator, Transform target) : base(animator, target)
{
_state = StateType.PATROL;
}
public override void OnEnter()
{
Debug.Log("进入巡逻状态");
}
public override void OnUpdate()
{
Debug.Log("正在执行巡逻状态");
time += Time.deltaTime;
if (time >= 5f)
{
Debug.Log("发现敌人!");
Transition(StateType.CHASE);
time = 0f;
}
}
public override void OnExit()
{
Debug.Log("退出巡逻状态");
}
}
追逐状态:
public class ChaseState : StateBase
{
private float time = 0f;
public ChaseState(Animator animator, Transform target) : base(animator, target)
{
_state = StateType.CHASE;
}
public override void OnEnter()
{
Debug.Log("进入追逐状态");
}
public override void OnUpdate()
{
Debug.Log("正在执行追逐状态");
time += Time.deltaTime;
if (time > 5f)
{
Debug.Log("敌人已到攻击范围");
Transition(StateType.ATTACK);
time = 0f;
}
}
public override void OnExit()
{
Debug.Log("退出追逐状态");
}
}
攻击状态:
public class AttackState : StateBase
{
public AttackState(Animator animator, Transform target) : base(animator, target)
{
_state = StateType.ATTACK;
}
public override void OnEnter()
{
Debug.Log("进入攻击状态");
}
public override void OnUpdate()
{
Debug.Log("正在执行攻击状态");
}
public override void OnExit()
{
Debug.Log("退出攻击状态");
}
}
定义一个Enemy测试类来测试:
public class Enemy : MonoBehaviour
{
private StateMachineSystem _stateMachine;
private Animator _animator;
private void Awake()
{
_animator = GetComponent<Animator>();
_stateMachine = new StateMachineSystem(StateType.IDLE, new IdleState(_animator, transform));
}
private void Start()
{
_stateMachine.AddState(StateType.PATROL,new PatrolState(_animator,transform));
_stateMachine.AddState(StateType.CHASE,new ChaseState(_animator,transform));
_stateMachine.AddState(StateType.ATTACK,new AttackState(_animator,transform));
}
private void Update()
{
_stateMachine.OnUpdate();
}
}
通过上面的代码可以发现,这样每个状态就分离开了,我们不需要关心其它状态的逻辑,只需要设置一个满足条件去切换对应的状态,每个状态只需要关心自己的实现,从而实现高内聚低耦合。
三、总结
当然上面的代码框架是最简单的FSM有限状态机的实现,本意只为理解其基本原理,大家可以发现虽然这样写进行了状态分离,但是每个状态还是需要在OnUpdate里面去做if esle的判断,这样写灵活度还是不够高,我们其实还可以将条件单独提取出来,再定义一个参数类,做成类似于Animator Controller那种状态过渡的形式,这样让状态机更灵活。不过这样写需要增加蛮多代码量,本文就不再过多阐述,大家可以根据这个思路自行扩展。