游戏角色分层有限状态机(Hierarchical Finite StateMachine, HFSM)框架
相较于一般的有限状态机,HFSM多了一个“分层”的思想,通过继承关系呈现层次化结构,简化复杂系统的状态管理。即在普通平级有限状态机(FSM)基础上,引入父子层级结构的状态机设计模式。角色同一时刻必须且只能处于一种叶子状态,但上层父状态处于半活跃状态状态机中的状态数目是有限的必然有一个初始状态每个状态只负责当前状态的逻辑(逻辑解耦)状态之间的切换需有明确的条件,一般通过事件(Event)进行切换。
目录
参考项目:https://github.com/Wafflus/unity-genshin-impact-movement-system
一.HFSM的概述
1.定义
相较于一般的有限状态机,HFSM多了一个“分层”的思想,通过继承关系呈现层次化结构,简化复杂系统的状态管理。即在普通平级有限状态机(FSM)基础上,引入父子层级结构的状态机设计模式。

HFSM的规则与普通FSM相似:
- 角色同一时刻必须且只能处于一种叶子状态,但上层父状态处于半活跃状态
- 状态机中的状态数目是有限的
- 必然有一个初始状态
- 每个状态只负责当前状态的逻辑(逻辑解耦)
- 状态之间的切换需有明确的条件,一般通过事件(Event)进行切换。
2.生命周期执行流程
根据面向对象的知识,我们知道:
一.同父状态下的子状态切换
- 退出旧叶子状态
- 进入新叶子状态
这种场景下,输入回调不会重新解绑又绑定,减少性能开销。
二.跨父状态切换
- 退出旧叶子状态
- 退出旧父状态
- 进入新父状态
- 进入新叶子状态
该场景中,所有通用逻辑会自动执行。
3.优势
- 代码复用:对于不同叶子状态的相同的功能,可以将其写在他们的父类中以节省重写的时间。如Idle、Run、和Walk中的地面检测功能可写在Move中。
- 可维护性:当写的代码多了后,碰巧又出现了bug,你会发现找起来特别的折磨(其实是我T-T)。而HFSM的层级清晰,不管是修改还是查bug,通用逻辑只需要检查父状态,具体行为只需要检查叶子状态即可。
二.HFSM的实现
1.实现角色具体状态的继承关系
1.所有状态类的接口BaseState
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//使用接口强制要求子类实现接口方法
public interface IState
{
public void Enter();
public void Exit();
public void Update();
public void PhysicsUpdate();
}
2.移动状态
using System;
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.Rendering.Universal;
public class MoveState : IState
{
protected MovementStateMachine stateMachine;
//构造函数
public MoveState(MovementStateMachine movementStateMachine)
{
stateMachine = movementStateMachine;
}
public virtual void Enter()
{
//添加输入回调等
}
public virtual void Exit()
{
//移除输入回调等
}
public virtual void PhysicsUpdate()
{
//移动状态需要每物理帧更新的具体逻辑
}
public virtual void Update()
{
}
//移动状态特有的具体逻辑......
}
3.移动状态的地面状态
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
public class GroundState : MoveState
{
public PlayerGroundedState(MovementStateMachine movementStateMachine) : base(movementStateMachine)
{
}
public override void PhysicsUpdate()
{
base.PhysicsUpdate();
//地面移动状态需要每物理帧更新的具体逻辑
}
public override void Enter()
{
base.Enter();
//其他逻辑...
}
public override void Exit()
{
base.Exit();
//其他逻辑...
}
//重写输入回调的添加和移除
//其他具体逻辑......
}
4.待机状态
using System;
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
public class Idle : GroundState
{
public IdleState(MovementStateMachine movementStateMachine) : base(movementStateMachine)
{
}
public override void Enter()
{
base.Enter();
//播放动画等其他具体逻辑...
}
public override void Exit()
{
base.Exit();
//停止动画等其他具体逻辑...
}
public override void Update()
{
base.Update();
if(stateMchine.player.input==Vector2.zero)
{
return;
}
//状态的切换
if(canRun)
{
stateMachine.ChangeState(stateMachine.Run);
}
//其他切换和移动逻辑...
}
public override void PhysicsUpdate()
{
base.PhysicsUpdate();
//锁定角色速度等其他具体逻辑...
}
}
其他状态同理。
若用事件切换状态而不是每帧检测条件的话,可在BaseState中加入一个方法,用于实现不同状态间的切换。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public interface IState
{
//原有方法...
public void EventTransition();
}
2.实现状态机管理叶子状态
1.所有状态机的父类
using UnityEngine;
public abstract class StateMachine
{
//保存当前的状态
protected IState currentState;
//用于状态间的切换,状态机的核心
public void ChangeState(BaseState newState)
{
currentState?.Exit();
currentState = newState;
currentState.Enter();
}
public void Update()
{
currentState?.Update();
}
public void PhysicsUpdate()
{
currentState?.PhysicsUpdate();
}
public void EventTransition()
{
currentState?.EventTransition();
}
}
2.移动状态机
public class MovementStateMachine : StateMachine
{
public Player player{ get; }
public Idle idle{ get; }
public Walk walk{ get; }
public Run run{ get; }
public Jump jump{ get; }
public Fall fall{ get; }
public Land land{ get; }
...
....
//构造函数
public MovementStateMachine(Player player)
{
this.player = player;
idle = new Idle(this);
...
....
//实例化所有叶子状态
}
}
3.战斗状态机
public class CombatStateMachine : StateMachine
{
public Player player{ get; }
public BaseAttack baseAttack{ get; }
...
....
//构造函数
public MovementStateMachine(Player player)
{
this.player = player;
baseAttack = new BaseAttack();
...
....
//实例化所有叶子状态
}
}
自此,我们的状态机框架已经基本成型。
那么我们怎么去使用它呢?
3.使用状态机
在上述的框架中,MovementStateMachine保存了一个Player的实例,这是因为状态机和各状态需要获取角色的数据(包括血量、蓝量等)和组件(刚体、碰撞体等)。我们只需在player脚本将当前player实例传给状态机即可。
而MoveState(CombatState同理)保存MovementStateMachine实例不仅能用于实现状态切换,还能间接获取player的数据和组件。
简单来说player角色脚本只负责角色的数据,状态机只负责管理状态,这样实现依赖关系能实现逻辑解耦,有利于我们后期的维护。
由于FSM必然有一个初始状态,且状态间的切换是在状态机内部自己实现的,所以我们只需要给角色一个初始状态即可,不用管后续状态的转换逻辑。
在角色的脚本中加上
public class Player:MonoBehaviour
{
private MovementStateMachine movementStateMachine;
private CombatStateMachine combatStateMachine;
private void Awake()
{
movementStateMachine = new MovementStateMachine(this);
combatStateMachine = new CombatStateMachine(this);
}
private void Start()
{
movementStateMachine.ChangeState(movementStateMachine.Idle);
}
}
让角色一开始进入待机状态,然后我们的状态机就开始有条不紊地运行啦!
更多推荐


所有评论(0)