MDD

有限状态机是MDDSkillEngine的逻辑驱动核心。

概念

故名思意,FSM就是管理有限个状态的机器。

任何时刻只有一个状态生效。

一个状态何时去往下一个状态由状态本身决定。

状态是什么

状态是一个抽象的概念

一个状态内包含什么取决于你对你想管理的状态的定义

也取决于你能够做到的管理行为的边界

MDDSkillEngine中状态包含了什么

在MDDSkillEngine中角色状态机用来驱动角色的几乎所有行为,

包括但不限于,

技能行为(技能动画,buff生成,特效,碰撞体,召唤物生成等)

物理行为(角色控制器行为,寻路等)

动画

换句话说FSM也是表达一个角色逻辑的核心

这么做的理由

让我们设想一个片段:

一个小姐姐眨了一下眼睛。

如果我们愿意

我们可以把小姐姐这个一眨眼的片段划分成无数个状态

从刚开始眨眼的一瞬的状态1

到结束眨眼的状态n

我们可以对每个状态进行逻辑的制定和规划

同时每个状态下面还可以嵌套子状态机(MDDSkillEngine暂时没用到子状态机)

子状态机的状态还可以继续嵌套孙状态机依次类推无限嵌套

所以

FSM的上限就是对游戏世界运行的元掌控

当然,一般来说在游戏开发中没人会抽风的用到这种地步

所以我愿将FSM作为技能的主逻辑驱动手段

这样在理论上FSM的能力上限及MDDSkillEngine的能力上限

~~~

一切皆有可能~

状态机的技术选型

一个我认为不太好的实现方式:Unity动画状态机

unity为游戏开发者提供了一个动画状态机

主要用来管理动画的跳转

其数据编辑主要通过节点可视化的连线方式

其管理动画少的时候没啥问题

但是一但动画一多又不好分层的时候...

就会出现这个经典蜘蛛网:

蜘蛛网动画状态机

我对这个蜘蛛网的看法是:

1.蜘蛛网的出现和状态机本身状态与状态之间逻辑上解耦设计目标背道而驰

一般来说

我们制作一个状态,往往需要专注在这个状态的本身

然后只关注这个状态能去到哪个状态,哪个状态能跳转到我正在制作的状态

更多的外部情况则无需太过关心

蜘蛛网则过于加重了制作者对整体状态机维护的精力

2.蜘蛛网可以有至少能看个整体布局,但不应该让人力来连连看

很明显如果状态过多让人来手动维护这个连连看非常可能成为一个不明智的决定

我认为这个蜘蛛网可以通过工具自动生成,生成的依据是已经编码好的状态机即可

这样既有了可视化的状态机整体布局又避免了对蜘蛛网的连连看维护

为角色控制选用一个合适的状态机模型-栈式轮询状态机

先让我们看看一个没有做任何处理的FSM

一个纯净的FSM数据结构图

纯净数据结构图

这个FSM中的状态暂时没有写任何跳转处理

接下来让我们设想一下我们玩过的游戏

一个act游戏或者moba游戏中的角色状态几乎都有个长期存在的状态idle

技能释放完毕后回到idle

死亡复活后回到idle

跳跃落地后回到idle

所以我们有了栈式轮询状态机的需求,大家可以看下和我使用栈式FSM思路不同的肛少的技能系统思路,栈式状态机概念我也是从肛少博客了解而来。

如果我们用一个栈来管理状态机的轮询

轮询时只对栈顶状态轮询

就像这样

初始入栈idle,入栈Run,轮询Run。

接受到释放Skill1,出栈run,入栈skill1,轮询skill1。

skill1结束,出栈skill1,轮询idle

栈式状态机工作图示

这样我们的状态机就有了一个天然的属性

就是除了栈底状态的所有状态都会指向栈底状态

栈式状态机天然属性之回归线

如下图:

栈式轮询状态机天然状态示例图

栈式状态机天然属性之保存状态

可以做到保存上一个状态让上一个状态无论如何可以完整的进行

例如实现一个可以在被中断之后继续续上的冲锋技能:

初始入栈idle,轮询idle。

释放冲锋(可被保存的状态),入栈冲锋,轮询冲锋

释放skill1,入栈skll1,轮询skill1

skill1结束,出栈skill1,继续轮询冲锋

冲锋结束,出栈冲锋,轮询idle

保存状态示意图

具体实现与使用

关于动画

我没有使用动画状态机

而是通过角色的FSM去播放动画

动画的播放与FadeInOut我将之托管给了Animancer

有兴趣的小伙伴可以详细了解一下这个插件,谁用谁知道

所以我几乎完全弃用了Unity Animator

这样我对动画的控制就简化为如下操作,并且拥有已经配置好的FadeInOut效果

如果有更多的播放动画的细分需求请读者自行查阅animancer api

用逻辑状态机去控制动画的播放关键点在于---需要有一个完备的动画模块可以让你想怎么播动画就怎么播动画

fsm.Owner.CachedAnimancer.Play(动画名);

另外在MDDSkillEngine中,角色动画仅受FSM的驱动(这句话很重要)。

关于状态机基本结构与实现

MDDSkillEngine是基于GameFramework制作的

GameFramework自带一个非常纯净的FSM

所以MDDSkillEngine中使用的FSM是在GameFramework的FSM之上改装而来

主要做的改变有4点

1.将轮询结构改为栈式轮询

2.为FSM添加黑板将轮询判定跳转改为监听式

3.增加可保存,栈底,是否可中断等状态特性,以及自身结束出栈方法

4.增加了一个靠打标签反射初始化的初始化流程

其中增加可保存状态,栈底状态以及结束出栈方法都是为了适配栈式状态机的特性

跳转方法与结束状态生命周期方法

        /// <summary>
        /// 结束当前状态的生命周期
        /// </summary>
        /// <exception cref="MDDGameFrameworkException"></exception>
        internal void FinishState()
        {
            if (m_CurrentState == null)
            {
                throw new MDDGameFrameworkException("Current state is invalid.");
            }

            if (!m_StateStack.Peek().IsButtomState)
            {
                m_StateStack.Peek().OnLeave(this, false);
                m_StateStack.Pop();
                m_StateStack.Peek().OnEnter(this);
            }      
        }

        /// <summary>
        /// 切换当前有限状态机状态。
        /// </summary>
        /// <param name="stateType">要切换到的有限状态机状态类型。</param>
        internal void ChangeState(Type stateType)
        {         
            if (m_CurrentState == null)
            {
                throw new MDDGameFrameworkException("Current state is invalid.");
            }

            if (m_CurrentState.CantStop)
            {
                return;
            }

            FsmState<T> state = GetState(stateType);
            if (state == null)
            {
                throw new MDDGameFrameworkException(Utility.Text.Format("FSM '{0}' can not change state to '{1}' which is not exist.", new TypeNamePair(typeof(T), Name), stateType.FullName));
            }

            //如果状态无法中断则返回操作
            if (m_StateStack.Peek().CantStop)
            {
                return;
            }

            //如果为栈底状态则清空其他所有状态直接进入栈底状态
            if (state.IsButtomState)
            {
                foreach (var item in m_StateStack)
                {
                    if (!item.IsButtomState)
                    {
                        item.OnLeave(this, false);
                    }
                }
                m_StateStack.Clear();
                m_StateStack.Push(state);
                m_CurrentStateTime = 0f;
                m_StateStack.Peek().OnEnter(this);
                return;
            }

            //如果为可保存状态则正常切换
            if (!m_StateStack.Peek().StrongState && !m_StateStack.Peek().IsButtomState)
            {               
                m_StateStack.Peek().OnLeave(this, false);
                m_CurrentStateTime = 0f;
                m_StateStack.Pop();
                m_StateStack.Push(state);
                m_StateStack.Peek().OnEnter(this);
            }
            else//如果为可保存状态则保存状态不出栈
            {
                m_StateStack.Push(state);
                m_StateStack.Peek().OnEnter(this);
            }            
        }


状态基类

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

namespace MDDGameFramework
{
    public abstract class FsmState
    {
        
    }

    /// <summary>
    /// 有限状态机状态基类。
    /// </summary>
    /// <typeparam name="T">有限状态机持有者类型。</typeparam>
    public abstract class FsmState<T> : FsmState where T : class
    {
        /// <summary>
        /// 该状态是否可以保存
        /// </summary>
        private bool m_StrongState;

        /// <summary>
        /// 该状态是否可被中断
        /// </summary>
        private bool m_CantStop;

        /// <summary>
        /// 该状态是否为栈底状态
        /// </summary>
        private bool m_IsButtomState;

        public virtual bool StrongState
        {
            get { return m_StrongState; }
            set { m_StrongState = value; }
        }

        public virtual bool CantStop
        {
            get { return m_CantStop; }
            set { m_CantStop = value; }
        }

        public virtual bool IsButtomState
        {
            get { return m_IsButtomState; }
            set { m_IsButtomState = value; }
        }

        public float duration = 0f;

        /// <summary>
        /// 初始化有限状态机状态基类的新实例。
        /// </summary>
        public FsmState()
        {
        }

        /// <summary>
        /// 有限状态机状态初始化时调用。
        /// </summary>
        /// <param name="fsm">有限状态机引用。</param>
        protected internal virtual void OnInit(IFsm<T> fsm)
        {
        }

        /// <summary>
        /// 有限状态机状态进入时调用。
        /// </summary>
        /// <param name="fsm">有限状态机引用。</param>
        protected internal virtual void OnEnter(IFsm<T> fsm)
        {
        }

        /// <summary>
        /// 有限状态机状态轮询时调用。
        /// </summary>
        /// <param name="fsm">有限状态机引用。</param>
        /// <param name="elapseSeconds">逻辑流逝时间,以秒为单位。</param>
        /// <param name="realElapseSeconds">真实流逝时间,以秒为单位。</param>
        protected internal virtual void OnUpdate(IFsm<T> fsm, float elapseSeconds, float realElapseSeconds)
        {
            duration += elapseSeconds;
        }

        /// <summary>
        /// 有限状态机状态物理轮询时调用。
        /// </summary>
        /// <param name="fsm">有限状态机引用。</param>
        /// <param name="elapseSeconds">逻辑流逝时间,以秒为单位。</param>
        /// <param name="realElapseSeconds">真实流逝时间,以秒为单位。</param>
        protected internal virtual void OnFixedUpdate(IFsm<T> fsm, float elapseSeconds, float realElapseSeconds)
        {
        }

        /// <summary>
        /// 有限状态机状态离开时调用。
        /// </summary>
        /// <param name="fsm">有限状态机引用。</param>
        /// <param name="isShutdown">是否是关闭有限状态机时触发。</param>
        protected internal virtual void OnLeave(IFsm<T> fsm, bool isShutdown)
        {
            duration = 0f;
        }

        /// <summary>
        /// 有限状态机状态销毁时调用。
        /// </summary>
        /// <param name="fsm">有限状态机引用。</param>
        protected internal virtual void OnDestroy(IFsm<T> fsm)
        {
        }

        protected void Finish(IFsm<T> fsm) 
        {
            Fsm<T> fsmImplement = (Fsm<T>)fsm;

            if (fsmImplement == null)
            {
                throw new MDDGameFrameworkException("FSM is invalid.");
            }

            fsmImplement.FinishState();
        }

        /// <summary>
        /// 切换当前有限状态机状态。
        /// </summary>
        /// <typeparam name="TState">要切换到的有限状态机状态类型。</typeparam>
        /// <param name="fsm">有限状态机引用。</param>
        protected void ChangeState<TState>(IFsm<T> fsm) where TState : FsmState<T>
        {
            Fsm<T> fsmImplement = (Fsm<T>)fsm;
            if (fsmImplement == null)
            {
                throw new MDDGameFrameworkException("FSM is invalid.");
            }

            fsmImplement.ChangeState<TState>();
        }

        /// <summary>
        /// 切换当前有限状态机状态。
        /// </summary>
        /// <param name="fsm">有限状态机引用。</param>
        /// <param name="stateType">要切换到的有限状态机状态类型。</param>
        protected void ChangeState(IFsm<T> fsm, Type stateType)
        {
            Fsm<T> fsmImplement = (Fsm<T>)fsm;
            if (fsmImplement == null)
            {
                throw new MDDGameFrameworkException("FSM is invalid.");
            }

            if (stateType == null)
            {
                throw new MDDGameFrameworkException("State type is invalid.");
            }

            if (!typeof(FsmState<T>).IsAssignableFrom(stateType))
            {
                throw new MDDGameFrameworkException(Utility.Text.Format("State type '{0}' is invalid.", stateType.FullName));
            }

            fsmImplement.ChangeState(stateType);
        }
    }
}

角色以及技能状态特化基类

using MDDGameFramework;
using System;
using MDDGameFramework.Runtime;
using UnityEngine;

namespace MDDSkillEngine
{
    public abstract class MDDFsmState<T> : FsmState<T> where T : Entity
    {
        protected IFsm<T> Fsm;
        protected float FinishTime = 0f;
        protected override void OnInit(IFsm<T> fsm)
        {
            base.OnInit(fsm);
            Fsm = fsm;
            //向黑板添加默认的状态跳转bool值
            fsm.SetData<VarBoolean>(GetType().Name, false);
            //添加观察者,监听是否有跳转到此状态的请求
            fsm.AddObserver(GetType().Name, Observing);
            Log.Info("{0}设置默认状态黑板变量{1}", LogConst.FSM, GetType().Name);
        }
       
        protected override void OnLeave(IFsm<T> fsm, bool isShutdown)
        {
            base.OnLeave(fsm, isShutdown);
            fsm.SetData<VarBoolean>(GetType().Name, false)
        }

        /// <summary>
        /// 状态跳转的依据函数
        /// </summary>
        /// <param name="type">黑板值变化类型</param>
        /// <param name="newValue">值</param>
        protected abstract void Observing(Blackboard.Type type, Variable newValue);


    }
}

黑板以及FSM本体源码过长就不放在文章了,感兴趣的小伙伴可以直接去看源码。

一个原初版本的角色状态原型

这是一个最开始制作MDDSkillEngine的简陋的技能状态,已经弃用

但是已经涵盖了核心思路,很适合用来做展示模型,类似于原初版本

其中状态驱动了碰撞体特效的生成动画的播放

同时在状态内部定义了剑刃风暴技能的碰撞的函数

在现在版本的MDDSkillEngine中大部分数据都会交给timeline编辑,并且会带来更多的功能,驱动依旧是FSM

其中碰撞函数,在实际演示demo中会直接抽成一个自定义的BUFF!!!!!!!!!

所有的打击效果,交由buff处理,即所有的碰撞效果都是往被击打方或者双方添加对应的自定义buff

这样所有技能的打击效果的编写就独立出来与其他系统完全解耦可以单独维护


namespace MDDSkillEngine
{
    [AkiState]
    public class Akijianrenfengbao : MDDFsmState<Entity>
    {
        //将技能设为不可中断
        public override bool CantStop
        {
            get
            {
                return true;
            }
        }

        private ClipState.Transition jianrenfengbao;

        private EventHandler<GameEventArgs> colliderAction;


        protected override void OnInit(IFsm<Entity> fsm)
        {
            base.OnInit(fsm);
            Log.Info("创建剑刃风暴状态。");
            jianrenfengbao = fsm.Owner.CachedAnimContainer.GetAnimation("jianrenfengbao");
            colliderAction = ColliderStayAction;
          
            Fsm = fsm;
        }

        protected override void OnEnter(IFsm<Entity> fsm)
        {
            duration = 5f;

            base.OnEnter(fsm);

            int effid;
            int colid;

            effid = Game.Entity.GenerateSerialId();
            colid = Game.Entity.GenerateSerialId();

            //动画
            fsm.Owner.CachedAnimancer.Play(jianrenfengbao);

            //特效
            Game.Entity.ShowEffect(new EffectData(effid, 70005)
            {
                Position = fsm.Owner.CachedTransform.position,
                Rotation = fsm.Owner.CachedTransform.rotation,
                KeepTime = 5f
            });

            //碰撞体
            Game.Entity.ShowCollider(new ColliderData(colid, 20000, fsm.Owner)
            {
                Rotation = fsm.Owner.CachedTransform.rotation,
                Position = fsm.Owner.CachedTransform.position + new Vector3(0f, 0.5f, 0),
                LocalScale = new Vector3(1f, 1f, 1f),
                Duration = 5f
            });

            Game.Event.Subscribe(ColliderEnterEventArgs.EventId, colliderAction);         
        }

        protected override void OnDestroy(IFsm<Entity> fsm)
        {
            base.OnDestroy(fsm);
            Log.Debug("销毁剑刃风暴状态。");
        }

        protected override void OnLeave(IFsm<Entity> fsm, bool isShutdown)
        {
            base.OnLeave(fsm, isShutdown);
            Game.Event.Unsubscribe(ColliderEnterEventArgs.EventId, colliderAction);
            fsm.SetData<VarBoolean>("jianrenfengbao", false);
            Log.Debug("离开剑刃风暴。");
        }

        protected override void OnUpdate(IFsm<Entity> fsm, float elapseSeconds, float realElapseSeconds)
        {
            base.OnUpdate(fsm, elapseSeconds, realElapseSeconds);

            if (duration >= 5f)
            {
                Finish(fsm);             
            }
        }

        //碰撞事件
        private void ColliderStayAction(object sender, GameEventArgs e)
        {
            ColliderEnterEventArgs col = (ColliderEnterEventArgs)e;

            if (col.Owner != Fsm.Owner)
            {
                return;
            }

            int entityDamageHP = AIUtility.CalcDamageHP(5, 0);

            TargetableObject target = col.Other as TargetableObject;

            target.ApplyDamage(target, entityDamageHP);

            if (target.IsDead)
            {
                Game.Fsm.GetFsm<Entity>(target.Id.ToString()).SetData<VarBoolean>("died", true);
                Game.Entity.HideEntity((Entity)sender);
                return;
            }

            Game.Buff.AddBuff(target.Id.ToString(), "Dubao", col.Other, col.Owner);

            Game.Fsm.GetFsm<Entity>(target.Id.ToString()).SetData<VarBoolean>("damage", true);
        }


        /// <summary>
        /// 状态跳转
        /// 基于黑板的观察函数
        /// </summary>
        /// <param name="type"></param>
        /// <param name="newValue"></param>
        protected override void Observing(Blackboard.Type type, Variable newValue)
        {
            VarBoolean varBoolean = (VarBoolean)newValue;

            if (varBoolean.Value == false)
                return;

            //可以根据需求自定跳转条件
            ChangeState(Fsm, GetType());
        }

    }
}

标签分类设计

因为要做到dota2中技能和英雄随意搭配的效果

所以要对实体特化状态与通用的技能状态进行分类

实体特化状态

比如卡尔的idle和run状态这一类本身的运动的非技能状态可以作为特化状态

这样我们可以保证每个角色有一定的特化状态来维持这个角色的定义

当然如果游戏设计者非要将所有状态都作为一个通用技能状态,比如将run也当成技能处理也不是不行

MDDSkillEngine只是提供一个架子,定义权还是在游戏作者手里

特化状态标签图例

通用技能状态

通用技能状态标签图例

优势效果展示

因为MDDSkillEngine主逻辑就是基于FSM驱动

所以天然的对技能的切换和取消有着支持

例如dota2中S技能的效果

S技能效果GIF

在技能前摇阶段按下s可以取消技能释放且不进入cd

在MDDSkillEngine中可以非常轻松的支持这种效果

所以我也认为这也是一个天然适合ACT的技能框架,但是因为时间问题目前没有提供demo

如何制定技能前摇与后摇并且与技能cd进行配合

将在之后引入timeline编辑文章中详细叙述


记录历程,整理思路,共享知识,分享思维。