在Unity中实现有限状态机

在Unity中实现有限状态机

概要

本文将介绍Unity开发中的有限状态机,给出对应的实现代码。

背景

有限状态机借鉴了图灵机的思想,可以看作是最简单的图灵机。

它包含4要素:

  • 现态
  • 条件
  • 动作
  • 次态

有限状态机的基本实现

基础的有限状态机不复杂,无非是几个状态定义成类,提供OnEnter/OnExit/OnUpdate方法,这里直接根据需求给出对应的代码实现。

需求
  • 按住左Ctrl蓄力
  • 蓄力0.5s 内松开取消蓄力
  • 蓄力2.0s 内松开播放技能动作1
  • 蓄力2.0s 以上松开播放技能动作2
  • 蓄力超过3.5s 播放技能动作2
  • 点击空格跳跃
实现

抽象类,定义进入状态和退出状态的行为

public interface IState
{
    public IState OnUpdate(Character t);
    public void OnEnter(Character t);
    public void OnExit(Character t);
}

状态机的关键在于控制状态的切换,这里直接在Character里进行相关逻辑控制。

public class Character : MonoBehaviour
{
    public Animator animator;
    protected IState state;

    public virtual void Update()
    {
        if (null != state)
        {
            IState newState = state.OnUpdate(this);
            //状态切换
            //state switch
            if (null != newState)
            {
                TransitionState(newState);
            }
        }
    }

    /// <summary>
    /// 状态切换
    /// Transition state
    /// </summary>
    /// <param name="newState"></param>
    public void TransitionState(IState newState)
    {
        Debug.Log($"状态变更{newState}");
        newState.OnEnter(this);

        if (null != state)
            state.OnExit(this);
        state = newState;
    }
}

演示一个技能类

public class UltimateSkillState : IState
{
    private int _fullPathHash;
    private int _skillId;
    private float _dt = 0;

    private int _prevHashId = 0;
    public void OnEnter(Character t)
    {
        _dt = 0;
        t.animator.SetInteger(AnimationTriggers.hashIdDefaultSkill, _skillId);

    }
    /// <summary>
    /// 构造
    /// </summary>
    /// <param name="skillId">技能id</param>
    /// <param name="animatorFullPath">动画全路径(如Base Layer.Jump)</param>
    public UltimateSkillState(int skillId, string animatorFullPath)
    {
        _skillId = skillId;
        _fullPathHash = Animator.StringToHash(animatorFullPath);
    }


    public void OnExit(Character t)
    {
        var _animatorStateInfo = t.animator.GetCurrentAnimatorStateInfo(0);
        //恢复默认技能
        t.animator.SetInteger(AnimationTriggers.hashIdDefaultSkill, 0);

    }

    public IState OnUpdate(Character t)
    {
        _dt += Time.deltaTime;
        var _animatorStateInfo = t.animator.GetCurrentAnimatorStateInfo(0);

        //技能播放完毕的判定依据是先播放指定动画,然后再播放到任意其他动作
        if (_prevHashId == 0)
        {
            _prevHashId = t.animator.GetCurrentAnimatorStateInfo(0).fullPathHash == _fullPathHash ? _fullPathHash : 0;
        }
        else
        {
            if (t.animator.GetCurrentAnimatorStateInfo(0).fullPathHash != _prevHashId)
            {
                //技能播放完毕
                return new IdleState();
            }
        }

        return null;
    }
}

最终效果

一些细节的分叉

静态状态与实例化状态

根据状态是否实例化,有限状态机又可以区分为静态状态和实例化状态

实例化状态

image-20230216211432439

实例化状态可以考虑用对象池来进行优化。

静态状态

若全局只有一个对象,可以用静态状态,一般把这些状态直接放置在基类状态类中。

//找一个地方声明静态变量,可以放到基状态类中
static JumpState TheJumpState;
public IState OnUpdate(Character t)
{

    if (Input.GetKeyDown(KeyCode.Space)) //space for jump
    {
        return TheJumpState;
    }
}

一般来说我用的都是实例化状态。

npc会有多个,怪物也会有多个,主角一开始是一个但是随着联机模块的加入也会变成多个。

一般我是先写成实例化状态,有需要再用实例化状态对象池来进行优化。

有限状态机的封装实现—状态机类

在实际的开发中,我们可以像上文一样把状态机的控制逻辑写在控制对象里。

但是随着类的膨胀,为了让控制对象的逻辑更简单清晰(单一职责原则),我们往往会引入一个StateMachine类。

这个状态机类,我们尽量设计的充分一些,让它能处理更多的情况。

全局状态

有的时候,玩家除了基础状态外,还有一个状态需要被处理。

如玩家流血状态,可以和跑跳攻击等状态同时存在。

假设我们沿用上文的状态机,那就需要在每个状态类里处理这个逻辑,过于繁琐。

处理起来很简单,引入一个全局的状态即可。

public void Update()
{
    if (globalState != null)
    {
        globalState.OnUpdate(owner);
    }

    if (currentState != null)
    {
        // 如果当前状态返回了一个新的状态,就切换到新的状态
        if (currentState.OnUpdate(owner) != null)
        {
            ChangeState(currentState.OnUpdate(owner));
        }
    }
}
状态翻转

有的时候,玩家会面临被某个状态打断后,再回到原本状态的需求。

例如在《模拟人生》中,玩家可能会感到本能的迫切要求,不得不去洗手间方便。

image-20230226174038105

实现起来很简单,我们只需要记录prevState,然后提供RevertToPreviousState函数即可。


/// <summary>
/// 状态翻转
/// </summary>
public void RevertToPreviousState()
{
    ChangeState(previousState);
}

完整的状态机代码如下

using UnityEngine;
using AboveCloud;
/// <summary>
/// 有限状态机
/// </summary>
public class FiniteStateMachine<T>
{
    /// <summary>
    /// 当前状态
    /// </summary>
    public IState<T> currentState { get; private set; }

    /// <summary>
    /// 上一个状态
    /// </summary>
    public IState<T> previousState { get; private set; }

    /// <summary>
    /// 全局状态
    /// </summary>
    public IState<T> globalState { get; private set; }

    /// <summary>
    /// 状态机拥有者
    /// </summary>
    public T owner { get; private set; }

    /// <summary>
    /// 构造函数
    /// </summary>
    /// <param name="owner"></param> nm 
    public FiniteStateMachine(T owner)
    {
        this.owner = owner;
    }

    /// <summary>
    /// 设置当前状态
    /// </summary>
    /// <param name="state"></param>
    public void SetCurrentState(IState<T> state)
    {
        currentState = state;
    }

    /// <summary>
    /// 设置全局状态
    /// </summary>
    /// <param name="state"></param>
    public void SetGlobalState(IState<T> state)
    {
        globalState = state;
    }

    /// <summary>
    /// 设置上一个状态
    /// </summary>
    /// <param name="state"></param>
    public void SetPreviousState(IState<T> state)
    {
        previousState = state;
    }

    /// <summary>
    /// 初始化状态机
    /// </summary>
    /// <param name="owner"></param>
    private void Init(T owner)
    {
        this.owner = owner;
    }


    /// <summary>
    /// 更新状态机
    /// </summary>
    public void Update()
    {
        if (globalState != null)
        {
            globalState.OnUpdate(owner);
        }

        if (currentState != null)
        {
            // 如果当前状态返回了一个新的状态,就切换到新的状态
            if (currentState.OnUpdate(owner) != null)
            {
                ChangeState(currentState.OnUpdate(owner));
            }
        }
    }

    /// <summary>
    /// 改变状态
    /// </summary>
    /// <param name="newState"></param>
    public void ChangeState(IState<T> newState)
    {
        if (newState == null)
        {
            Debug.LogError("newState is null");
            return;
        }

        previousState = currentState;

        if (currentState != null)
        {
            Debug.LogFormat("Exit State: {0}", currentState.GetType().Name);
            currentState.OnExit(owner);
        }

        currentState = newState;

        if (currentState != null)
        {
            Debug.LogFormat("Enter State: {0}", currentState.GetType().Name);
            currentState.OnEnter(owner);
        }
    }
}

这样的状态机已经能处理许多情况了。

但如果我们游戏里有许多的AI对象,这种实现就显得有点力不从心:

所有的条件过渡都定义在具体的状态类里,后期的扩展性会比较差。

举个例子,有些AI可以从状态A切换到状态B,有些AI只能从状态A切换到状态C,我该如何生成这些对象的AI?

开闭原则,尽量不要因为新增了一个对象,而要去修改老的状态。

合理的方案是把Transition对象化,开发者可以通过AddState来定义一个对象有哪些行为,通过AddTranslateState来定义对象响应哪些切换。

对于转换条件复杂的AI对象,我们希望可以通过组装的方式来实现状态机的生成。

有限状态机的衍生

并发状态机

在传统的有限状态机,一次只有一个状态。

但有时候我们需要处于一个状态的时候,能做另一个状态的行为。

如主角有跑RunState,跳的状态JumpState,现在我们需要让它可以在跑,跳的同时也能开枪。

如果严格按照上文的有限状态机进行开发,就需要继续开发FireRunState和FireJumpState,状态的数量将急剧膨胀。

有点像桥接模式的概念,这时候我们要做的是分离状态的维度。

Run/Jump是一个维度,开枪是另一个维度,分别处理即可。

public void Update()
{       
	if (currentState != null)
    {
        // 如果当前状态返回了一个新的状态,就切换到新的状态
        if (currentState.OnUpdate(owner) != null)
        {
            ChangeState(currentState.OnUpdate(owner));
        }
    }
    // 分离武器的维度,作为第二种状态
    if (currentEquipState != null)
    {
        // 如果当前状态返回了一个新的状态,就切换到新的状态
        if (currentEquipState.OnUpdate(owner) != null)
        {
            ChangeState(currentEquipState.OnUpdate(owner));
        }
    }
}

当状态处于不同的维度时,并发状态工作得很好。

如何实现代码简化/状态分层

往往在开发中,角色会有大量的状态。

而这些状态中不乏相似的状态。我们可能会在这些状态中重复不少代码。

比如,我们有站立,走路,跑步和滑动这几个状态。

他们都可以响应跳跃Jump动作。

如果继续沿用上文所述的状态机,响应jump的代码我们就得写四遍Jump的逻辑。

又比如有一些状态的优先级是比较高的,比如说机器人电量不足的时候我们希望它去充电。

如果只有一个层次的话,我们需要在每个状态里都处理这个高优先状态的跳转,产生大量冗余的复杂代码。

有几种方法可以实现状态代码的复用:

  • 继承
  • 状态栈
使用继承实现代码简化

使用继承实现分层比较简单,但是多重继承在复杂性上升后会面临不好维护的问题。

在BaseIdle这个基类实现移动的控制

public class BaseIdle : IState<Player>
{

    public virtual void OnEnter(Player t)
    {

    }

    public virtual void OnExit(Player t)
    {

    }

    public virtual IState<Player> OnUpdate(Player player)
    {
        if (player.inputMoveDir != Vector3.zero)
        {
            return new MoveState();
        }

        return null;
    }
}

IdleState继承BaseIdle,加入它新的逻辑。

// 这里演示Idle状态闲置5秒后可切换到IdleS状态
public class IdleState : BaseIdle
{
    private float _idleDuration;
    private static readonly float _idleDurationMax = 5;
    public override void OnEnter(Player player)
    {
        _idleDuration = 0;
        base.OnEnter(player);
    }

    public override void OnExit(Player player)
    {
        _idleDuration = 0;
        base.OnEnter(player);
    }

    public override IState<Player> OnUpdate(Player player)
    {
        _idleDuration += Time.deltaTime;
        if (_idleDuration >= _idleDurationMax)
        {
            return new IdleS_State();
        }
        return base.OnUpdate(player);
    }
}
使用状态栈实现代码简化

我们可以实现状态栈来实现分层状态机HFSM,分层状态机通过将状态嵌套实现了状态的分层。篇幅较长,本篇略去。

此外我们还可以用状态栈来实现下推自动机

  • 8
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

NickPansh

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值