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()    

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

简单理解一下,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;
    }
}

一个最简单的自定义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;            
    }

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

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

这相当于在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;
               }                                                                                                                                                                                             
           }                                                    
       }
   }

这是一个实际项目中简化出来的例子,因此其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);
        }
    }
  • 自定义track通过TrackClipType指定了track使用的clip资源类型
  • 通过TrackBindingType指定了要绑定的对象或组件,这儿我们绑定的是一个MonoBehaviour组件,用于让GameObject沿着曲线移动。
  • CreateTrackMixer方法中,我们创建了Mixer。
  • GatherProperties的作用在于编辑器模式进行预览时,防止绑定对象的值被timeline预览修改,因此需要将受影响的对象或组件进行序列化,在退出预览时,这些值会被恢复。

注意的问题

  • 实际项目中需要注意的问题还有很多,比如说由于Timeline需要在编辑器中预览,因此Timeline自定义的代码会在预览时被执行,所以要使用Application.isPlaying进行判断。
  • 关于Playable Graph的生命周期回调,比如OnGraphStartOnGraphStop,在进入和退出预览时也会被调用。

猜你喜欢

转载自blog.csdn.net/n5/article/details/127801925
今日推荐