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

[复制链接]
发表于 2025-12-4 22:08:03 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
前言

参考Animancer在状态机中的InputBuffer,在UnityHFSM中实现类似的InputBuffer机制,同时扩展一个状态打断机制
插件先容:
Animancer:Unity的动画插件,易于修改和扩展。
UnityHFSM:Github上的一个开源分层状态机项目

基于继续的方法使用UnityHFSM:
UnityHFSM有非常方便的使用方法,可以在不创建新类的情况下创建状态和状态机,界说转换条件等,但如果状态逻辑比力复杂,而且有同一的父级举动,则发起使用类继续的方式界说状态和状态机类。
本文是笔者在做一个本身的ARPG小项目时做出来的,思绪仅供参考。

预输入

Animancer的InputBuffer:

Animancer的InputBuffer紧张处理惩罚流程如下:

  • ​ 在构造函数中绑定一个状态机
  • ​ 外部调用Buffer函数,传入盼望的目标状态和超时时长
  • ​ InputBuffer在Update函数中轮询是否可以大概转换到目标状态,一旦转换乐成即立即竣事轮询
  • ​ 超时后也会竣事轮询
注意点:

  • ​ Animancer吸收的是状态机接口,而不是具体的状态机类。但由于须要继续来扩展UntiyHFSM的状态机和状态举动的须要,我在实现时会传入的是类而非接口。
  • ​ 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是脚色状态的罗列值
  1. public class InputBuffer
  2. {
  3.     public bool IsActive => Action != null;
  4.     public float TimeOut;
  5.     public Action Action;
  6.    
  7.     public CharacterStateMachineBase StateMachine;
  8.     public ECharacterState TargetState;
  9.     public InputBuffer(CharacterStateMachineBase stateMachine)
  10.     {
  11.         StateMachine = stateMachine;
  12.         StateMachine.StateChanged += OnStateChanged;
  13.     }
  14.     ~InputBuffer()
  15.     {
  16.         StateMachine.StateChanged -= OnStateChanged;
  17.     }
  18.     public void Buffer(Action action, ECharacterState targetState, float timeOut)
  19.     {
  20.         Action = action;
  21.         TargetState = targetState;
  22.         TimeOut = timeOut;
  23.     }
  24.     public void Update()
  25.     {
  26.         if (IsActive)
  27.         {
  28.             Action();
  29.             TimeOut -= Time.deltaTime;
  30.             if (TimeOut < 0)
  31.                 Clear();
  32.         }
  33.     }
  34.     public virtual void Clear()
  35.     {
  36.         Action = null;
  37.         TimeOut = default;
  38.     }
  39.     public void OnStateChanged(UnityHFSM.StateBase<ECharacterState> state)
  40.     {
  41.         if (IsActive && state.name == TargetState)
  42.         {
  43.             Clear();
  44.         }
  45.     }
  46. }
复制代码
在外部调用时如下
  1. //CharacterBrain.cs
  2. //控制的玩家
  3. public Character ControlledCharacter;
  4. //攻击缓冲
  5. public float AttackTimeout = 1f;
  6. InputBuffer AttackBuffer;
  7. //绑定操作
  8. void Start()
  9. {
  10.     GameInput.Instance.onAttack += () =>
  11.     {
  12.         AttackBuffer.Buffer(
  13.             () => ControlledCharacter.Attack(),
  14.             ECharacterState.Attack,
  15.             AttackTimeout);
  16.     };
  17. }
  18. //轮询
  19. void Update()
  20. {
  21.         AttackBuffer.Update();
  22. }
复制代码
Character的Attack函数实际上就是在调用脚色状态机的Trigger函数,如下
  1. //Character.cs
  2. public void Attack()
  3. {
  4.     if (Equipment.CurrentWeapon == null)
  5.         return;
  6.     StateMachine.Trigger("Attack");
  7. }
复制代码

打断机制

我把打断机制写在了状态机中,作为一种扩展举动,在UnityHFSM中,一个State有一个needsExitTime字段,该字段为true时,除非逼迫转换,否则无法退出本状态,共同Animancer的动画变乱(OnEnd)可以很好的让动画来控制状态的转换(我如许做是盼望脚色的各种动作更加真实,让逻辑和体现更容易同一)。
为此我们只须要新增一个叫做CanExitBeforeEnd的bool变量即可体现本状态是否可以被打断。于此同时,实际上完全没有须要为每个状态新建一个CanExitBeforeEnd变量,将该变量放在状态机类中,供每个状态变更,供外界访问即可。
现在我们只使用CanExitBeforeEnd表近况态盼望被改变,但没有直接改变的逻辑,假设我们当前使用Trigger的方法举行状态变更,则我们只须要在每次Trigger前查询一次当前状态是否须要变更,须要变更且状态的needsExitTime为true,则将其设置为false,再实行Trigger利用即可。
StateMachine的Trigger利用并不是虚函数,只须要在源码中为其添加virtual关键字即可,然后在扩展的CharacterStateMachineBase中重写Trigger逻辑,代码如下
  1. public class CharacterStateMachineData
  2. {
  3.     public bool CanExitBeforeEnd;
  4. }
  5. public class CharacterStateMachineBase : StateMachine<ECharacterState>
  6. {
  7.     public Character Owner;
  8.     public CharacterStateMachineData Data;
  9.     public CharacterStateMachineBase(Character owner, CharacterStateMachineData data = null) : base()
  10.     {
  11.         Owner = owner;
  12.         if(data != null )
  13.             Data = data;
  14.         else
  15.             Data = new CharacterStateMachineData();
  16.     }
  17.         //分层状态机递归获取具体状态
  18.     public CharacterStateBase CurrentState
  19.     {
  20.         get
  21.         {
  22.             if (ActiveState is CharacterStateMachineBase)
  23.                 return (ActiveState as CharacterStateMachineBase).CurrentState;
  24.             return ActiveState as CharacterStateBase;
  25.         }
  26.     }
  27.     public override void Trigger(string trigger)
  28.     {
  29.         //允许玩家通过一些动作和输入打断当前状态
  30.         if(ActiveState.needsExitTime && Data.CanExitBeforeEnd)
  31.             ActiveState.needsExitTime = false;
  32.         base.Trigger(trigger);
  33.     }
  34. }
  35. public class CharacterStateBase : State<ECharacterState>
  36. {
  37.     public Character Owner;
  38.     new public CharacterStateMachineBase fsm => base.fsm as CharacterStateMachineBase;
  39.     public virtual Vector3 DeltaMotion => Owner.Animancer.Animator.deltaPosition;
  40.     public CharacterStateBase(Character owner) : base()
  41.     {
  42.         Owner = owner;
  43.     }
  44.     public override void OnEnter()
  45.     {
  46.         base.OnEnter();
  47.         fsm.Data.CanExitBeforeEnd = false;
  48.     }
  49. }
复制代码
以上是在使用Trigger时打断的利用,假设我们使用的是寻常的Transition举行状态变更,那么是无法打断的,比如如下的移动利用,完全没有使用Trigger
  1. MovementStateMachineBase GroundFSM = new MovementStateMachineBase(owner);
  2. GroundFSM.AddState(EMovementState.Idle, new IdleState(owner));
  3. GroundFSM.AddState(EMovementState.Move, new MoveState(owner));
  4. GroundFSM.AddTwoWayTransition(EMovementState.Idle, EMovementState.Move,
  5.     t => Owner.Parameters.MovementDirection.magnitude > 0);
复制代码
为此,我们可以在盼望被打断的状态机中特殊指出何时可以被打断,我当前只在攻击状态时可以被打断,不打断会放完攻击动画,打断会立即进入其他状态并播放动画
特殊的代码如下,该代码会在AttackState的Update函数中被轮询
Parameters是Character的字段,用于与直接的GameInput解耦
  1. private void UpdateInterrupt()
  2. {
  3.     if(!fsm.Data.CanExitBeforeEnd)
  4.         return;
  5.     //移动打断
  6.     if (Owner.Parameters.MovementDirection.magnitude > 0)
  7.     {
  8.         needsExitTime = false;
  9.     }
  10. }
复制代码
回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表