• Unity自定义Timeline总结


    前言

    Timeline最基本的作用是编辑过场动画。实际上任何预定义的线性流程都可以使用Timeline编辑,例如沿固定路线巡逻的敌人。由于Timeline可以同时编辑和播放多条不同类型的轨道,比如动画和声音,并且可以可视化的设置事件发送的点,因此编辑这种预定义流程非常方便。并且由于Timeline可扩展自定义,因此Timeline可用于制作任何配合线性流程的游戏Gameplay。本文正是基于之前的一个小游戏使用自定义Timeline功能的总结,自定义的部分包含PlayableAsset/PlayableBehaviour/TrackAsset/Signal。主要参考资料为Unity Blog:Extending timeline a practical guide

    Timeline基本概念

    Timeline组成

    一个Timeline由一或多个Track组成,而Track又包含了按时间线性播放的Clip。而Clip又分为两类。一是占据了整条Track的无限Clip,例如对于AnimationTrack,进行录制关键帧,就会得到这种包含多个关键帧的无限Clip:
    在这里插入图片描述
    由于这种clip没有明确的长度和结束时间,所以track只能包含一个唯一的无限clip。这个无限clip是不能整体移动,缩放等操作的,如果想进去操作或者在一个track上放置多个clip,则需要将其转换为独立的clip。在无限clip上,右键菜单选择Convert to clip track,则可转换为独立clip:
    在这里插入图片描述
    在Track上可以拖动,改变长度,复制这种独立clip,且clip之间的位置可以重叠,这样他们会进行混合。当然clip是否可以混合是由其实现的ITimelineClipAssetClipCaps决定的,对于动画clip是支持混合的。这儿的独立clip,实际上是一个PlayableAsset,在Timeline编辑器中点击clip会在Inspector窗口显示他的属性,而我们自定义的clip的属性也是在这儿进行操作。
    在这里插入图片描述
    这儿的独立clip,是一个Animation Playable Asset,可以在Inspector中设置clip的属性。

    Timeline Asset

    编辑好的Timeline是以playable为后缀的资源文件存储在Unity工程目录中,这就是Timeline Asset。虽然后缀为playable,但其实这不是只有一个playable,而应该理解为是一组playable的序列的集合,即包含一组Track,每个Track包含一组playable序列。Timeline背后的机制是Playable API,而Timeline的Track上的每一个clip其实是一个对应于Playable API中的Playable对象,这些clip在Timeline Asset中是Playable Asset,例如上面的Animation Playable Asset

    Timeline Instance

    在场景中使用的是Timeline Instance,即在Game Object上添加Playable Director组件,该组件指定一个Timeline Asset即.playable文件。同时如果Timeline Asset中的各个Track需要绑定GameObject或Component作为目标,则还需要设定Playable Director组件中的Bindings。例如:

    在这里插入图片描述
    播放Timeline其实就是使用PlayableDirector组件的Play方法。

    自定义Timeline

    首先,自定义Timeline一定是为了满足某种需求,需要在Timeline编辑器上,编辑特定的Clip,以及对于Track绑定特定的对象。而Timeline中的clip在运行时都是Playable对象,这是Playable API中Playable Graph中的节点。在Playable Graph中,通常有Playable节点和PlayableOutput两类节点。而Mixer节点其实也是一种特殊的Playable节点,比如AnimationPlayableMixer。而我们自定义的Playable节点都是ScriptPlayable类型的节点。ScriptPlayable是一个泛型结构体,其泛型类型是实现IPlayableBehaviour接口的类。

    public struct ScriptPlayable<T> : IPlayable, IEquatable<ScriptPlayable<T>> where T : class, IPlayableBehaviour, new()    
    
    • 1

    Unity提供了一个抽象类PlayableBehaviour实现了IPlayableBehaviour接口,我们要做的就是继承这个抽象类。
    另外,Clip需要保存在资源中,这需要使用PlayableAsset,这同样是来源于Playable API

    	// A base class for assets that can be used to instantiate a Playable at runtime.
        [AssetFileNameExtensionAttribute("playable", new[] { })]
        [RequiredByNativeCodeAttribute]
        public abstract class PlayableAsset : ScriptableObject, IPlayableAsset
    
    • 1
    • 2
    • 3
    • 4

    简单理解一下,PlayableAsset是clip的资源,保存在timeline的playable后缀的资源文件中。而PlayableBehaviour是自定义的Playable行为。PlayableAsset有一个CreatePlayable方法,可以创建出Playable对象。

    自定义PlayableAsset

    public class CustomPlayableAsset : PlayableAsset, ITimelineClipAsset
    {
        public CustomPlayableBehaviour template;
    
        public ClipCaps clipCaps {
            get {
                return ClipCaps.SpeedMultiplier;
            }
        }    
    
        public override Playable CreatePlayable(PlayableGraph graph, GameObject owner)
        {
            var playable = ScriptPlayable<CustomPlayableBehaviour >.Create(graph, template);                    
            return playable;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    一个最简单的自定义PlayableAsset如上。基本上需要做的就是实现CreatePlayable方法,在其中创建出playable对象并返回。由于是自定义的playable,因此创建的是一个ScriptPlayable对象,且泛型参数为自定义的PlayableBehaviour。这儿的template参数,可以使用一个预设好的PlayableBehaviour作为模板来创建playable。新创建出来的playableAsset中的PlayableBehaviour具有和这个模板相同的值。而这个模板可以在Clip的Inspector中编辑。

    自定义PlayableBehaviour

    自定义Timeline Clip的主要数据都是存在这个behaviour中,也就是说,你想在timeline中对什么数据进行K动画,就把它放在这个类中。例如:

    	[System.Serializable]
        public class CustomPlayableBehaviour : PlayableBehaviour
        {
            [Range(0, 1)]
            public float progress = 0f;            
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这是一个极其简单的PlayableBehaviour,只有一个参数progress。在这个小游戏中,是一个预编译好的曲线的进度。通过对progress K动画,可以随着时间控制从曲线上采样出点,然后用这个点去控制相关的对象。正常来说,PlayableBehaviour中需要有相关逻辑进行实际的Play操作,这也正是Playable的含义。其实PlayableBehaviour中有这个方法:

    public virtual void ProcessFrame(Playable playable, FrameData info, object playerData);
    
    • 1

    这相当于在Update这个Playable节点,在其中我们可以使用当前的progress值去采样曲线,然后控制对象。当然我这个例子没在这儿做,因为我们还需要实现MixerBehaviour,而相关的逻辑会在MixerBehaviour中实现。

    实现一个MixerBehaviour

    MixerBehaviour其实也是继承自PlayableBehaviour,因此他和普通的PlayableBehaviour具有相同的接口。他的主要功能是用来混合不同的clip。其混合逻辑也是在ProcessFrame中实现。那么,怎么让他成为一个Mixer呢,这其实是需要自定义Track,并在自定义的Track中指定,下面会讲。我们先看一下Mixer的实现方式:

    public class CustomMixerBehaviour : PlayableBehaviour
       {
           private PathCreator pathCreator;
           private PathFollower pathFollower;
    
           private PlayableDirector director;
           private CustomPlayableBehaviour currentClip;
    
           public override void OnGraphStart(Playable playable)
           {
               director = playable.GetGraph().GetResolver() as PlayableDirector;
               pathCreator = director.gameObject.GetComponentInChildren<PathCreator>();                         
           }                  
    
           public override void ProcessFrame(Playable playable, FrameData info, object playerData)
           {                       
               if(pathFollower== null){
                   pathFollower= playerData as PathFollower; 
                   if(pathFollower != null){
                       pathFollower.PathCreator = pathCreator;                   
                   }                               
               }                                 
    
               double time = director.time;                
                                                         
               int inputCount = playable.GetInputCount();           
    
               for(int i=0; i < inputCount; i++){                
                   float inputWeight = playable.GetInputWeight(i);
                   var inputPlayable = (ScriptPlayable<CustomPlayableBehaviour >)playable.GetInput(i);
                   CustomPlayableBehaviour input = inputPlayable.GetBehaviour();            
                   
                   //找到当前在时间范围内的clip
                   if ((time >= input.OwningClip.start) && (time < input.OwningClip.end)){    
                       pathFollower.Move(input.progress);                      
                       break;
                   }                                                                                                                                                                                             
               }                                                    
           }
       }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    这是一个实际项目中简化出来的例子,因此其Mixer逻辑并没有执行任何混合操作,这儿只是演示了如何获取当前所有的playable节点,然后找到正在执行的节点,并执行他。而混合操作其实也是对于track上所有的Playable节点(即clip)分别进行计算,然后使用不同的权重将计算结果进行混合。

    自定义Track

    [TrackColor(0f, 1.0f, 0.5f)]
        [TrackClipType(typeof(CustomPlayableAsset))]
        [TrackBindingType(typeof(PathFollower))]    
        public class CustomTrack : TrackAsset
        {          
            public override Playable CreateTrackMixer(PlayableGraph graph, GameObject go, int inputCount)
            {
                foreach (var clip in GetClips())
                {
                    var playableAsset = clip.asset as CustomPlayableAsset;
    
                    if (playableAsset)
                    {                    
                        playableAsset.OwningClip = clip;
                    }
                }
    
                return ScriptPlayable<CustomMixerBehaviour>.Create(graph, inputCount);
            }
    
            public override void GatherProperties(PlayableDirector director, IPropertyCollector driver)
            {
    #if UNITY_EDITOR
                PathFollower trackBinding = director.GetGenericBinding(this) as PathFollower;
                if(trackBinding == null){
                    return;
                }                        
                
                var serializedObject = new UnityEditor.SerializedObject(trackBinding );
                var iterator = serializedObject.GetIterator();
                while(iterator.NextVisible(true)){
                    if(iterator.hasVisibleChildren)
                        continue;
    
                    driver.AddFromName<AlignPathMoveBehaviour>(trackBinding .gameObject, iterator.propertyPath);
                }
    #endif
                base.GatherProperties(director, driver);
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 自定义track通过TrackClipType指定了track使用的clip资源类型
    • 通过TrackBindingType指定了要绑定的对象或组件,这儿我们绑定的是一个MonoBehaviour组件,用于让GameObject沿着曲线移动。
    • CreateTrackMixer方法中,我们创建了Mixer。
    • GatherProperties的作用在于编辑器模式进行预览时,防止绑定对象的值被timeline预览修改,因此需要将受影响的对象或组件进行序列化,在退出预览时,这些值会被恢复。

    注意的问题

    • 实际项目中需要注意的问题还有很多,比如说由于Timeline需要在编辑器中预览,因此Timeline自定义的代码会在预览时被执行,所以要使用Application.isPlaying进行判断。
    • 关于Playable Graph的生命周期回调,比如OnGraphStartOnGraphStop,在进入和退出预览时也会被调用。
  • 相关阅读:
    linux——主从同步
    转换年金是什么意思呢?
    Go:如何在GoLand中引用github.com中的第三方包
    带你深入了解什么是 Java 线程池技术
    .NET App 与Windows系统媒体控制(SMTC)交互
    光模块字母含义及参数简称大全
    线上突然查询变慢怎么核查
    淘宝获取sku详情接口工具
    Text-to-Image最新论文、代码汇总
    【python练习】在棋盘上收集奖品,跟着书本理思路
  • 原文地址:https://blog.csdn.net/n5/article/details/127801925