Unity设计模式(1) FSM有限状态机

有限状态机(FSM)的核心原理是基于状态和状态之间的转换。它可以用来描述系统的行为和流程,尤其是在处理离散事件和复杂逻辑时使代码有较强的可维护性及健壮性。

为什么使用状态机

在一开始学习程序的时候,你可能会陷入一种误区,就是想把所有事情全在一个脚本里给干了,可是当你需要在玩家控制器里拓展一些状态的时候,你大概率需要靠复杂嵌套的一连串的if选择结构来修补之前的代码,也有可能为了加一种状态,导致以前写的代码都要修改判定。改着改着,不需一段时间,你就会发现自己已经看不懂以前写的代码了,这是及其不利于代码的健壮性和可维护性的,所以这种思路需要及时扭转。

所以我们引入状态机的设计模式,状态机模式具有某时刻仅存在单状态的特征,所以当你要拓展状态时,仅需找到与新状态存在直接联系的状态类中修改,至于那些八竿子打不着的状态就无需考虑修改了,便于开发者进行拓展维护,一种状态仅与有限个状态之间有切换联系,其实你可能已经见过了一种典型的状态机:动画状态机-Animator。

我们可以在Animator里的动画状态上添加动画事件脚本,这里的脚本不同于在Project窗口内创建的C#脚本

可以看到状态机脚本默认继承了一个叫做StateMachineBehaviour的基类

继续导航到基类的定义

这就是Animator状态机的搭建思路,我们先不必太过深入Animator这方面的知识。


一.基本思路

1. 状态(State)

状态机的基础是状态。一个状态表示系统的某个特定条件或模式。在任何给定时刻,系统总是处于某一个状态。状态可以是系统的某个阶段、条件或模式。

  • 示例:在一个简单的电梯控制系统中,状态可以包括“停止”、“向上移动”、“向下移动”。

2. 事件(Event)

事件是引起状态改变的触发条件。事件可以是外部输入或内部条件变化。当系统接收到一个事件时,它可能会根据当前状态和事件的组合转移到另一个状态。

  • 示例:电梯系统中的事件可以是“呼叫电梯”、“按下楼层按钮”等。

3. 状态转换(State Transition)

状态转换是指在系统的状态之间的变化。每当事件发生时,系统会根据当前的状态和事件决定是否需要转换到另一个状态。这种转换是通过状态转换规则来实现的。

  • 示例:当电梯处于“停止”状态,并且接收到“呼叫电梯”事件时,电梯状态可以转换为“向上移动”或“向下移动”。

在Unity里,我们经常选择将一种状态作为一个类,通过类与类的切换来达到切换不同状态的目的

4. 状态机的组成

一个有限状态机通常由以下几个部分组成:

  • 状态集合:所有可能的状态。
  • 事件集合:所有可能的事件。
  • 转换规则:定义在特定事件发生时如何从一个状态转换到另一个状态。
  • 初始状态:状态机开始时的状态。
  • 终止状态(可选):状态机完成工作后可能进入的状态(不是所有的状态机都有终止状态)。

5. 状态机的类型

状态机有几种不同的类型:

  • 确定性有限状态机(DFA, Deterministic Finite Automaton):对于每一个状态和事件的组合,只有一个确定的下一状态。
  • 非确定性有限状态机(NFA, Nondeterministic Finite Automaton):对于一个状态和事件的组合,可能存在多个可能的下一状态。
  • 一般所有的状态都是策划提前能想到的所有状态,是有限的

6. 应用场景

有限状态机广泛应用于各种场景:

  • 协议设计:用于网络协议的状态管理。
  • 用户界面:用于不同界面状态之间的切换和管理
  • 角色控制:复杂的玩家控制系统和批量的AI行为控制系统
  • 游戏流程:有时整个游戏的流程也可以是一个巨大的状态机

二.基本有限状态机的实现

需要注意的是,状态机的结构不是确定的,有简有繁,具备上面提到的状态切换特征的结构都可以被称为状态机的设计模式,这里给出我个人的一种比较清晰的实现方式。

注:具体需求都是根据实际情况来的,有时,简单用Switch来控制的状态机的效果也不差。但是加入你想在游戏中设计一种拥有复杂的boss,比如血量达到70%释放小技能并且进入狂暴,等一系列复杂的机制,你就需要一种更结构化的状态机搭建方案,这种方案将一个类作为一种状态,所以不可避免的会出现脚本数量增加的结果。当然,你也可以进一步优化,将所有状态类的切换条件进行封装,暴露更简单的接口。

一.结构需求:

1.StateMachine脚本(无需继承MonoBehaviour):

状态机脚本  此脚本包含一个当前状态类变量(currentState) 和 两个成员方法  

InitializeState():进行状态的初始化  

ChangeState():从当前状态切换到新的状态

2.State脚本(无需继承MonoBehaviour):

状态类基类,Controoler的所有状态类脚本都会继承此脚本

含有一个构造函数和三种状态函数

OnEnter():进入此状态时调用

OnUpdate():处于此状态时每帧调用

OnExit():退出此状态时调用

为了使这三种函数能够交给状态子类去灵活重写,我使用virtul关键字来修饰

在每种状态子类的Update()函数中都会写从当前状态切换的条件,每帧检测

3.Controller脚本(一般情况下需继承MonoBehaviour):

控制器脚本

一个Controller具备一架有限状态机,和 自身具备的所有 状态对象 

二.运作原理分析:

游戏开始运行,controller先通过stateMachine先进行初始化状态的设置,controller在自身Update中运行stateMachine的currentState,从而进入一种state内部的Onupdate()里去执行当前状态的行为逻辑,直到满足切换状态的条件,进行状态的切换

三.实际运用

假设现在我们要使用有限状态机的思想来做一个敌人的AI

1.需求分析

EnemyStateMachine:Enemy状态机脚本

EnemyState:所有Enemy状态类的基类

Enmy:实际挂载的AI控制脚本

四.实现源码

1.Enemy类(玩家控制器脚本)

一般是根据项目需求先列出所有状态

using UnityEngine;

public class Enemy : MonoBehaviour
{
    public EnemyStateMachine stateMachine = new EnemyStateMachine();
    public EnemyIdleState idleState;
    public EnemyRushState walkState;
    public EnemyAttackState attackState;
    public EnemyPatrolState patrolState;
    public Animator anim;
    private void Awake()
    {
        idleState = new EnemyIdleState(this, stateMachine, "idle", anim);
        walkState = new EnemyRushState(this, stateMachine, "rush", anim);
        attackState = new EnemyAttackState(this, stateMachine, "attack", anim);
        patrolState = new EnemyPatrolState(this, stateMachine, "patrol", anim);
        stateMachine.InitializeState(idleState);
    }
    void Update()
    {
        stateMachine.currentState.OnUpdate();
    }
}

这里的anim我简单做了一个Enemy的AnimatorController,这里的动画名称与AnimatorController参数列表里的参数名称需要对应

2.EnemyStateMachine(状态机类脚本)

public class EnemyStateMachine
{
    public EnemyState currentState;
    /// <summary>
    /// 状态初始化
    /// </summary>
    /// <param name="initState">初始化状态</param>
    public void InitializeState(EnemyState initState)
    {
        currentState = initState;
        currentState.OnEnter();
    }

    /// <summary>
    /// 状态切换
    /// </summary>
    /// <param name="newState">新状态</param>
    public void ChangState(EnemyState newState)
    {
        currentState.OnExit();
        currentState = newState;
        currentState.OnEnter();
    }
}

3.EnemyState类(所有状态类的基类脚本)

using UnityEngine;

public class EnemyState
{
    protected Enemy enemy;
    //控制器对象的动画名称,先假设Enemy有动画
    protected string animBoolName;
    //控制器对象的Animator,先假设Enemy有动画
    protected Animator anim;

    protected EnemyStateMachine stateMachine;

    /// <summary>
    /// 此构造函数用于状态对象的初始化
    /// </summary>
    /// <param name="enemy">控制器对象</param>
    /// <param name="stateMachine">控制器对象的状态机</param>
    /// <param name="animBoolName">控制器对象的动画名称</param>
    public EnemyState(Enemy enemy, EnemyStateMachine stateMachine, string animBoolName, Animator anim)
    {
        this.enemy = enemy;
        this.animBoolName = animBoolName;
        this.stateMachine = stateMachine;
        this.anim = anim;
    }

    /// <summary>
    /// 进入本状态时调用
    /// </summary>
    public virtual void OnEnter()
    {
        //如果需要再进入状态时就播放动画,就在此处调用
        SetBoolAnimPlay(animBoolName);
    }

    /// <summary>
    /// 本状态时刻更新时调用
    /// </summary>
    public virtual void OnUpdate() { }

    /// <summary>
    /// 退出本状态时调用
    /// </summary>
    public virtual void OnExit() { }

    //个性化函数,不必须
    public virtual void SetBoolAnimPlay(string animName)
    {
        anim.SetBool(animName, true);
    }
}

4.EnemyState子类

举例Enemy的几种状态类,里面写了简单的测试代码

按下1,进入patrol状态                                     按下4,进入Idle状态

按下3,进入Idle状态                                         按下2,进入RushState 

五.编辑器内运行结果

可以看到我们实现了目标

三.泛型有限状态机的实现

上面知识一种状态机的具体应用例子,假如我们需要很多种状态机,可以预见的是,这些状态机的状态基类都是个性化的,但状态机的代码基本是一致的,下面我利用泛型制作了一种FSM泛型框架,适用于大部分状态机的需求情景

先把源码发一下,结尾有简单的分析

1.FSM_StateMachine类(状态机基类)

public class FSM_StateMachine<T>
{
    public FSM_States<T> currentState;
    public virtual void InitializeState(FSM_States<T> initState)
    {
        currentState = initState;
    }

    public virtual void ChangeState(FSM_States<T> newState)
    {
        currentState.OnExit();
        currentState = newState;
        currentState.OnExit();
    }
}

2.FSM_State类(状态基类)

public class FSM_States<T>
{
    protected FSM_StateMachine<T> stateMachine;

    public virtual void OnEnter() { }
    public virtual void OnUpdate() { }
    public virtual void OnExit() { }
}

现在我们试着在玩家控制器中再实现一下我们的状态机

3.Player类(玩家控制器类)

using UnityEngine;

public class Player : MonoBehaviour
{
    public PlayerStateMachine stateMachine { get; private set; }
    public PlayerIdleState idleState { get; private set; }
    public PlayerWalkState walkState { get; private set; }
    public PlayerRushState rushState { get; private set; }
    public PlayerJumpState jumpState { get; private set; }

    private void Awake()
    {
        stateMachine = new PlayerStateMachine();
        idleState = new PlayerIdleState(this, stateMachine, "idle");
        walkState = new PlayerWalkState(this, stateMachine, "walk");
        rushState = new PlayerRushState(this, stateMachine, "rush");
        jumpState = new PlayerJumpState(this, stateMachine, "jump");
    }
    private void Update()
    {
        stateMachine.currentState.OnUpdate();
    }
}

4.PlayerStateMachine类(Player类型的状态机脚本)

public class PlayerStateMachine : FSM_StateMachine<Player>
{
    public override void ChangeState(FSM_States<Player> newState)
    {
        base.ChangeState(newState);
    }

    public override void InitializeState(FSM_States<Player> initState)
    {
        base.InitializeState(initState);
    }
}

5.PlayerState类(Player的有限状态的基类)

public class PlayerStates : FSM_States<Player>
{
    protected Player player;
    protected string animBoolName;
    public PlayerStates(Player _player, PlayerStateMachine _stateMachine, string _animBoolName)
    {
        player = _player;
        stateMachine = _stateMachine;
        animBoolName = _animBoolName;

    }

    public override void OnEnter()
    {
        base.OnEnter();
    }

    public override void OnExit()
    {
        base.OnExit();
    }

    public override void OnUpdate()
    {
        base.OnUpdate();
    }
}

6.PlayerState的所有子类

这里列举玩家的四种状态Idle,Rush,Jump,Attack

具体的逻辑这里就先不写了,个人的方案肯定不是最完善的,读者可以自行拓展修改,当然如果有更优美的写法,欢迎大佬们在评论区赐教!@~@

这里分析一下相比之下不太容易理解的地方

1.PlayerState的作用?

因为Player的有限状态内,需要调用的控制器是一个Player类型的实例对象,即代码里的player,所以继承FSM_State时将其中的T标记为Player属性,在构造函数里,我们就可以使用Player类型来初始化出所有属于玩家的状态类型,  含义是说明本状态是独属于Player的状态

2.PlayerStateMachine的作用?

在PlayerStateMachine脚本里为FSM_StateMachine基类的T 赋值为Player类型,使自身的CurrentState标记为Player的状态,同时重写InitializeState()和ChangState()内的参数类型为Player,含义是说明自身(PlayerStateMachine类型的状态机)只会接收具有Player属性状态的参数

本篇丸

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值