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是否可以混合是由其实现的ITimelineClipAsset
的ClipCaps
决定的,对于动画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的生命周期回调,比如
OnGraphStart
和OnGraphStop
,在进入和退出预览时也会被调用。