赞
踩
Timeline最基本的作用是编辑过场动画。实际上任何预定义的线性流程都可以使用Timeline编辑,例如沿固定路线巡逻的敌人。由于Timeline可以同时编辑和播放多条不同类型的轨道,比如动画和声音,并且可以可视化的设置事件发送的点,因此编辑这种预定义流程非常方便。并且由于Timeline可扩展自定义,因此Timeline可用于制作任何配合线性流程的游戏Gameplay。本文正是基于之前的一个小游戏使用自定义Timeline功能的总结,自定义的部分包含PlayableAsset/PlayableBehaviour/TrackAsset/Signal。主要参考资料为Unity Blog:Extending timeline a practical guide
一个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是以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,即在Game Object上添加Playable Director组件,该组件指定一个Timeline Asset即.playable
文件。同时如果Timeline Asset中的各个Track需要绑定GameObject或Component作为目标,则还需要设定Playable Director组件中的Bindings。例如:
播放Timeline其实就是使用PlayableDirector组件的Play方法。
首先,自定义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
对象。
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中编辑。
自定义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
其实也是继承自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)分别进行计算,然后使用不同的权重将计算结果进行混合。
[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); } }
CreateTrackMixer
方法中,我们创建了Mixer。Application.isPlaying
进行判断。OnGraphStart
和OnGraphStop
,在进入和退出预览时也会被调用。Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。