当前位置: 首页 > article >正文

【Unity/HFSM】使用UnityHFSM实现输入缓冲(预输入)和打断机制

文章目录

    • 前言
    • 预输入
      • Animancer的InputBuffer:
      • 在UnityHFSM中实现InputBuffer:
    • 打断机制

前言

参考Animancer在状态机中的InputBuffer,在UnityHFSM中实现类似的InputBuffer机制,同时扩展一个状态打断机制

插件介绍:

Animancer:Unity的动画插件,易于修改和扩展。

UnityHFSM:Github上的一个开源分层状态机项目


基于继承的方法使用UnityHFSM:

UnityHFSM有非常方便的使用方法,可以在不创建新类的情况下创建状态和状态机,定义转换条件等,但如果状态逻辑比较复杂,并且有统一的父级行为,则建议使用类继承的方式定义状态和状态机类。

本文是笔者在做一个自己的ARPG小项目时做出来的,思路仅供参考。


预输入

Animancer的InputBuffer:

Animancer的InputBuffer主要处理流程如下:

  1. ​ 在构造函数中绑定一个状态机
  2. ​ 外部调用Buffer函数,传入期望的目标状态和超时时长
  3. ​ InputBuffer在Update函数中轮询是否能够转换到目标状态,一旦转换成功即立刻结束轮询
  4. ​ 超时后也会结束轮询

注意点:

  1. ​ Animancer接收的是状态机接口,而不是具体的状态机类。但由于需要继承来扩展UntiyHFSM的状态机和状态行为的需要,我在实现时会传入的是类而非接口。
  2. ​ Animancer的InputBuffer并未继承自Monobehavior,并且没有使用任何UnityEngine的东西,即Update需要在其他Mono类中调用,Time.deltaTime也要靠外部传入,方便起见,我实现时直接使用Time.deltaTime。

在UnityHFSM中实现InputBuffer:

Animancer的状态机有TryResetState函数,并且会返回是否成功,UntiyHFSM中无此函数,故使用StateMachine类的StateChanged来触发事件,并与期望的目标状态进行比对,一致则终止轮询。

Animancer直接使用TryResetState函数来改变状态,状态是否允许进入的条件也写在状态中,UnityHFSM依赖于Transition定义两个指定状态之间的转换,虽然也有RequestStateChange来强制转换到某个状态,但这样做并不优雅,并且可能出错。由于这个操作的多样性和复杂性,由外界传入Action来决定轮询时该进行的操作。(大多数时候使用UnityHFSM的StateMachine的Trigger函数来转换状态)

以下是代码,CharacterStateMachineBase是我扩展的角色状态机基类,ECharacterState是角色状态的枚举值

public class InputBuffer
{
    public bool IsActive => Action != null;

    public float TimeOut;
    public Action Action;
   
    public CharacterStateMachineBase StateMachine;
    public ECharacterState TargetState;

    public InputBuffer(CharacterStateMachineBase stateMachine)
    {
        StateMachine = stateMachine;
        StateMachine.StateChanged += OnStateChanged;
    }

    ~InputBuffer()
    {
        StateMachine.StateChanged -= OnStateChanged;
    }

    public void Buffer(Action action, ECharacterState targetState, float timeOut)
    {
        Action = action;
        TargetState = targetState;
        TimeOut = timeOut;
    }

    public void Update()
    {
        if (IsActive)
        {
            Action();

            TimeOut -= Time.deltaTime;
            if (TimeOut < 0)
                Clear();
        }
    }

    public virtual void Clear()
    {
        Action = null;
        TimeOut = default;
    }

    public void OnStateChanged(UnityHFSM.StateBase<ECharacterState> state)
    {
        if (IsActive && state.name == TargetState)
        {
            Clear();
        }
    }
}

在外部调用时如下

//CharacterBrain.cs

//控制的玩家
public Character ControlledCharacter;

//攻击缓冲
public float AttackTimeout = 1f;
InputBuffer AttackBuffer;

//绑定操作
void Start()
{
    GameInput.Instance.onAttack += () =>
    {
        AttackBuffer.Buffer(
            () => ControlledCharacter.Attack(), 
            ECharacterState.Attack, 
            AttackTimeout);
    };
}

//轮询
void Update()
{
	AttackBuffer.Update();
}

Character的Attack函数实际上就是在调用角色状态机的Trigger函数,如下

//Character.cs

public void Attack()
{
    if (Equipment.CurrentWeapon == null)
        return;
    StateMachine.Trigger("Attack");
}

打断机制

我把打断机制写在了状态机中,作为一种扩展行为,在UnityHFSM中,一个State有一个needsExitTime字段,该字段为true时,除非强制转换,否则无法退出本状态,配合Animancer的动画事件(OnEnd)可以很好的让动画来控制状态的转换(我这样做是希望角色的各种动作更加真实,让逻辑和表现更容易统一)。

为此我们只需要新增一个叫做CanExitBeforeEnd的bool变量即可表示本状态是否可以被打断。于此同时,实际上完全没有必要为每个状态新建一个CanExitBeforeEnd变量,将该变量放在状态机类中,供每个状态变更,供外界访问即可。

现在我们只使用CanExitBeforeEnd表示状态希望被改变,但没有直接改变的逻辑,假设我们当前使用Trigger的方法进行状态变更,则我们只需要在每次Trigger前查询一次当前状态是否需要变更,需要变更且状态的needsExitTime为true,则将其设置为false,再执行Trigger操作即可。

StateMachine的Trigger操作并不是虚函数,只需要在源码中为其添加virtual关键字即可,然后在扩展的CharacterStateMachineBase中重写Trigger逻辑,代码如下

public class CharacterStateMachineData
{
    public bool CanExitBeforeEnd;
}

public class CharacterStateMachineBase : StateMachine<ECharacterState>
{
    public Character Owner;

    public CharacterStateMachineData Data;

    public CharacterStateMachineBase(Character owner, CharacterStateMachineData data = null) : base()
    {
        Owner = owner;

        if(data != null )
            Data = data;
        else
            Data = new CharacterStateMachineData();
    }

	//分层状态机递归获取具体状态
    public CharacterStateBase CurrentState
    {
        get
        {
            if (ActiveState is CharacterStateMachineBase)
                return (ActiveState as CharacterStateMachineBase).CurrentState;
            return ActiveState as CharacterStateBase;
        }
    }

    public override void Trigger(string trigger)
    {
        //允许玩家通过一些动作和输入打断当前状态
        if(ActiveState.needsExitTime && Data.CanExitBeforeEnd)
            ActiveState.needsExitTime = false;

        base.Trigger(trigger);
    }
}

public class CharacterStateBase : State<ECharacterState>
{
    public Character Owner;

    new public CharacterStateMachineBase fsm => base.fsm as CharacterStateMachineBase;

    public virtual Vector3 DeltaMotion => Owner.Animancer.Animator.deltaPosition;

    public CharacterStateBase(Character owner) : base()
    {
        Owner = owner;
    }

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

        fsm.Data.CanExitBeforeEnd = false;
    }
}

以上是在使用Trigger时打断的操作,假设我们使用的是普通的Transition进行状态变更,那么是无法打断的,比如如下的移动操作,完全没有使用Trigger

MovementStateMachineBase GroundFSM = new MovementStateMachineBase(owner);
GroundFSM.AddState(EMovementState.Idle, new IdleState(owner));
GroundFSM.AddState(EMovementState.Move, new MoveState(owner));
GroundFSM.AddTwoWayTransition(EMovementState.Idle, EMovementState.Move,
    t => Owner.Parameters.MovementDirection.magnitude > 0);

为此,我们可以在期望被打断的状态机中特别指出何时可以被打断,我当前只在攻击状态时可以被打断,不打断会放完攻击动画,打断会立刻进入其他状态并播放动画

特别的代码如下,该代码会在AttackState的Update函数中被轮询

Parameters是Character的字段,用于与直接的GameInput解耦

private void UpdateInterrupt()
{
    if(!fsm.Data.CanExitBeforeEnd)
        return;

    //移动打断
    if (Owner.Parameters.MovementDirection.magnitude > 0)
    {
        needsExitTime = false;
    }
}

http://www.kler.cn/a/441758.html

相关文章:

  • Vue平台开发三——项目管理页面
  • [答疑]这个消息名是写发送数据还是接收数据
  • R语言的图形用户界面
  • 【深度学习项目】语义分割-FCN网络(原理、网络架构、基于Pytorch实现FCN网络)
  • 代码中使用 Iterable<T> 作为方法参数的解释
  • 学习ASP.NET Core的身份认证(基于JwtBearer的身份认证6)
  • Redis API(springboot整合,已封装)
  • Mac上使用ln指令创建软链接、硬链接
  • 模拟法简介(蓝桥杯)
  • Sql注入(靶场)14-20关
  • 力扣.——560. 和为 K 的子数组
  • 关于SQL注入的面试题及经验分享
  • 测试框架 —— Playwright Fixture夹具有效利用的建议指南!
  • Springboot和vue前后端交互实现验证码登录
  • 【Leetcode 每日一题 - 扩展】1326. 灌溉花园的最少水龙头数目
  • 如何在 Ubuntu 22.04 上安装 Strapi CMS
  • [SAP ABAP] 序列化与反序列化
  • Javer学习Groovy
  • Chinese-Clip实现以文搜图和以图搜图
  • WPF Combox使用 Text无法选择正确获取CHange后的Text
  • java服务器中,如何判定是该使用单例系统,还是微服务架构,多库分布式,服务分布式,前端分布式
  • 2.Nuxt学习 组件使用和路由跳转相关
  • 关于SAP Router连接不稳定的改良
  • unity 雷达
  • SQL Server 表值函数使用示例
  • 负载均衡oj项目:介绍