【Unity3D日常开发】(四十二)Unity3D中有限状态机的简单实现

举报
恬静的小魔龙 发表于 2021/12/22 08:33:46 2021/12/22
【摘要】 推荐阅读CSDN主页GitHub开源地址Unity3D插件分享简书地址我的个人博客QQ群:1040082875 一、前言Hello,大家好,我是☆恬静的小魔龙☆,正所谓学而不思则罔,思而不学则殆,最近项目开发中,人物的动作特别多,用状态机去切换感觉太麻烦,然后切换的效果也并不理想。比如下面的状态机:每次“走→站立→跑”,都一些卡顿,没有那么丝滑,所以就想学习一下FSM(有限状态机)。 二、有...

推荐阅读

一、前言

Hello,大家好,我是☆恬静的小魔龙☆,正所谓学而不思则罔,思而不学则殆,最近项目开发中,人物的动作特别多,用状态机去切换感觉太麻烦,然后切换的效果也并不理想。

比如下面的状态机:
在这里插入图片描述
每次“走→站立→跑”,都一些卡顿,没有那么丝滑,所以就想学习一下FSM(有限状态机)。

二、有限状态机

什么是有限状态机:

如其名有限状态机,就是可以枚举出有限个状态,然后状态直接可以进行切换的状态机。总体来说,有限状态机就是在不同阶段呈现出不同的运行状态的系统,这些状态是有限的、不重叠的。这样的系统在某一时刻响应其状态中的一个状态。

为啥需要有限状态机:

在开发中,一个角色或者AI会有很多的状态,也会产生很多的动画,比如攻击、移动、巡逻、追击、逃跑、死完,这些动画状态会不停地切换,简单的方式就是使用Switch进行判断,然后每一个case去进行相应的状态处理和转换条件判断。
这种方式有点事简单方便,缺点就是不利于扩展,所以就出现了有限状态机去管理。
总结来说就是,为了解决游戏过于麻烦的状态转换

有限状态机与设计模式

细节依赖抽象,抽象不依赖细节,基于抽象编程,让框架先跑起来。

有限状态机参考的是状态设计模式的理念,允许对象在内部状态发生改变时改变它的行为,对象看起来好像修改了它的类,类的行为是基于它的状态改变的。
有限状态机,类比于Switch就是将cast中的逻辑封装到各个State对象中了,这样做可以方便扩展,后期如果需要增加新状态,只需要继承基类,添加实现就好,不用修改原来的代码,这就是开闭原则。

源工程下载:

https://download.csdn.net/download/q764424567/20600344

三、有限状态机的实现

3-1、有限状态机基类

首先,我们需要一个基类,让其他的State对象进行继承:

/// <summary>
/// 状态抽象类
/// </summary>
public abstract class FSMState
{
    // 所属的状态机
    protected FSMSystem m_FSM = null;
    protected FSMState(FSMSystem fsm)
    {
        m_FSM = fsm;
    }
    /// <summary>
    /// 进入状态
    /// </summary>
    public virtual void OnEnter() { }
    /// <summary>
    /// 状态中进行的动作
    /// </summary>
    public abstract void Action();
    /// <summary>
    /// 检测状态转换
    /// </summary>
    public abstract void Check();
    /// <summary>
    /// 退出状态
    /// </summary>
    public virtual void OnExit() { }
}

3-2、有限状态机系统

接着,需要一个有限状态机系统去管理控制这些状态机,包含添加状态、删除状态、改变状态、状态改变等。

using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// 有限状态机系统
/// </summary>
public class FSMSystem
{
    /// <summary>
    /// 状态机所属游戏物体
    /// </summary>
    public GameObject OwnerGo { get; private set; }
    public Animation Ani { get; private set; }

    // 用字典存储每个状态ID对应的状态
    private Dictionary<StateID, FSMState> m_StateMap = new Dictionary<StateID, FSMState>();

    public FSMSystem(GameObject ownerGo)
    {
        OwnerGo = ownerGo;
        Ani = OwnerGo.GetComponent<Animation>();
    }

    /// <summary>
    /// 当前状态ID
    /// </summary>
    public StateID CurrentStateID { get; private set; }
    /// <summary>
    /// 当前状态
    /// </summary>
    public FSMState CurrentState { get; private set; }

    /// <summary>
    /// 添加状态
    /// </summary>
    /// <param name="id">添加的状态ID</param>
    /// <param name="state">对应的状态对象</param>
    public void AddState(StateID id, FSMState state)
    {
        // 如果当前状态为空,就设置为默认状态
        if (CurrentState == null)
        {
            CurrentStateID = id;
            CurrentState = state;
        }
        if (m_StateMap.ContainsKey(id))
        {
            Debug.LogErrorFormat("状态ID:{0}已经存在,不能重复添加!", id);
            return;
        }
        m_StateMap.Add(id, state);
    }

    /// <summary>
    /// 移除状态
    /// </summary>
    /// <param name="id">要移除的状态ID</param>
    public void RemoveState(StateID id)
    {
        if (!m_StateMap.ContainsKey(id))
        {
            Debug.LogWarningFormat("状态ID:{0}不存在,不需要移除", id);
            return;
        }
        m_StateMap.Remove(id);
    }

    /// <summary>
    /// 改变状态
    /// </summary>
    /// <param name="id">需要转换到的目标状态ID</param>
    public void ChangeState(StateID id)
    {
        if (id == CurrentStateID) return;
        if (!m_StateMap.ContainsKey(id))
        {
            Debug.LogErrorFormat("状态ID:{0}不存在!", id);
            return;
        }
        if (CurrentState != null)
            CurrentState.OnExit();
        CurrentStateID = id;
        CurrentState = m_StateMap[id];
        CurrentState.OnEnter();
    }

    /// <summary>
    /// 更新,在状态机持有者物体的Update中调用
    /// </summary>
    public void Update()
    {
        CurrentState.Action();
        CurrentState.Check();
    }
}

3-3、添加状态

这时候,就需要看一下我们的模型有多少个动画了,比如我这个模型:
在这里插入图片描述
共12个动画片段,但是我只需要里面的9个动画片段,所以先定义一个枚举,将所有用到的动画片段枚举出来:

/// <summary>
/// 状态ID
/// </summary>
public enum StateID
{
    Stand,
    Standwait,
    Walk,
    Jump1,
    Run,
    Jump2,
    Down,
    Take,
    Work
}

接着就添加状态:

/// <summary>
/// 站立
/// </summary>
public class StandState : FSMState
{
    private float m_Timer = 0f;
    public StandState(FSMSystem fsm) : base(fsm)
    {
        Debug.Log("StandState");
    }
    public override void Action()
    {
        m_FSM.Ani.Play("Stand");
    }
    public override void Check()
    {
        m_Timer += Time.deltaTime;
        // 如果旋转达到1秒
        if (m_Timer > 0.25f)
        {
            // 转换到移动状态
            m_FSM.ChangeState(StateID.Standwait);
        }
    }
    public override void OnExit()
    {
        m_Timer = 0;
    }
}
/// <summary>
/// 站立等待
/// </summary>
public class StandWaitState : FSMState
{
    public StandWaitState(FSMSystem fsm) : base(fsm)
    {
        Debug.Log("StandWaitState");
    }
    public override void Action()
    {
        m_FSM.Ani.Play("Stand-wait");
    }
    public override void Check() { }
}
/// <summary>
/// 走
/// </summary>
public class WalkState : FSMState
{
    public WalkState(FSMSystem fsm) : base(fsm)
    {
        Debug.Log("WalkState");
    }
    public override void Action()
    {
        m_FSM.Ani.Play("walk");
    }
    public override void Check() { }
}
/// <summary>
/// 小跳
/// </summary>
public class Jump1State : FSMState
{
    private float m_Timer = 0f;
    public Jump1State(FSMSystem fsm) : base(fsm)
    {
        Debug.Log("Jump1State");
    }
    public override void Action()
    {
        m_FSM.Ani.Play("jump1");
    }
    public override void Check() 
    {
        m_Timer += Time.deltaTime;
        // 如果旋转达到1秒
        if (m_Timer > 0.6f)
        {
            // 转换到移动状态
            m_FSM.ChangeState(StateID.Standwait);
        }
    }
    public override void OnExit()
    {
        m_Timer = 0;
    }
}
/// <summary>
/// 跑
/// </summary>
public class Run2State : FSMState
{
    public Run2State(FSMSystem fsm) : base(fsm)
    {
        Debug.Log("Run2State");
    }
    public override void Action()
    {
        m_FSM.Ani.Play("run");
    }
    public override void Check() { }
}
/// <summary>
/// 大跳
/// </summary>
public class Jump2State : FSMState
{
    private float m_Timer = 0f;
    public Jump2State(FSMSystem fsm) : base(fsm)
    {
        Debug.Log("Jump2State");
    }
    public override void Action()
    {
        m_FSM.Ani.Play("jump2");
    }
    public override void Check()
    {
        m_Timer += Time.deltaTime;
        // 如果旋转达到1秒
        if (m_Timer > 0.75f)
        {
            // 转换到移动状态
            m_FSM.ChangeState(StateID.Standwait);
        }
    }
    public override void OnExit()
    {
        m_Timer = 0;
    }
}
/// <summary>
/// 拿取
/// </summary>
public class TakeState : FSMState
{
    public TakeState(FSMSystem fsm) : base(fsm)
    {
        Debug.Log("TakeState");
    }
    public override void OnEnter()
    {
        m_FSM.Ani.Play("take");
    }
    public override void Action() { }
    public override void Check() { }
    public override void OnExit() { }
}
/// <summary>
/// 工作
/// </summary>
public class WorkState : FSMState
{
    public WorkState(FSMSystem fsm) : base(fsm)
    {
        Debug.Log("WorkState");
    }
    public override void OnEnter()
    {
        m_FSM.Ani.Play("work");
    }
    public override void Action() { }
    public override void Check() { }
    public override void OnExit() { }
}
/// <summary>
/// 蹲
/// </summary>
public class DownState : FSMState
{
    public DownState(FSMSystem fsm) : base(fsm)
    {
        Debug.Log("DownState");
    }
    public override void OnEnter()
    {
        m_FSM.Ani.Play("Down");
    }
    public override void Action() { }
    public override void Check() { }
    public override void OnExit() { }
}

我为了方便,将有限状态机的基类、状态枚举、状态对象都写到一个脚本里面了,大家觉得不爽也可以分开来。

3-4、调用动画

新建一个Character类,用来调用动画:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Character : MonoBehaviour
{
    // 有限状态机
    private FSMSystem m_FSM = null;

    private void Start()
    {
        // 实例化有限状态机
        m_FSM = new FSMSystem(this.gameObject);
        // 添加状态到有限状态机中
        m_FSM.AddState(StateID.Stand, new StandState(m_FSM));
        m_FSM.AddState(StateID.Standwait, new StandWaitState(m_FSM));
        m_FSM.AddState(StateID.Walk, new WalkState(m_FSM));
        m_FSM.AddState(StateID.Jump1, new Jump1State(m_FSM));
        m_FSM.AddState(StateID.Run, new Run2State(m_FSM));
        m_FSM.AddState(StateID.Jump2, new Jump2State(m_FSM));
        m_FSM.AddState(StateID.Down, new DownState(m_FSM));
        m_FSM.AddState(StateID.Take, new TakeState(m_FSM));
        m_FSM.AddState(StateID.Work, new WorkState(m_FSM));
    }

    private void Update()
    {
        // 调用有限状态机中的Update方法
        if (m_FSM != null)
            m_FSM.Update();

        if (Input.GetKey(KeyCode.Alpha1))
        {
            m_FSM.ChangeState(StateID.Stand);
        }
        if (Input.GetKey(KeyCode.Alpha2))
        {
            m_FSM.ChangeState(StateID.Standwait);
        }
        if (Input.GetKey(KeyCode.Alpha3))
        {
            m_FSM.ChangeState(StateID.Walk);
        }
        if (Input.GetKey(KeyCode.Alpha4))
        {
            m_FSM.ChangeState(StateID.Jump1);
        }
        if (Input.GetKey(KeyCode.Alpha5))
        {
            m_FSM.ChangeState(StateID.Run);
        }
        if (Input.GetKey(KeyCode.Alpha6))
        {
            m_FSM.ChangeState(StateID.Jump2);
        }
        if (Input.GetKey(KeyCode.Alpha7))
        {
            m_FSM.ChangeState(StateID.Down);
        }
        if (Input.GetKey(KeyCode.Alpha8))
        {
            m_FSM.ChangeState(StateID.Take);
        }
        if (Input.GetKey(KeyCode.Alpha9))
        {
            m_FSM.ChangeState(StateID.Work);
        }
    }
}

挂载到对象上:
在这里插入图片描述
效果图:

在这里插入图片描述

四、后言

这个例子没有太多炫技的代码,作为一个学习有限状态机的入门例子来看还是很不错的。

希望大家今天也有学习哦。
在这里插入图片描述

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。