MDD

将timeline引入技能系统的意义就是为了更好的技能表现。

技术选型以及概念

技术选型

timeline插件我决定选用Slate,是一个开源的timeline插件

其比unity自带的timeline更轻量更易于改造和扩展

具体详情,可以看Slate源码解读

概念

在寻常情况下

当我们引入timeline时会发现timeline什么都能驱动

所以很可能会出现这种形式,将状态也放进timeline驱动

在MDDSkillEngine中错误的示范

可以看到在示例图中timeline几乎驱动了一切,将状态也放进了轨道

但是这对我来说有一个疑问

如果这样做,那状态本身驱动什么呢,仿佛状态就成了一个标记失去了存在的大部分意义

MDDSkillEngine中的用法-timeline编辑的及角色状态本身

将状态轨道去掉

然后将一段timeline就当成一个角色状态来编辑处理

卡尔寒冰之墙SkillTimeline示意图

一段timeline就是一个状态!

这样我们就需要做一个适配工作

即如何让timeline编辑好的逻辑跑在状态机中,细节将在稍后叙述

接下来我们需要捋一下,适配技能我们需要哪些轨道功能

适配技能的轨道划分以及部分细节

动画轨道

这是一个朴实的轨道,当时间轴走到相应的动画块进行播放即可

其中淡入淡出交由动画模块处理

注意:一般不让动画影响本身影响角色的位移和旋转

碰撞体轨道

碰撞体轨道的作用就是生成碰撞体

其中轨道的上的每个clip都能生成一个碰撞体

可以配置多个轨道

通过编写轨道逻辑我们可以让碰撞体生成到理想的位置

可以做到通过表现可视化非常精准的控制碰撞体生成的位置与大小

生成碰撞盒示意图

为碰撞体轨道引入贝塞尔曲线位移编辑

碰撞体clip编辑界面
贝塞尔曲线作用

勾选了速度选项会将贝塞尔曲线位移逻辑下放到碰撞体脚本上

如果不勾选则由timeline驱动

适配碰撞体基本参数

预制名
逻辑脚本
触发碰撞后逻辑

直接通过对Clip的编辑以下配置:

碰撞体预制

碰撞体挂载逻辑

碰撞后逻辑

这样我们就获得了一个灵活的配置方式

把碰撞体预制的制作---单纯的模型属性,例如形状材质

碰撞体逻辑的编写---一般指碰撞体自身的行为,例如碰撞体上存在一个ai效果

碰撞后的逻辑

拆开解耦,分别制作

特效轨道

特效轨道的功能是碰撞体轨道的子集

就不详细说明了

CD轨道

这个是为了适配某些游戏的特殊需求而制作的轨道

比如dota2中抬手但是没有释放技能时

技能是没有进入cd的

cd轨道

如果我们在进入cd clip技能之前中止技能(也就是切换状态) 则技能不会进入cd

这里又衍生出了另一个问题

就是在之前提到的 技能的释放前逻辑是由行为树管理的

那么技能行为树就需要接收到技能是否释放成功的信号来决定技能是否进入cd

这就需要解决一下状态机与行为树的通话问题

具体解决方案会在后续文章中写出

FadeInOut轨道

主要用来控制技能在何时是不会被打断的

fadeinout轨道
type选择

结束当前状态轨道

让我们看一个非常常见的案例技能

卡尔的电磁脉冲 www

电磁脉冲timeline示例图

kaer的电磁脉冲会在施法动作完成以后差不多2秒才产生实际技能效果 生成造成伤害并且削蓝的检测体

但是这一非常常见的技能中的人物状态切换效果会在检测体生成之前早早完成

施法状态可能就持续半秒,效果产生却在两秒以后

所以我们需要为timeline指定一个状态结束的时间点功能,并且需要保留timeline后续逻辑的驱动

状态结束轨道

为了做到能够在状态切换完成后保留之后的timeline逻辑,我选择在FSM中开启一个协程去跑timeline逻辑

具体实现会在后面代码解析中给出

这个实现其实可以通过魔法弹避免,比如在技能状态时间内就生成一个延迟触发的碰撞体,但是我为了自己舒服还是这么写了,因为这样可以快速做到所见即所得,往后MDDSkillEngine被更多的业务驱动更改以后,这个设计可能会改变,所以希望读者多多思考得失,我也希望能够抛砖引玉

实体生成轨道

实体轨道主要用来生成召唤物类的物体

人物贝塞尔曲线位移轨道-暂时未作

镜头轨道-暂时未做

蓄力轨道-暂时未做

声音轨道-暂时未作

旋转轨道-暂时未作

代码实现思路

大家在博客里看个思路即可

阅读代码可以直接下载源码进工程看

在博客中展示的代码只是为了展现一个关键步骤,打字表达太累了><

1.Editor模式下的表现完全由Slate托管

slate自带runtime和editor支持

这边我只取用slate的editor支持

如果需要新的轨道支持也就是和扩展普通slate轨道一般无二,只是我们无需考虑表现轨道对runtime的适配。

2.将Slate的数据导出

Editor数据导出没什么好说的

是一个老牛搬砖的活老老实实一个个导出即可

代码如下:

其中序列化工具为OdinSerializer,其功能强大在纯客户端的情况下谁用谁知道

using OdinSerializer;
using Slate;
using Slate.ActionClips;
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEngine;


namespace MDDSkillEngine
{
    public class MDDSkillDataGenerator : EditorWindow
    {      
        /// <summary>
        /// 处理技能数据
        /// 导出slate中的timeline数据到 skilldata中
        /// </summary>
        [MenuItem("MDDSkillEngine/GeneratorSkillData")]
        public static void GeneratorSkillData()
        {
            //路径
            string fullPath = Application.dataPath + "/MDDSkillEngine/SkillRes/";
            string savePath = Application.dataPath + "/MDDSkillEngine/SkillRes/SkillTimelineRuntime/";
            Debug.Log(fullPath);
            //获得指定路径下面的所有资源文件
            if (Directory.Exists(fullPath))
            {
                DirectoryInfo dirInfo = new DirectoryInfo(fullPath);
                FileInfo[] files = dirInfo.GetFiles("*", SearchOption.AllDirectories); //包括子目录
                Debug.Log(files.Length);

                for (int i = 0; i < files.Length; i++)
                {
                    if (files[i].Name.EndsWith(".prefab"))
                    {
                        Debug.Log("预制体名字" + files[i].Name);
                        string path = "Assets/MDDSkillEngine/SkillRes/SkillTimeline/" + files[i].Name;
                        Debug.Log("预制体路径" + path);
                        GameObject obj = AssetDatabase.LoadAssetAtPath(path, typeof(GameObject)) as GameObject;
                        Debug.Log("obj的名字" + obj.name);

                        Cutscene Data = obj.GetComponent<Cutscene>();

                        byte[] bytes = SerializationUtility.SerializeValue(HandleSkillData(Data), DataFormat.Binary);

                        File.WriteAllBytes(savePath + Data.gameObject.name+ ".bytes", bytes);

                    }
                }
            }
            else
            {
                Debug.Log("资源路径不存在");
            }

            Debug.Log("数据导出完成");
        }


        /// <summary>
        /// 返回一份slate数据中的技能导出数据
        /// </summary>
        /// <param name="Data"></param>
        /// <returns></returns>
        public static SkillData HandleSkillData(Cutscene Data)
        {
            List<SkillDataBase> dataList = new List<SkillDataBase>();

            float FinishTime = 0f;
            float CDTime = 0f;

            foreach (var group in Data.groups)
            {
                foreach (var track in group.tracks)
                {
                    switch (track.SkillDataType)
                    {
                        case SkillDataType.None:
                            Debug.LogError($"检测到未指定类型的技能数据{track.name}");
                            continue;
                        case SkillDataType.Effect:
                            {
                                foreach (var clip in track.clips)
                                {
                                    if (clip is EffectInstance)
                                    {
                                        EffectInstance effectInstance = (EffectInstance)clip;
                                        EffectSkillData data = new EffectSkillData();
                                        data.OnInit(effectInstance);
                                        dataList.Add(data);
                                    }
                                }
                                break;
                            }
                        case SkillDataType.Animation:
                            {
                                foreach (var clip in track.clips)
                                {
                                    if (clip is PlayAnimatorClip)
                                    {
                                        PlayAnimatorClip playAnimatorClip = (PlayAnimatorClip)clip;
                                        AnimationSkillData data = new AnimationSkillData();
                                        data.OnInit(playAnimatorClip);
                                        dataList.Add(data);
                                    }
                                }
                                break;
                            }
                        case (SkillDataType.Collider):
                            {
                                foreach (var clip in track.clips)
                                {
                                    if (clip is InstanceCollider)
                                    {
                                        InstanceCollider instanceCollider = (InstanceCollider)clip;
                                        ColliderSkillData data = new ColliderSkillData();
                                        data.OnInit(instanceCollider);
                                        dataList.Add(data);
                                    }
                                }
                                break;
                            }
                        case (SkillDataType.CD):
                            {
                                foreach (var clip in track.clips)
                                {
                                    if (clip is CD)
                                    {
                                        CD cd = (CD)clip;
                                        CDSkillData data = new CDSkillData();
                                        data.OnInit(cd);
                                        dataList.Add(data);
                                        CDTime = clip.startTime;
                                    }
                                }
                                break;
                            }
                        case (SkillDataType.InOut):
                            {
                                foreach (var clip in track.clips)
                                {
                                    if (clip is SkillFadeInOut)
                                    {
                                        SkillFadeInOut skillFadeInOut = (SkillFadeInOut)clip;
                                        FadeInOutSkillData data = new FadeInOutSkillData();
                                        data.OnInit(skillFadeInOut);
                                        dataList.Add(data);
                                    }
                                }
                                break;
                            }
                        case (SkillDataType.FinishState):
                            {
                                foreach (var clip in track.clips)
                                {
                                    if (clip is FinishStateTime)
                                    {
                                        FinishTime = clip.startTime;
                                    }
                                }
                                break;
                            }

                    }
                }
            }

            SkillData resultData = new SkillData(dataList);

            resultData.FinishStateTime = FinishTime;
            resultData.Length = Data.length;
            resultData.targetType = Data._targetType;
            resultData.CDTime = CDTime;

            Debug.LogError(resultData.Length);

            return resultData;
        }

    }

}

3.构建FSM中的TimelineRuntime

直接将slate运行逻辑搬运过来即可

同时因为驱动改为了FSM,所以更加轻量无需生成大量gameobject辅助运行

我偷了个懒直接取消了group和track的概念

只保留了clip

同时暂时没有将回放功能搬入,因为涉及到和命令模式的适配

所以timeline驱动就是驱动所有的clip

TimelineRuntime代码如下:

using MDDGameFramework.Runtime;
using MDDGameFramework;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

namespace MDDSkillEngine
{
    public abstract class SkillTimeline
    {
        /// <summary>
        /// 技能目标类型
        /// </summary>
        public TargetType TargetType;

        /// <summary>
        /// 技能进入cd的时间
        /// </summary>
        public float CDTime = 0f;
        /// <summary>
        /// 用于控制状态是否可以强制切换
        /// </summary>
        /// <param name="b"></param>
        public abstract void SetStateCantStop(bool b);
    }

    /// <summary>
    /// 技能运行时辅助类
    /// 主要用于将slate逻辑复制过来
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public class SkillTimeline<T> : SkillTimeline where T : Entity
    {
        public enum PlayingDirection
        {
            Forwards,
            Backwards
        }

        public T owner;

        public IFsm<T> fsm;

        public List<SkillClip> skillClips = new List<SkillClip>();

        public float currentTime;

        public float lastTime;

        public float length;

        public float previousTime;

        public PlayingDirection playingDirection = PlayingDirection.Forwards;

        public float playbackSpeed = 1f;

        private bool isActive = true;

        private List<IDirectableTimePointer> timePointers = new List<IDirectableTimePointer>();
        private List<IDirectableTimePointer> unsortedStartTimePointers = new List<IDirectableTimePointer>();

        public void Init(IFsm<T> fsm, SkillData skillData)
        {         
            this.fsm = fsm;
            owner = fsm.Owner;

            length = skillData.Length;
            TargetType = skillData.targetType;
            CDTime = skillData.CDTime;

            InitSkillClip(skillData);
        }


        public void Updata(float delta)
        {
            delta *= playbackSpeed;
            currentTime += playingDirection == PlayingDirection.Forwards ? delta : -delta;

            if (currentTime >= length && playingDirection == PlayingDirection.Forwards)
            {
                Sample(currentTime ,false);
                return;
            }

            Sample(currentTime ,true);
        }

        public void Exit()
        {
            currentTime = 0f;
            lastTime = 0f;
            previousTime = 0f;
            isActive = true;

            foreach (var v in timePointers)
            {
                v.isTrigger = false;
            }

            foreach (var v in unsortedStartTimePointers)
            {
                v.isTrigger = false;
            }

            foreach (var v in skillClips)
            {
                v.Close();
            }
        }
      
        /// <summary>
        /// 初始化技能clip
        /// </summary>
        /// <param name="skillData"></param>
        public void InitSkillClip(SkillData skillData)
        {
            foreach (var data in skillData.skillData)
            {
                switch (data.DataType)
                {
                    case SkillDataType.Effect:
                        {
                            EffectClip effectClip = new EffectClip();
                            effectClip.Init(data, owner,this);
                            skillClips.Add(effectClip);
                        }
                        break;
                    case SkillDataType.Animation:
                        {
                            AnimationClip animationClip = new AnimationClip();
                            animationClip.Init(data, owner, this);
                            skillClips.Add(animationClip);
                        }
                        break;
                    case SkillDataType.Collider:
                        {
                            ColliderClip colliderClip = new ColliderClip();
                            colliderClip.Init(data, owner, this);
                            skillClips.Add(colliderClip);
                        }
                        break;
                    case SkillDataType.CD:
                        {
                            CDClip cdClip = new CDClip();
                            cdClip.Init(data, owner, this);
                            skillClips.Add(cdClip);
                        }
                        break;
                    case SkillDataType.InOut:
                        {
                            FadeInOutClip fadeInOutClip = new FadeInOutClip();
                            fadeInOutClip.Init(data, owner, this);
                            skillClips.Add(fadeInOutClip);
                        }
                        break;
                    case SkillDataType.Entity:
                        {
                            EntityClip entityClip = new EntityClip();
                            entityClip.Init(data, owner, this);
                            skillClips.Add(entityClip);
                        }
                        break;

                }
            }

          
            InitTimePointer();

            Log.Info("{0}初始化Skilltimeline:name:{1} 成功.", LogConst.SKillTimeline, owner.Name);
        }

        /// <summary>
        /// 初始化时间点
        /// </summary>
        private void InitTimePointer()
        {
            foreach (var item in skillClips)
            {
                var timePointer = new StartTimePointer(item);

                timePointers.Add(timePointer);
                timePointers.Add(new EndTimePointer(item));

                unsortedStartTimePointers.Add(timePointer);                
            }

            timePointers = timePointers.OrderBy(p => p.time).ToList();
        }

        private void Sample(float time,bool b)
        {
            currentTime = time;

            if (!isActive)
                return;

            isActive = b;

            //ignore same minmax times
            if ((currentTime == 0 || currentTime == length) && previousTime == currentTime)
            {
                return;
            }

            //Sample pointers
            if (timePointers != null)
            {
                //Log.Error("Sample pointers");
                Internal_SamplePointers(currentTime, previousTime);
            }


            previousTime = currentTime;
        }

        private void Internal_SamplePointers(float currentTime, float previousTime)
        {
            if (!Application.isPlaying || currentTime > previousTime)
            {
                for (var i = 0; i < timePointers.Count; i++)
                {
                    try
                    {
                        //Debug.LogError("TriggerForward");
                        timePointers[i].TriggerForward(currentTime, previousTime);
                    }
                    catch (System.Exception e) { Debug.LogException(e); }
                }
            }

            //Update timePointers triggering backwards
            if (!Application.isPlaying || currentTime < previousTime)
            {
                for (var i = timePointers.Count - 1; i >= 0; i--)
                {
                    try { timePointers[i].TriggerBackward(currentTime, previousTime); }
                    catch (System.Exception e) { Debug.LogException(e); }
                }
            }

            //Update timePointers
            if (unsortedStartTimePointers != null)
            {
                for (var i = 0; i < unsortedStartTimePointers.Count; i++)
                {
                    try { unsortedStartTimePointers[i].Update(currentTime, previousTime); }
                    catch (System.Exception e) { Debug.LogException(e); }
                }
            }
        }

        public override void SetStateCantStop(bool b)
        {
            Log.Info("{0}设置状态:{1},cantstop:{2}",LogConst.FSM, fsm.CurrentState.GetType(), b);
            fsm.CurrentState.CantStop = b;
        }
    }


    public abstract class SkillClip
    {
        public Entity actor;

        public SkillTimeline skillTimeline;

        public float duration;

        public float time;

        public float startTime;

        public float endTime;

        virtual public void Init(SkillDataBase data, Entity actor, SkillTimeline skillTimeline)
        {
            startTime = data.StartTime;
            endTime = data.EndTime;
        }

        abstract public void Enter();

        virtual public void Close()
        {
            time = 0;
            duration = 0;
        }

        virtual public void Exit()
        {
        }
        virtual public void Update(float currentTime, float previousTime)
        {
            time += currentTime;
        }
    }


    ///An interface for TimePointers (since structs can't be abstract)
    public interface IDirectableTimePointer
    {
        bool isTrigger { get; set; }
        SkillClip target { get; }
        float time { get; }
        void TriggerForward(float currentTime, float previousTime);
        void TriggerBackward(float currentTime, float previousTime);
        void Update(float currentTime, float previousTime);
    }

    ///----------------------------------------------------------------------------------------------

    ///Wraps the startTime of a group, track or clip (IDirectable) along with it's relevant execution
    public struct StartTimePointer : IDirectableTimePointer
    {
        public bool isTrigger 
        {
            get
            {
                return triggered;
            }
            set
            {
                triggered = value;
            }
        }

        public bool triggered;
        private float lastTargetStartTime;
        public SkillClip target { get; private set; }
        float IDirectableTimePointer.time { get { return target.startTime; } }

        public StartTimePointer(SkillClip target)
        {
            this.target = target;
            triggered = false;
            lastTargetStartTime = target.startTime;
        }

        //...
        void IDirectableTimePointer.TriggerForward(float currentTime, float previousTime)
        {
            if (currentTime >= target.startTime)
            {
                if (!triggered)
                {
                    //Debug.LogError($"triggered{target.name}");
                    triggered = true;
                    target.Enter();
                    target.Update(target.ToLocalTime(currentTime), 0);
                }
            }
        }

        //...
        void IDirectableTimePointer.Update(float currentTime, float previousTime)
        {

            //update target and try auto-key
            if (currentTime >= target.startTime && currentTime < target.endTime && currentTime > 0)
            {

                var deltaMoveClip = target.startTime - lastTargetStartTime;
                var localCurrentTime = target.ToLocalTime(currentTime);
                var localPreviousTime = target.ToLocalTime(previousTime + deltaMoveClip);


                target.Update(localCurrentTime, localPreviousTime);
                lastTargetStartTime = target.startTime;
            }
        }

        //...
        void IDirectableTimePointer.TriggerBackward(float currentTime, float previousTime)
        {
            if (currentTime < target.startTime || currentTime <= 0)
            {
                if (triggered)
                {
                    triggered = false;
                    target.Update(0, target.ToLocalTime(previousTime));
                    //target.Reverse();
                }
            }
        }
    }

    ///----------------------------------------------------------------------------------------------

    ///Wraps the endTime of a group, track or clip (IDirectable) along with it's relevant execution
    public struct EndTimePointer : IDirectableTimePointer
    {
        public bool isTrigger
        {
            get
            {
                return triggered;
            }
            set
            {
                triggered = value;
            }
        }

        private bool triggered;
        public SkillClip target { get; private set; }
        float IDirectableTimePointer.time { get { return target.endTime; } }

        public EndTimePointer(SkillClip target)
        {
            this.target = target;
            triggered = false;
        }

        //...
        void IDirectableTimePointer.TriggerForward(float currentTime, float previousTime)
        {
            if (currentTime >= target.endTime)
            {
                if (!triggered)
                {
                    triggered = true;
                    target.Update(target.GetLength(), target.ToLocalTime(previousTime));
                    target.Exit();
                }
            }
        }

        //...
        void IDirectableTimePointer.Update(float currentTime, float previousTime)
        {
            //Update is/should never be called in TimeOutPointers
            throw new System.NotImplementedException();
        }

        //...
        void IDirectableTimePointer.TriggerBackward(float currentTime, float previousTime)
        {
            if (currentTime < target.endTime)
            {
                if (triggered)
                {
                    triggered = false;
                    //target.ReverseEnter();
                    target.Update(target.ToLocalTime(currentTime), target.GetLength());
                }
            }
        }
    }
}






技能状态基类代码

为了快速适配协程代码写的有点丑陋大家见谅

using MDDGameFramework;
using MDDGameFramework.Runtime;
using Sirenix.Serialization;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace MDDSkillEngine
{
    public abstract class SkillTimelineState<T> : MDDFsmState<T> where T : Entity
    {
        /// <summary>
        /// 一个简易的timeline池子
        /// </summary>
        private Queue<SkillTimeline<T>> skillTimelineQuene;

        /// <summary>
        /// 在运行中的协程字典
        /// </summary>
        private Dictionary<SkillTimeline<T>, CoroutineHandler> coroutineHandlerDic;

        /// <summary>
        /// 与当前激活状态相匹配的timline
        /// </summary>
        private SkillTimeline<T> currentSkillTimeline;

        /// <summary>
        /// skilltimeline数据资源加载回调
        /// </summary>
        private LoadBinaryCallbacks assetCallbacks;

        /// <summary>
        ///  skilltimeline数据
        /// </summary>
        private SkillData skillData;

        /// <summary>
        /// 缓存一下委托防止频繁GC
        /// </summary>
        private System.Action<bool> SkillTimelineEndCallBack;

       
        private float Duration;

        protected override void OnInit(IFsm<T> fsm)
        {
            base.OnInit(fsm);
            skillTimelineQuene = new Queue<SkillTimeline<T>>();
            coroutineHandlerDic = new Dictionary<SkillTimeline<T>, CoroutineHandler>();
            assetCallbacks = new LoadBinaryCallbacks(LoadCallBack);
            Game.Resource.LoadBinary(AssetUtility.GetSkillTimelineAsset(GetType().Name), assetCallbacks);
        }

        protected override void OnEnter(IFsm<T> fsm)
        {
            base.OnEnter(fsm);

            //根据timeline池开启协程
            if (skillTimelineQuene.Count == 0)
            {
                SkillTimeline<T> skillTimeline = new SkillTimeline<T>();
                skillTimeline.Init(fsm, skillData);
                //开启协程
                currentSkillTimeline=skillTimeline; 
                coroutineHandlerDic.Add(skillTimeline, UpdateSkillTimeline(skillTimeline).Start());
                Log.Info("{0}储备不足创建新的skilltimeline", LogConst.FSM);
            }
            else
            {
                SkillTimeline<T> skillTimeline = skillTimelineQuene.Dequeue();
                //开启协程
                currentSkillTimeline = skillTimeline;
                coroutineHandlerDic.Add(skillTimeline, UpdateSkillTimeline(skillTimeline).Start());              
                Log.Info("{0}使用储备的skilltimeline", LogConst.FSM);
            }
        }

        protected override void OnUpdate(IFsm<T> fsm, float elapseSeconds, float realElapseSeconds)
        {
            base.OnUpdate(fsm, elapseSeconds, realElapseSeconds);
            Duration += elapseSeconds;

            //如果没添加状态结束轨道则根据timeline持续时间结束状态
            if (FinishTime != 0f)
            {
                if (Duration >= FinishTime)
                {
                    Finish(fsm);
                }
            }           
        }

        protected override void OnLeave(IFsm<T> fsm, bool isShutdown)
        {
            base.OnLeave(fsm, isShutdown);

            if (currentSkillTimeline != null)
            {
                //适配cdtrack
                if (currentSkillTimeline.CDTime >= Duration)
                {
                    currentSkillTimeline.Exit();
                    Game.Coroutine.StopCoroutine(coroutineHandlerDic[currentSkillTimeline].CoroutineWapper);
                    ReferencePool.Release(coroutineHandlerDic[currentSkillTimeline]);
                    coroutineHandlerDic.Remove(currentSkillTimeline);
                    skillTimelineQuene.Enqueue(currentSkillTimeline);
                }
            }

            Duration = 0f;
        }

        /// <summary>
        /// skilltimeline驱动函数
        /// </summary>
        /// <param name="skillTimeline"></param>
        /// <returns></returns>
        public IEnumerator UpdateSkillTimeline(SkillTimeline<T> skillTimeline)
        {
            float DurationTime = 0;

            while (DurationTime <= skillTimeline.length)
            {               
                skillTimeline.Updata(Time.deltaTime);
                DurationTime += Time.deltaTime;
                yield return YieldHelper.WaitForEndOfFrame;
            }

            skillTimeline.Exit();

            ReferencePool.Release(coroutineHandlerDic[skillTimeline]);
            coroutineHandlerDic.Remove(skillTimeline);           
            skillTimelineQuene.Enqueue(skillTimeline);
            Log.Info("{0}回收skilltimeline{1}.count{2}", LogConst.SKillTimeline, DurationTime, skillTimelineQuene.Count);
        }



        private void LoadCallBack(string entityAssetName, object entityAsset, float duration, object userData)
        {
            Log.Info("{0}加载skilltimeline数据成功 name:{1}", LogConst.SKillTimeline, entityAssetName);

            byte[] data = entityAsset as byte[];

            skillData = SerializationUtility.DeserializeValue<SkillData>(data, DataFormat.Binary);

            if (skillData == null)
            {
                Log.Error("{0}数据转化失败 name:{1}", LogConst.SKillTimeline, entityAssetName);
            }
            FinishTime = skillData.FinishStateTime;
            SkillTimeline<T> skillTimeline = new SkillTimeline<T>();
            skillTimeline.Init(Fsm, skillData);
            skillTimelineQuene.Enqueue(skillTimeline);
        }
    }
}

为了管理协程,能够暂停协程引入了如下关键代码

        /// <summary>
        /// 用于在untiy自带协程上再进行一次封装
        /// 给unity自带协程扩展一些生命周期
        /// </summary>
        /// <returns></returns>
        IEnumerator CallWrapper()
        {
            yield return null;
            IEnumerator e = Coroutine;
            while (Running)
            {
                if (Paused)
                    yield return null;
                else
                {
                    if (e != null && e.MoveNext())
                    {
                        yield return e.Current;
                    }
                    else
                    {
                        Running = false;
                    }
                }
            }
            CompletedAction?.Invoke(Stopped);
        }

ColliderClip runtimeCode

主要展示如何将timeline 具体的Clip逻辑搬入自定义runtime

本质上是对editor运行逻辑的复刻

using System;
using System.Collections.Generic;
using Animancer;
using MDDGameFramework.Runtime;
using UnityEngine;
using MDDGameFramework;

namespace MDDSkillEngine
{
    public class ColliderClip : SkillClip
    {
        ColliderSkillData skillData;

        int id;

        List<Vector3> bezierPath = new List<Vector3>();

        public override void Init(SkillDataBase data, Entity actor, SkillTimeline skillTimeline)
        {
            base.Init(data, actor, skillTimeline);
            skillData = data as ColliderSkillData;
            this.skillTimeline = skillTimeline;
            this.actor = actor;
        }

        public override void Enter()
        {
            id = Game.Entity.GenerateSerialId();

            Game.Entity.ShowCollider(Utility.Assembly.GetType(Utility.Text.Format("MDDSkillEngine.{0}", skillData.ColliderLogic)), skillData.ColliderName, new ColliderData(id, 0, actor)
            {
                targetType = skillTimeline.TargetType,
                HitEffectName = skillData.EffectName,
                buffName = skillData.AddBuffName,
                localRotation = skillData.localRotation,
                localScale = skillData.localScale,
                localeftPostion = skillData.localeftPostion,
                boundCenter = skillData.boundCenter,
                boundSize = skillData.boundSize,
                height = skillData.height,
                radius = skillData.redius,
                Duration = this.GetLength(),
                hasPath = skillData.hasPath,
                useSpeed = skillData.useSpeed,
                bezierPath = skillData.bezierPath,
                bezierPathLength = skillData.bezierPathLength,
                bezierPathParentPosition = skillData.bezierPathParentPosition,
                bezierPathParentRotation = skillData.bezierPathParentRotation,
            });



            if (!skillData.useSpeed && skillData.hasPath)
            {
                bezierPath.Clear();

                //坐标转换 将曲线本地坐标转换为世界坐标
                for (int i = 0; i < skillData.bezierPath.Length; i++)
                {
                    Vector3 vec3;
                    vec3 = actor.CachedTransform.TransformPoint(skillData.bezierPath[i]);
                    bezierPath.Add(vec3);
                }
            }

            SkillTimeline<Entity> skillTimeline1 = skillTimeline as SkillTimeline<Entity>;
            Log.Info("{0}进入Colliderclip :{1} currenttime:{2}", LogConst.SKillTimeline, skillData.ResouceName, skillTimeline1.currentTime);
        }

        public override void Update(float currentTime, float previousTime)
        {
            base.Update(currentTime, previousTime);
            // Log.Info("{0}upodateColliderClip name:{1}", LogConst.SKillTimeline, GetType().Name);
            duration += currentTime;

            //利用贝塞尔曲线
            if (!skillData.useSpeed)
            {
                if (skillData.hasPath)
                {
                    if (Game.Entity.HasEntity(id))
                    {
                        Entity entity = Game.Entity.GetGameEntity(id);
                        entity.transform.position = AIUtility.GetPoint(currentTime / this.GetLength(), skillData.bezierPathLength, bezierPath);
                    }
                }
            }
        }

        public override void Exit()
        {
            //Game.Entity.HideEntity(colid);
            base.Exit();
            id = 0;
            Log.Info("{0}离开ColliderClip name:{1}", LogConst.SKillTimeline, GetType().Name);
        }


    }
}


4.一个简易的快速编辑技能状态的场景

通过菜单进入场景

通过菜单进入示例图

进行相关功能的选择

选择相对于的技能进行编辑即可

可以用timeline实现的优秀案例

冰鸭大招!

冰鸭大招GIF

我不知道米哈游具体是怎么实现的

但是这个技能效果用timeline编辑来实现是比较合适的

拆解如下:

进入大招状态(逻辑上切换形态)

动画轨道播放对应动画,视情况引入旋转轨道

理律回旋上升的曲线交给角色位移轨道的贝塞尔曲线编辑即可

碰撞体轨道在理律落地时生成对应检测范围

多特效轨道细致摆放

摄像机轨道细致调教,同时可以给相机轨道加入贝塞尔曲线编辑辅助运镜

在引入timeline编辑后,手巧 + 在线的动画编辑能力 + 对应的美术资源 + MDDSkillEngine就可以复刻冰鸭大招啦。

小结

如果有小伙伴按照目录顺序看到了这里,就已经看完MDDSkillEngine的核心思路了。

很显然MDDSkillEngine需要打磨的地方还多了去了。

但之后我并不打算单纯的继续维护MDDSkillEngine的更新了(可能会手痒把ACTDemo做一做)

我更希望的是我对技能拆解的思路能够起到抛砖引玉的作用。

让更多小伙伴对技能系统的理解更深入一步的同时诞生出更好的想法,我的思路也是受了很多小伙伴的启发。

接下来我会使用MDDSkillEngine投入到具体的游戏制作当中,在制作游戏的同时,顺便让具体的业务去推动MDDSkillEngine的完善。


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