目录

一.HFSM的概述

1.定义

2.生命周期执行流程

  一.同父状态下的子状态切换

 二.跨父状态切换

3.优势

  二.HFSM的实现

1.实现角色具体状态的继承关系

2.实现状态机管理叶子状态

3.使用状态机


参考项目:https://github.com/Wafflus/unity-genshin-impact-movement-system

一.HFSM的概述

1.定义

    相较于一般的有限状态机,HFSM多了一个“分层”的思想,通过继承关系呈现层次化结构,简化复杂系统的状态管理。即在普通平级有限状态机(FSM)基础上,引入父子层级结构的状态机设计模式。

  HFSM的规则与普通FSM相似:

  1.   角色同一时刻必须且只能处于一种叶子状态,但上层父状态处于半活跃状态
  2.   状态机中的状态数目是有限的
  3.   必然有一个初始状态
  4.   每个状态只负责当前状态的逻辑(逻辑解耦)
  5.   状态之间的切换需有明确的条件,一般通过事件(Event)进行切换。

2.生命周期执行流程

  根据面向对象的知识,我们知道:

  一.同父状态下的子状态切换
  1. 退出旧叶子状态
  2. 进入新叶子状态

  这种场景下,输入回调不会重新解绑又绑定,减少性能开销。

 二.跨父状态切换
  1. 退出旧叶子状态
  2. 退出旧父状态
  3. 进入新父状态
  4. 进入新叶子状态

  该场景中,所有通用逻辑会自动执行。

3.优势

  1.   代码复用:对于不同叶子状态的相同的功能,可以将其写在他们的父类中以节省重写的时间。如Idle、Run、和Walk中的地面检测功能可写在Move中。
  2.   可维护性:当写的代码多了后,碰巧又出现了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);
    }
}

让角色一开始进入待机状态,然后我们的状态机就开始有条不紊地运行啦!

Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐