Files
obsidian-notes/3Resources/游戏开发/Timeline结构及其源码浅析.md
2025-07-04 18:23:21 +08:00

60 KiB
Raw Blame History

原文发布链接, tags
原文发布链接 tags
https://github.com/RingleaderWang/UnityBlog/blob/main/Unity_025/Timeline%E7%BB%93%E6%9E%84%E5%8F%8A%E5%85%B6%E6%BA%90%E7%A0%81%E6%B5%85%E6%9E%90.md
Timeline

综述

Unity的Timeline本质是一个包含很多可播放片段Playable Clip的区间树IntervalTree这个区间树可以排序、搜索、以及控制所有 Clip的激活与停止最后利用底层的Playable系统运行所激活的Clips就是这么个东西。

当然这么定义不够准确IntervalTree不控制clip激活只是排序和搜索clip。IntervalTree本质是装clip的容器不考虑性能的话用数组也行。这么定义只是方便理解Timeline的底层结构罢了

Timeline的两个主要作用

  • 充当技能编辑器(策划程序向)
  • 做cg动画/过场动画/动画演出(导演动画向)

Timeline整体结构如下

包括底层的Playable系统其在c++端封闭黑盒暴露部分API给c#端使用Timeline就是利用这些API实现的功能。

详细来看,Timeline侧主要由PlayableAssetPlayableBindingTrackAssetPlayableDirector等组成;

Playable系统侧主要是PlayablePlayableOutputPlayableGraph三部分组成。

因为后面会频繁出现包含"Playable"的单词,所以需要辨析强调下

  • Playable系统指底层c++端关于Playable的所有功能包含playable/playableout节点创建与连接、playableGraph的构建运行销毁等。

  • PlayableAPI指底层Playable系统暴露给c#端的接口方法。

  • PlayableGraph控制Playable/PlayableOutput的创建、连接与运行

  • Playable节点playableGraph中的数据生产与传递节点包含叶子节点、mixer节点、layer节点等可以进行数据处理与传递、权重混合等操作

  • PlayableOutputplayableGraph中的数据输出节点将Playable节点传递的数据交由对应的target运行

  • Playable不同语境含义不同在PlayableGraph中指playable节点讲源码时指playable节点对应的Playable这个类/结构体timeline语境中一般指asset创建的对应playable类/结构体放到整个unity一般指Playable API或Playable系统

  • Timeline

    • Timeline窗口:编排可播放片段的窗口。
    • Timeline文件Timeline窗口编辑后生成的文件扩展名为”.playable“。
    • TimelineAsset指Timeline这个文件资产或者实例化后对应的类。
    • TimelinePlayable由TimelineAsset类在运行时调用create方法生成对应的TimelinePlayable构建playable节点树并管理所有轨道中的clip。
  • Playable Clip可播放片段比如AnimationClip、AudioClip、Prefab等

  • PlayableAsset跟Playable有关的静态资产文件或其实例化的类运行时可以创建对应的Playable。包含TimelineAsset、TrackAsset、PlayableClipAsset等。

  • TrackAsset特殊的PlayableAssetTimeline窗口中一般直接称作track轨道每个轨道可以放很多clip这些clip就是PlayableClipAsset运行时TrackAsset一般会创建MixerPlayable来混合PlayableClipAsset创建的Playable。

  • PlayableBinding绑定track和最终运行数据的target用于创建PlayableOutput

  • PlayableDirector控制PlayableAsset用于创建PlayableGraph来运行构建的timeline

Timeline主要有三块要点:

  • Timeline静态与运行态,涉及:
    • TimelineAsset CreatePlayable 方法
    • TimelinePlayable Create方法、Compile方法、PrepareFrame方法
    • TrackAsset CreatePlayableGraph方法、CompileClips方法
  • IntervalTree与RuntimeClip内部结构,涉及:
    • TrackAsset CompileClips方法
    • IntervalTree Rebuild方法
  • ScriptPlayable生命周期涉及IPlayableBehaviour接口

重点结构如下图所示:

Timeline的完整流程

接下来会详细分析整个系统。

Timeline 操作简介

  1. 打开timeline窗口Window>Sequencing>Timeline

  2. 创建Timeline直接选择gameobject 点击Timeline窗口的Create创建或者Project窗口右键Create>Timeline进行创建。timeline文件扩展名为 playable是静态资产

  3. Timeline窗口create timeline对象会自动创建PlayableDirector组件并绑定timeline。手动新建的话可以直接拖动timeline资产到对象也能自动生成PlayableDirector组件。

  4. PlayableDirector用于为timeline创建对应的playable graph可以激活play On Awake那样组件awake后就能play注意PlayableDirector组件awake远早于用户脚本awake无法在用户脚本awake方法中控制PlayableDirector的play on awake参数)

    • Bindings会显示特定轨道所绑定的对象用于指明当前Track控制的对象。
    • Wrap Mode指明Timeline播放完毕后的行为none会直接stop timeline所在的graph就是直接停止timelinehold的话就是持续播放timeline最后一帧Loop的话会从头循环播放。
    • Update Method 指明Timeline play如何控制GameTime是跟随系统自动playUnscaled Game Time就是无视系统时间倍速的自动play游戏pause也会播放timelineManual就是由用户手动evaluate进行timeline play。DSP似乎跟音频有关但官方说跟Unscaled Game Time差不多暂不研究。
  5. Timeline窗口右键添加轨道。 主要有如下几个轨道:

    • Activation Track:控制激活的轨道
    • Animation Track:控制动画播放的轨道
    • Audio Track:控制音频播放的轨道
    • Control Track控制prefab、粒子系统或者子timeline显示或播放的轨道
    • Signal Track:控制帧事件的轨道
    • Custom Track,就是用户自己继承TrackAssetPlayableAssetPlayableBehaviour等来实现自定义playable。
  6. 创建好track和playable后就可以用PlayableDirector自动播放或手动Play()播放。

这里只是简单介绍下Timeline的用法还有很多细节可以自己摸索或者去官方文档查看。

下面将进入本文的主题深入讲解Timeline的结构同时会浅析所涉及的Timeline和Playable源码。

Timeline 静态结构分析

后面我们会了解到,在运行时,timeline会生成树形结构的PlayableGraph,那谁指导它生成呢?就是PlayableAsset这即是Timeline的静态结构

在Timeline窗口编辑资产后形成资产树(也是树形结构的,根节点TimelinePlayable,下一层是root track/output track,再下一层是 PlayableAsset)。

PlayableDirector添加timeline资产然后设置bindingdirector就能管理整个timeline资产了。

等后面director组件初始化 并根据timeline资产树实例化为对应的TimelineAsset后,便能以有序生成各个Playable

所以想理解运行时Timeline就必须先搞明白 静态Timeline的结构并理清各PlayableAsset类的关系以及clip和track、playTarget与track的关系即Binding

PlayableAsset 静态资产

在Timeline窗口创建两个TrackActivation Track、Audio Track并分别添加对应的clip。如下

这个扩展名为 ".playable"的 timeline 文件,就是所谓的静态资产。

timeline资产文件实例化后就是TimelineAsset类TimelineAsset下一层是TrackAssetTrackAsset包含数个PlayableAsset比如AnimationPlayableAsset、AudioPlayableAsset等

结构图如下:

对应的Timeline.playable yaml文件如下图所示

实例化时,会根据这个yaml文件解析成 PlayableAsset节点链利用PlayableAsset的parentchildren参数),根节点Root PlayableAsset是TimelineAsset,下一层是Root Track,对于Group track或者AnimationTrack允许其下还有SubTrack每个Track都可以包含数个PlayableAsset

  • root PlayableAsset就是TimelineAsset是所有root track的父级节点。
  • root track就是TimelineAsset的直接子track从timeline窗口看就是最外层track。
  • output track 会生成PlayableOutput的track。除 GroupTrack、subGroupTrack、override track外的所有track 都是 output track。

红框为root track黄框为output track

PlayableDirector

用Timeline窗口查看timeline资产有两种方式产生的效果会不同

  • 第一种是从Project窗口打开的独立timeline资产它无法预览播放track左侧也没有绑定对象
  • 另一种就是从挂载PlayableDirector组件对象点开那么它的timeline就是可预览播放的track左侧也会显示Bindings

第二种其实就是PlayableDirector组件做的绑定。

  • PlayableDirector利用key-value 来实现绑定。
  • key就是Track对象(在PlayableBinding中被称作sourceObject.
  • value 就是目标对象(在PlayableOutput中被称作target如ActivationTrack对应的就是待控制的预制体AudioSourceTrack对应的就是Audio Source组件 AnimationTrack 对应的就是Animator。

PlayableDirector组件中SceneBindings值

这样包含PlayableDirector的结构图就变成下图所示

包含PlayableDirector的TimelineAsset结构图

PlayableAsset UML类图

TrackAssetTimelineAsset 其实都是继承自PlayableAsset,是一种特殊的PlayableAsset

PlayableAsset UML类图如下图所示:

  • TimelineAsset:最特别的PlayableAsset存储了所有trackAsset包含一个CreatePlayable方法,指导运行时创建TimelinePlayable
  • ClipPlayableAsset(蓝色) :表示实现ITimelineClipAsset接口的特殊PlayableAsset,具体类有ActivationPlayableAssetAudioPlayableAssetAnimationPlayableAssetControlPlayableAsset。这些PlayableAsset拥有一个clipCaps属性表明这个片段在Timeline窗口拥有的能力这些能力包含
    • Looping 循环
    • Extrapolation 外推断分Pre和post表示两个clip中间空白区域的播放逻辑
      • none 保持为开始帧
      • hold 保持为最后一帧
      • loop 循环(跳帧循环)
      • pingpong 镜像循环
      • continue 根据动画源文件的loop决定是连续循环还是保持为最后一帧
    • ClipIn 起始裁切
    • SpeedMultiplier 倍速
    • blend 混合
  • PlayableAsset都包含一个CreatePlayable方法用于运行时创建对应的Playable。
  • TrackAsset(左下黄色) 拥有CreateTrackMixer方法运行时也是创建Playable重写CreateTrackMixer方法的话可以创建自定义MixerPlayable可以接收多个input输入。
  • TrackAssetCompileClips方法在运行时编译其所包含的所有clip生成RuntimeClip并加入到TimelinePlayableIntervalTree用于Timeline运行时标识哪个clip被激活。
  • 继承TrackAsset的类有GroupTrack、ActivationTrack、AudioTrack、ControlTrack、SignalTrack、AnimationTrack等这与timeline窗口能添加的track一致。
  • TrackAsset 有两个特性(Attribute
    • TrackClipType表明Track能添加的clip类型也就是上面继承playable且实现ITimelineClipAsset接口的类。
    • TrackBindingType :表明 output Track对应的PlayableOutputtarget参数类型,也即PlayableBindingoutputTargetType参数。
[TrackClipType(typeof(AnimationPlayableAsset)]
[TrackBindingType(typeof(Animator))]

Playable系统

上文介绍了Timeline的静态结构为了更好地理解运行时Timeline需要先了解底层的Playable系统。Playable 系统包括PlayableGraph、Playable和PlayableOutput。

下面依次介绍。

PlayableGraph

PlayableGraph管理Playable和PlayableOutput节点控制整个节点树的运行。

PlayableGraph 与 PlayableDirector 类

Timeline 运行时会根据PlayableDirector的Playable参数创建PlayableGraph。

PlayableDirector可以用PlayableDirector.playableGraph属性获得创建的graph PlayableGraph也可以用PlayableGraph.GetResolver() as PlayableDirector 解析出PlayableDirector。PlayableGraph的很多方法都可以直接在PlayableDirector中调用。

PlayableGraph 和 PlayableDirector UML类图如下

PlayableGraph的结构

对于前面的 timeline 资产:

在运行时形成的Graph结构如下

PlayableGraph的构建过程:

  • PlayableDirector RebuildGraph触发构建graph动作
  • 根据director的TimelineAsset 创建 TimelinePlayable
  • 然后依次 编译所有track和clip创建对应的MixerPlayable、PlayableOutput和Playable。
    • TimelinePlayable作为根节点下一层是MixerPlayable或LayerMixerPlayable叶子节点是包含clip的Playable。
    • Playable之间是通过Connect连接PlayableOutput通过setSourcePlayable与TimelinePlayable连接。
  • 编译clips期间会build IntervalTree为后续graph play作准备。

大体过程图如下(下一章还会有更详细的步骤分析):

Timeline中关于graph的构建步骤很复杂我们可以用如下案例了解其核心逻辑注意一定要及时销毁graph

public AnimationClip clip;  
public AnimationClip clip2;  
private PlayableGraph graph;

void DestoryGraph(PlayableGraph graph)  
{  
    if (graph.IsValid())  
    {        
	    graph.Destroy();  
    }
}
  private void CreateMixerGraph()
    {
        DestoryGraph(graph);
        // 1. 创建 PlayableGraph
        graph = PlayableGraph.Create("MixerPlayableGraph");
        // 2. 获取 Animator 并创建 AnimationPlayableOutput
        var animator = GetComponent<Animator>();
        var animationOutput = AnimationPlayableOutput.Create(graph, "Animation", animator);
        // 3. 创建 AnimationClipPlayable
        var clipPlayable = AnimationClipPlayable.Create(graph, clip);
        var clipPlayable2 = AnimationClipPlayable.Create(graph, clip2);
        // 4. 创建一个 Mixer管理多个动画输入
        AnimationMixerPlayable mixer = AnimationMixerPlayable.Create(graph, 2);
        mixer.ConnectInput(0, clipPlayable, 0,1);
        mixer.ConnectInput(1, clipPlayable2, 0,1);
        // 5. 设置 mixer 作为输出
        animationOutput.SetSourcePlayable(mixer);
        // 6. 播放图谱
        graph.Play();
    }
public void OnDestroy()  
{  
    DestoryGraph(graph); 
}

逻辑很简单就4步

  • 创建graph
  • 创建playable、mixerPlayable 并 connect
  • 创建playableOutput 并 setSourceOutput
  • 运行graph

使用Playable Graph Monitor 查看 整个graph如下。

如果你使用官方的PlayableGraph Visualizer 的话,你会发现和 Playable Graph Monitor所展示的不一样。

比如还是这个例子:

PlayableGraph Visualizer展示

Playable Graph Monitor展示

你可以发现区别很大Visualizer从PlayableOutput往Playable构图且两个PlayableOutput独立构图而Monitor是从叶子Playable节点往PlayableOutput构图两个PlayableOutput连在一个output端口上。

到底哪个对呢?

其实都对。Visualizer是从graph play角度构图的Monitor是从graph create角度构图的。

执行遍历节点获取数据时其发起端其实是PlayableOutput所以Visualizer把各个PlayableOutput独立开来构图但这种展示方式对认识graph的结构不如Monitor清晰而且官方Visualizer不能缩放插件还有些bug会导致系统闪退所以直接用Monitor就行。

不过Monitor的展示还是有些问题两个PlayableOutput连在同一个output port了这应该是Unity Timeline自身的问题你理解成第二个PlayableOutput连在第二个port就行以此类推。不影响运行

下面将继续介绍涉及的Playable与PlayableOutput。

Playable 的结构与类

PlayableGraph 中的 Playable 节点负责 数据的生产、传递与处理包含叶子节点、mixer节点和layer节点等。

Playable结构

Playable是一个多输入多输出的有向节点拥有speed、time、duration、isDone、PlayState等参数标识自身的运行状态并对外提供控制自身运行、暂停与销毁的接口。

节点属性/方法

  • Playable节点拥有多个input port、多个output port用于连接其他Playable节点。
  • 节点连接前需保证port存在否则报错Connecting invalid input/output,需要用SetInputCount()/SetOutputCount()申请端口或者create时指明inputCount参数。
  • 同一port不能连接多个节点否则报错Cannot connect input/output port, it is already connected, the tree topology will be invalid. Disconnect it first
  • Input有权重weight的概念默认是0可以在connect时指定weight或者用Playable.SetInputWeight(inputIndex, weight)设定输入权重
  • Playable节点间连接可以使用PlayableGraph.Connect(...)或者扩展方法的Playable.ConnectInput(...)默认权重为0
PlayableGraph.Connect(...)

bool Connect<U, V>(  
  U source,  
  int sourceOutputPort,  
  V destination,  
  int destinationInputPort)  
  where U : struct, IPlayable  
  where V : struct, IPlayable

允许的结构:

不允许的结构三个节点首尾相接出现死循环如果使用graph visulizerunity会奔溃

一些特殊的属性/方法

  • GetScriptInstance()指明playable生命周期的回调方法
  • GetClip()/SetClip一些包含Clip内容的Playable如 AnimationClipPlayable、AudioClipPlayable、AnimatorControllerPlayable、MaterialEffectPlayable、CameraPlayable、VideoPlayable 拥有存取此Playable绑定的可播放片段的方法。这些方法名不固定根据Playable类型也可以叫 Get/SetMaterial()、GetAnimatorController、Get/SetCamera()、GetAnimationClip()等。

运行属性/方法

  • Play statePuase/Playing,标识此节点运行状态
  • Speedplayable的播放速度
  • Play()运行playable
  • Pause()停止自身playable运行
  • IsDone:标识此节点运行完毕
  • Destroy()利用所属graph销毁自身Playable
  • TraversalModeMix/Passthrough表示遍历节点input的方式Mix就是将各输入进行加权;passthrough就是匹配input与output第几个output来请求数据就从第几个input获取数据。Passthrough目前只用于TimelinePlayable使之成为graph的统一入口。

Traversal mode 验证

对Traversal mode这个参数的理解非常重要不明白它的话你会误以为每个PlayableOutput都会接收所有叶子节点的数据。

我们来创建个graph案例

[Tooltip("false则mode是passthroughtrue则mode是mix")]  
public bool changeTraversalMode = false;  
[Tooltip("false则PlayableOutput的source为playable1true则为playable2")]  
public bool changeSourcePlayable = false;

//newInput system绑定空格键触发
public void BackspaceDown(InputAction.CallbackContext ctx)  
{  
    if (ctx.performed)  
    {   
        ValidateTraversalMode2(changeTraversalMode,changeSourcePlayable);  
    }
}

private void ValidateTraversalMode2(bool _changeTraversalMode,bool _changeSourcePlayable)  
{  
    DestoryGraph(graph);  
    graph = PlayableGraph.Create();  
    var playable1 = Playable.Create(graph,1);  
    var playable2 = Playable.Create(graph,2);  
    var playable3 = Playable.Create(graph);  
    playable3.SetTraversalMode(!_changeTraversalMode?PlayableTraversalMode.Passthrough:PlayableTraversalMode.Mix);  
    var playable4 = Playable.Create(graph,4);  
    var playable5 = Playable.Create(graph,5);  
	playable3.SetInputCount(10);  
    playable3.SetOutputCount(10);  
    graph.Connect(playable3, 0, playable1, 0);  
    graph.Connect(playable3, 1, playable2, 0);  
    graph.Connect(playable4, 0, playable3, 0);  
    graph.Connect(playable5, 0, playable3, 1);  
    ScriptPlayableOutput output = ScriptPlayableOutput.Create(graph, "customOutput");  
    output.SetSourcePlayable(!_changeSourcePlayable?playable1:playable2,100);
    graph.Play();  
}

当前中间Playable的 traversalMode是passthrough可以看到只有port 0 这条线有数据传输。

mode为passthrough数据流向

改变PlayableOutput连在port 1 这端数据又只接受input 1 这个port输入了。

mode为passthrough的数据流向改变sourcePlayable

而当 MODE 设为 MIX input 0 和 input 1 都接收数据。

MODE=MIX的数据流向

通过这个例子,能大概理解 TraversalMode 遍历模式到底是什么意思。

不过要注意按照Passthrough源码注释的本意

应该是sourceOutputPortPlayableOutput跟对应input位置的节点进行直通但实测sourceOutputPort没起作用实际上是由PlayableOutput在graph中的顺序决定也就是下图中PlayableOutput前面的#0 #1 #2 标号其实就是PlayableOutput数组的index

passthrough mode下input port匹配的其实是PlayableOutput数组标号

测试代码你可以发现SetSourcePlayable的port你设50设100都不影响。

void ValidateTraversalMode()  
{  
    DestoryGraph(graph);  
    graph = PlayableGraph.Create("TraversalModeGraph");  
    ScriptPlayableOutput output = ScriptPlayableOutput.Create(graph, "customOutput");  
    ScriptPlayableOutput output2 = ScriptPlayableOutput.Create(graph, "customOutput2");  
    ScriptPlayableOutput output3 = ScriptPlayableOutput.Create(graph, "customOutput3");  
    var playable = ScriptPlayable<CustomClipBehaviour>.Create(graph);  
    var playable2 = ScriptPlayable<CustomClipBehaviour>.Create(graph);  
    var playable3 = ScriptPlayable<CustomClipBehaviour>.Create(graph);  
    var playable4 = ScriptPlayable<CustomClipBehaviour>.Create(graph);  
    var playable5 = ScriptPlayable<CustomClipBehaviour>.Create(graph);  
    var mixer = ScriptPlayable<CustomMixerBehaviour>.Create(graph);  
    mixer.SetTraversalMode(PlayableTraversalMode.Passthrough);//默认mode为mix这里改为passthrough  
    mixer.SetInputCount(10);// 很重要否则连接报错Connecting invalid input  
    mixer.ConnectInput(4,playable,0,1);  
    mixer.ConnectInput(6,playable2,0,1);  
    mixer.ConnectInput(2,playable3,0,1);  
    mixer.ConnectInput(8,playable4,0,1);  
    mixer.ConnectInput(1,playable5,0,1);  
    output.SetSourcePlayable(mixer,30);// 这个SetSourcePlayable的port根本没用完全根据PlayableOutput顺序获取的  
    output2.SetSourcePlayable(mixer,50);  
    output3.SetSourcePlayable(mixer,100);  
    graph.Play();  
}

但从Animation计算权重的过程也能看出SourceOutputPort本意就是playable的output port。但实测中Playableout获取数据时并没有使用这个参数更没有校验你设多少都可以只校验了不能重复绑定但没校验port是否存在 如果是bug的话官方可能需要做下面两个步骤进行修复

  • 每connect一个节点到TimelinePlayableTimelinePlayable的outputCount加一这样直通才有意义
  • 对每个Playableout从根节点TimelinePlayable开始遍历时严格按照TimelinePlayable.getInput(PlayableOutput.GetSourceOutputport())获取绑定的Playable而不是用PlaybleGraph中类似PlayableOytput[]数组的index来get input playable。Timeline生成的graph中PlayableOutput.GetSourcePlayable()得到的就是TimelinePlayable

Playble UML类图

PlayableAsset创建的playable有两种一种是不带生命周期管理的Playable另一种是带生命周期管理的实现IPlayableBehaviour接口。第二种其实就是Timeline模块中基于ScriptPlayable实现的一种官方自定义playable。

基础Playable 的 UML 类图

注意这些Playable都是Struct没有像class一样的继承能力但这些子Playable都重写了隐/显式操作符所以功能上等价于存在继承关系。比如下面AudioClipPlayable例子

public static implicit operator Playable(AudioClipPlayable playable)  
{  
  return new Playable(playable.GetHandle());  
}  
  
public static explicit operator AudioClipPlayable(Playable playable)  
{  
  return new AudioClipPlayable(playable.GetHandle());  
}

// 自动转换
AudioClipPlayable audioClipPlayable = ...;
Playable playable = audioClipPlayable;

// 需要显式转换
Playable playable = ...;
AudioClipPlayable audioClipPlayable = (AudioClipPlayable)playable;
  • 这些Playable都拥有Create方法供TrackAsset执行CompileClips方法或CompileTrackPlayable方法期间调用。
  • Playable按功能大概分成两类
    • 一类如上面蓝色AnimationClipPlayable等和绿色ScriptPlayable的Playable在graph中常作为叶子节点的其内部是含有类似clip的属性能getClip、setClip有的没暴露set只能在create时传入
    • 一类是上面黄底色的Playable在graph中作为中间节点可能包含多个输入所以命名一般叫MixerPlayable、LayerPlayable对多个输入的权重或其他数据进行处理。
  • 上面结构图并没有列出所有Playable详细的可以查看IPlayable的所有实现。
  • ScriptPlayable比较特殊,能够接收PlayableBehaviour控制Playable的生命周期,这样形成的就是自定义PlayableTimeline里的很多Playable本质上就是这种官方自定义Playable

下面就详细介绍这些所谓的“官方自定义Playable”。

PlayableBehaviour 的 UML 类图

继承PlayableBehaviour的类分为几种:

  • TimelinePlayable作为graph中所有其他playable的根父节点控制所有clip的激活与否。
  • Audio的两个名字比较特别AudioMixerPropertiesAudioClipPropertiesAudioClipProperties甚至连一个回调都没有这就涉及到PlayableBehaviour的第二种作用传递PlayableAsset/TrackAsset参数给运行时的各生命周期。这种参数传递有两种用法,后面会介绍。
  • Activation有关的 ActivationControlPlayable 和 ActivationMixerPlayable 。 比较好玩的是 ActivationControlPlayable 只用在 ControlTrack ,而 ActivationMixerPlayable 只用在 ActivationTrack 。
  • ControlTrack 的三个playable
    • 控制粒子的ParticleControlPlayable
    • 控制prefab的PrefabControlPlayable
    • 控制PlayableDirector也就是控制子TimelineDirectorControlPlayable

Behaviour 与 PlayableAsset的数据交互

我们在自定义Track 自定义PlayableAsset时可能有这么个需求Track或PlayableAsset自身需要拥有特殊属性,而且能在后面的生命周期中获取这个特殊属性。

比如我的技能系统用的SkillPlayableAsset有个参数标注这个clip用于技能的哪个阶段就需要SkiIlPhase这么一个参数后面behaviour回调触发时就能根据这个参数针对性做处理。

Track或PlayableAsset自身需要拥有特殊属性的场景

这就是behaviour 与 playableAsset的数据交互问题。

如果你是官方设定的基础Playable你可以在Playable上设置这么个特殊属性create的时候都塞进去生命周期回调时也能正常获取。但你自定义track用的都是ScriptPlayable没有额外参数怎么办那就是用PlayableBehaviour传参

传参有两种方式这同时也涉及到ScriptPlayable createPlayable的两种方式。

// T : class, IPlayableBehaviour, new()
public static ScriptPlayable<T> Create(PlayableGraph graph, int inputCount = 0)  
{  
  return new ScriptPlayable<T>(ScriptPlayable<T>.CreateHandle(graph, default (T), inputCount));  
}  
  
public static ScriptPlayable<T> Create(PlayableGraph graph, T template, int inputCount = 0)  
{  
  return new ScriptPlayable<T>(ScriptPlayable<T>.CreateHandle(graph, template, inputCount));  
}

带 new T() 的 ScriptPlayable 创建

如上ScriptPlayable 创建Playable时可以带T template 参数T就是实现了IPlayableBehaviour且有无参构造函数的类。

这样自定track或playableAsset在创建playable时就可以将含有特殊属性的T传进来behaviour回调时就能获取到你设的特殊属性。

// 存在 XXPlayableBehaviour内含param1、param2两个参数

public class XXPlayableAsset : PlayableAsset
{
	public XXPlayableBehaviour template = new XXPlayableBehaviour();
	public override Playable CreatePlayable (PlayableGraph graph, GameObject owner){
	   var playable = ScriptPlayable<XXPlayableBehaviour>.Create(graph,template);
	   return playable;
	}
}
XXPlayableAsset Inspector面板展示
template
- param1
- param2

这样你在XXPlayableAsset Inspector就能展示XXPlayableBehaviour的两个参数param1和param2但不够好看外面会套一层参数名像下面这样

解决方法有两个一种是类似AudioTrack的处理方法写个AudioTrackInspector手动提取出各个参数。另一个参考官方案例的自定义AttributeNoFoldOut。

// Custom property drawer that draws all child properties inline
[CustomPropertyDrawer(typeof(NoFoldOutAttribute))]
public class NoFoldOutPropertyDrawer : PropertyDrawer
{
    public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
    {
        if (!property.hasChildren)
            return base.GetPropertyHeight(property, label);
        property.isExpanded = true;
        return EditorGUI.GetPropertyHeight(property, label, true) -
               EditorGUI.GetPropertyHeight(property, label, false);
    }

    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        if (!property.hasChildren)
            EditorGUI.PropertyField(position, property, label);
        else
        {
            SerializedProperty iter = property.Copy();
            var nextSibling = property.Copy();
            nextSibling.Next(false);
            property.Next(true);
            do
            {
                // We need to check against nextSibling to properly stop
                // otherwise we will draw properties that are not child of this
                // foldout.
                if (SerializedProperty.EqualContents(property, nextSibling))
                    break;
                float height = EditorGUI.GetPropertyHeight(property, property.hasVisibleChildren);
                position.height = height;
                EditorGUI.PropertyField(position, property, property.hasVisibleChildren);
                position.y = position.y + height;
            }
            while (property.NextVisible(false));
        }
    }
}

这样使用时给参数加[NoFoldOut]特性就能直接展示子属性了。

// 存在 XXPlayableBehaviour内含param1、param2两个参数
public class XXPlayableAsset : PlayableAsset
{
	[NoFoldOut]
	public XXPlayableBehaviour template = new XXPlayableBehaviour();
	public override Playable CreatePlayable (PlayableGraph graph, GameObject owner){
	   var playable = ScriptPlayable<XXPlayableBehaviour>.Create(graph,template);
	   return playable;
	}
}

使用 default (T) 的 ScriptPlayable 创建

如果你使用 default (T) 创建,意味着你的这些特殊属性都变成默认值。

不过你也可以手动操作改变如下代码所示create完ScriptPlayable后再手动赋值。

// 存在 XXPlayableBehaviour内含param1、param2两个参数

public class XXPlayableAsset : PlayableAsset
{
	public override Playable CreatePlayable (PlayableGraph graph, GameObject owner)
	{
	   var playable = ScriptPlayable<XXPlayableBehaviour>.Create(graph);
	   var xxPlayableBehaviour = playable.GetBehaviour();
	   xxPlayableBehaviour.param1 = value1;
	   xxPlayableBehaviour.param2 = value2;
	   return playable;
	}
}

不过这种在Asset还要再申明一遍param冗杂所以推荐用第一种。

ExposedReference用法

因为Asset不能引用scene场景中的对象但如果你非要引用也不是不可以Timeline提供了ExposedReference方法让你能在Behaviour回调中获取场景中的对象。

public class XXPlayableBehaviour : PlayableBehaviour
{
	public ExposedReference<Transform> exposedTransform;
	public override void OnGraphStart(Playable playable){
		Transform transform = exposedTransform.Resolve(playable.GetGraph().GetResolver());  
		Debug.Log("exposedTransform.Resolve(playable.GetGraph().GetResolver()):"+transform.position);
		Debug.Log("gameobject name:" + transform.gameObject.name);
	
	}
}

public class XXPlayableAsset : PlayableAsset
{
	public XXPlayableBehaviour template = new XXPlayableBehaviour();
	public override Playable CreatePlayable (PlayableGraph graph, GameObject owner){
	   var playable = ScriptPlayable<XXPlayableBehaviour>.Create(graph,template);
	   return playable;
	}
}

注意使用时一定要从点击挂载PlayableDirector的对象对应的timeline窗口操作。

ExposedReference用法

运行时可以正常打印但发现打印的对象position数据有问题似乎有(0.22, 1.28, -1.66)的偏差,不清楚原因,暂时还是不要用ExposedReference

ExposedReference用法有待进一步验证

PlayableOutput 结构与类

PlayableOutput 作为 PlayableGraph 中的数据输出节点在graph Play 时负责发起 Playable节点的遍历没连PlayableOutput 对应的Playable不运行并将Playable节点传递的数据交由对应的target运行。

PlayableOutput 结构

PlayableOutput 的结构相当简单。

  • ReferenceObject对应的TrackAsset
  • SourcePlayablePlayableOutput连接的Playable使用扩展方法PlayableOutput.SetSourcePlayable(Playable value, int port)连接
  • 注意连接的SourcePlayable和port不能都一样否则报错Cannot set multiple PlayableOutputs to the same source playable and output port
  • weightPlayableOutput也拥有权重默认为1可以通过PlayableOutput.SetWeight(float value) 设置。
  • targetAnimationPlayableOutput、AudioPlayableOutput、TexturePlayableOutput拥有这个属性表示Track所绑定的对象用于后续处理Playable传过来的数据。

UML 类图

相比Playable的类图PlayableOutput就显得眉清目秀了。

  • 跟Playable一样PlayableOutput也是struct结构体它的继承也是重载显隐操作符。
  • 每种PlayableOutput都有个create方法来创建一个具体的PlayableOutput。
  • 除了ScriptPlayableOutput外都有个target参数用于指明track binding的target对象。

运行时Timeline

前文介绍了Timeline编辑态流程至于运行时则主要包含三个步骤

  • director组件初始化
  • graph Build
  • graph Play

初始化 PlayableDirector

看Timeline源码最难受的就是这看不到尾就算了还看不到头过程全藏在Unity c++端。

不过好在静态文件已知create Playable步骤已知中间无非就是graph的创建那由谁触发呢只有PlayableDirector了。

所以隐藏的过程就是 PlayableDirector 组件的初始化和 PlaybleAsset 实例化,如果 play on awake 打开,则开始构建 graph否则等待 director 执行 Play() 或 RebuildGraph() 才开始构建 graph。

再次强调下PlayableDirector组件awake远早于用户脚本awake无法在用户脚本awake方法中控制PlayableDirector的play on awake参数。

就是说,如果play on awake 是 false你在自己脚本改为truedirector也不会自动运行。不过可以先让director disable在用户脚本awake 时 enable这个director间接实现那还不如手动Play() )。

构建 Graph

构建 graph简易过程如下图

详细过程如下:

  1. 根据PlayableDirector 的 TimelineAsset 执行创建Playable动作c++端触发)
  2. 创建TimelinePlayable节点,设置traversalMode为Passthrough
  3. 遍历Timeline中的每个OutputTrack,为其创建MixerPlayable节点和Playable节点
    1. 创建MixerPlayable父节点
    2. 遍历TrackAsset的每个TimelineClip创建对应的ClipPlayable子节点
    3. 利用MixerPlayable、刚创建的ClipPlayable和对应的TimelineClip创建RuntimeClip然后将RuntimeClip存储到TimelinePlayable的区间树IntervalTree同时执行ClipPlayable的Pause()方法其实就是设置playstate为paused
    4. 连接ClipPlayable子节点和MixerPlayable父节点权重设置为0
  4. 将上面创建的每个MixerPlayable连接TimelinePlayable权重为1
  5. 紧接着对每个OutputTrack创建对应的playableOutput节点SetSourcePlayableTimelinePlayable权重设为1ReferenceObject为这个TrackAsset
    • 如果是动画track的话最后会为PlayableOutput注册一个权重处理回调m_EvaluateCallbacks.Add(new AnimationOutputWeightProcessor(animOutput))构造AnimationOutputWeightProcessor时将AnimationPlayaleOutput的权重设为0默认是1。这个Processor会在graph后续运行时触发WeightEvaluate方法处理Animation相关的权重会重新调整权重
    • 注意playableOutput的target不是在此时添加的这时传入的target都是null可以通过((AnimationPlayableOutput)graph.GetOutput(0)).GetTarget()验证值一定是null假设第一个是动画的output情况下
      • 真正设置target的动作可能在构建gragh动作之后在c++端手动根据binding信息设置可能是通过共有的ReferenceObject 这个key来确定target的
      • 所以play阶段target是有值的如果编辑时你确实binding了
      • 那为什么不在创建时顺便设置target呢我猜还是静态资产没办法赋scene中对象的原因。这些TrackAsset实际是没法存target对象的。

build graph的方法调用链如下也可以看到一个明显的c++端调用):

AnimationPlayable的权重处理

Timeline 会利用 AnimationOutputWeightProcessor Evaluate方法处理AnimationMixer 、AnimationLayer和 AnimationPlayableOutput的权重。

大概效果如下设四个clip初始权重分别为0.260.140.190.13连到各自Mixer和Layer后如果输入权重和小于1按比例放大为:

  • mixer input0 weight 0.65= 0.26/(0.26+0.14)*1
  • mixer input1 weight 0.35= 0.14/(0.26+0.14)*1
  • layer input0 weight 0.56=(0.26+0.14)/(0.26+0.14+0.19+0.13)*1
  • AnimationOutput weight 0.72 = 0.26+0.14+0.19+0.13

如果输入权重和≥1则只对≥1的输入钳值到1不等比例缩小且AnimationOutput weight 钳值为1.

比如四个clip初始权重分别为0.60.90.20.3AnimationOutputWeightProcessor.Evaluate() 执行完后:

  • mixer input0 weight 0.6 0.6+0.9>1不缩放且0.6<1 不钳值)
  • mixer input1 weight 0.90.6+0.9>1不缩放且0.9<1 不钳值)
  • layer input0 weight 1 0.6+0.9+0.2+0.3>1不缩放且0.6+0.9>1 则钳值到1
  • layer input1 weight 0.5 0.6+0.9+0.2+0.3>1不缩放且0.2+0.3<1 不钳值)
  • AnimationOutput weight10.6+0.9+0.2+0.3>1 钳值到1

这么处理的目的按源码注释的说法”对动画轨道上的权重进行后处理以正确归一化混合器权重从而避免混合时出现默认姿势并确保子轨道、图层以及图层图正确混合。Does a post processing of the weights on an animation track to properly normalize the mixer weights so that blending does not bring default poses and subtracks, layers and layer graphs blend correctly ”

测试用例过长,参见:🔗TimelineTestForAnimationWeightInfo.

运行 Graph

graph play或evaluate后主要两个步骤

  1. TimelinePlayable PrepareFrame() 处理clip的激活 与权重
    1. 搜索区间树处理clip enable与disable改变对应Playable的playstate
    2. 根据mixin/mixout curve设置激活的clip所在的mixer input weight权重
    3. 执行AnimationOutputWeightProcessor.Evaluate()处理mixer layer和PlayaleOutput 的权重
  2. 后序遍历Playable执行playing态的Playable的ProcessFrame方法。

graph play或evaluate后两个步骤

当然这不是终点数据经过一个个playable处理后传递到playableOutput最终交由target处理。

Playable数据传递与output处理数据

ScriptPlayable数据处理在ProcessFrame中可以实现Prefab的enable disablevideo的播放暂停等功能。

而基础Playable如AnimationClipPlayable其Animation数据处理是隐藏起来的实际是通过AnimationStream结构体进行数据交互,你可以 使用 AnimationScriptPlayable 并实现IAnimationJobProcessAnimation 方法来自定义处理动画数据。

Playable使用细则

至于数据传入AnimatorPlayableOutput后怎么处理更是黑盒了可以看一下下面这位博主的分析

死板地介绍Unity动画系统设计

Animator的高级使用估计得等到有IK或者更精细动画需求后才可能再继续研究了。 目前在SMB+Timeline加持下已能满足大部分需求。

IntervalTree 结构

文章一开时给 Timeline 下的定义就是 “包含很多可播放片段Playable Clip的区间树IntervalTree”,下面便详细解析下 IntervalTree 的结构。

TimelinePlayable利用IntervalTree来管理RuntimeClipUML类图如下

IntervalTree UML类图
  • m_Entries记录所有 RuntimeClip 和其左右边界的List
  • m_NodesIntervalTree中的节点
    • node first/last 表示存储在当前节点中的所有Runtimeclip中的首和尾index
    • center排序前比较重要表示待排序Runtimeclip左右边界中点用于后续递归地进行二分排序
    • left/right左右子节点

Build IntervalTree 逻辑:

private void Rebuild()
{
	m_Nodes.Clear();
	m_Nodes.Capacity = m_Entries.Capacity;
	Rebuild(0, m_Entries.Count - 1);
}

private int Rebuild(int start, int end)
{
	IntervalTreeNode intervalTreeNode = new IntervalTreeNode();

	// minimum size, don't subdivide
	int count = end - start + 1;
	if (count < kMinNodeSize)
	{
		intervalTreeNode = new IntervalTreeNode() { center = kCenterUnknown, first = start, last = end, left = kInvalidNode, right = kInvalidNode };
		m_Nodes.Add(intervalTreeNode);
		return m_Nodes.Count - 1;
	}

	var min = Int64.MaxValue;
	var max = Int64.MinValue;

	for (int i = start; i <= end; i++)
	{
		var o = m_Entries[i];
		min = Math.Min(min, o.intervalStart);
		max = Math.Max(max, o.intervalEnd);
	}

	var center = (max + min) / 2;
	intervalTreeNode.center = center;

	// first pass, put every thing left of center, left
	int x = start;
	int y = end;
	while (true)
	{
		while (x <= end && m_Entries[x].intervalEnd < center)
			x++;

		while (y >= start && m_Entries[y].intervalEnd >= center)
			y--;

		if (x > y)
			break;

		var nodeX = m_Entries[x];
		var nodeY = m_Entries[y];

		m_Entries[y] = nodeX;
		m_Entries[x] = nodeY;
	}

	intervalTreeNode.first = x;

	// second pass, put every start passed the center right
	y = end;
	while (true)
	{
		while (x <= end && m_Entries[x].intervalStart <= center)
			x++;

		while (y >= start && m_Entries[y].intervalStart > center)
			y--;

		if (x > y)
			break;

		var nodeX = m_Entries[x];
		var nodeY = m_Entries[y];

		m_Entries[y] = nodeX;
		m_Entries[x] = nodeY;
	}

	intervalTreeNode.last = y;

	// reserve a place
	m_Nodes.Add(new IntervalTreeNode());
	int index = m_Nodes.Count - 1;

	intervalTreeNode.left = kInvalidNode;
	intervalTreeNode.right = kInvalidNode;

	if (start < intervalTreeNode.first)
		intervalTreeNode.left = Rebuild(start, intervalTreeNode.first - 1);

	if (end > intervalTreeNode.last)
		intervalTreeNode.right = Rebuild(intervalTreeNode.last + 1, end);

	m_Nodes[index] = intervalTreeNode;
	return index;
}

构建思路非常简单:

  • 排序开始时遍历所有Runtimeclip确定最左端和最右端边界然后取边界的中点作为根节点的center这样构建的tree不至于一边短一边长。
  • 然后把所有Runtimeclip完全在中间值center左侧的放到m_Entries靠下位置把所有Runtimeclip完全在中间值center右侧的放到m_Entries靠上位置被center贯穿的Runtimeclip保留在当前节点中。
  • 这样就完成了初步排序然后再递归地排序靠下的这坨并作为左子节点再递归地排序靠上的这坨并作为右子节点。这样整棵IntervalTree就构建完成了。

最终结构如下图所示:

IntervalTree内部结构示例

RuntimeClip 结构

下面就以 AnimationClip 为例讲解IntervalTreeNode中RuntimeClip的创建逻辑

  1. 创建TimelineClip 主逻辑在TrackAsset

    1. 根据TrackClipTypeAttribute的定义获取限定的ClipTypeGetType().GetCustomAttributes(typeof(TrackClipTypeAttribute))
    2. 创建限定ClipType的 TimelineClip 容器newClip = CreateNewClipContainerInternal()
    3. 把特定类型 ClipPlayableAsset 比如AnimationPlayableAsset塞进TimelineClip中asset参数中。此时AnimationPlayableAsset中clip为null
  2. 将具体AnimationClip塞进AnimationPlayableAsset的clip变量中AddClipOnTrack(newClip, parentTrack, candidateTime, assignableObject, state)

  3. 创建RuntimeClip 主逻辑在TrackAsset 的 CompileClips方法

    1. 根据timelineClip和clip对应的Playable创建RuntimeClip
    2. 将新建的RuntimeClip加入区间树IntervalTree
  4. 重新排序区间树节点(可延迟执行)

最终TimelineClip 与 RuntimeClip结构如下

TimelineClip 与 RuntimeClip结构

运行时 IntervalTree

我们使用区间树 IntervalTree 最主要目的就是能快速搜索RuntimeClip为什么呢

因为每帧Timeline都会执行PrepareFrame方法指明哪些clip在当前时间激活哪些clip在当前时间停止如果只是用List结构搜索时间复杂度 O(n)而使用IntervalTree时间复杂度便降到 O(logn)了。

TimelinePlayable PrepareFrame 方法执行逻辑:

  1. 利用IntervalTree获取当前帧所有激活的RuntimeClip
  2. disable上一帧激活这一帧未激活的clip会执行Playable的Pause()方法
  3. enable这一帧激活的clip会执行Playable的Play()方法
  4. 根据mixin/mixout curve设置此clip所在的mixer input weight权重

PlayableGraph 的生命周期

前文运行时多次提到了 PrepareFrame、ProcessFrame其实这就是ScriptPlayable在某段生命周期执行的回调函数,我们可以利用 ScriptPlayable 传入自定义PlayableBehaviour 来干涉Playable的生命周期。

Playable的生命周期有

  • GraphStartgraph play 开始时
  • GraphStopgraph stop时
  • PlayableCreatePlayable Create时
  • PlayableDestroyPlayable Destroy时
  • BehaviourPlayPlayable 运行时
  • BehaviourPausePlayable 暂停时
  • PrepareFrame:每帧处理数据前
  • ProcessFrame:每帧开始处理数据时

当然可能并非都是这些生命周期,比如AnimationScriptPlayable就有个ProcessAnimationProcessRootMotion过程可能就没有所谓的ProcessFrame了。

当然对于ScriptPlayable上面的生命周期是确定的在Playable处于上面的生命周期时便会执行对应的回调函数注册在PlayableBehaviour里

下面的自定义PlayableBehaviour就是简单打印回调方法和playable

public class TimelineTestForAnimationScriptBehaviour : PlayableBehaviour
    {
        public override void OnGraphStart(Playable playable)
        {
            DebugLog(playable,"OnGraphStart");
        }

        private static void DebugLog(Playable playable, string methodName)
        {
            Debug.Log($"Playable by in/output cnt_{playable.GetInputCount()}{playable.GetOutputCount()} Behaviour:{methodName}");
        }

        public override void OnGraphStop(Playable playable)
        {
            DebugLog(playable,"OnGraphStop");
        }

        public override void OnPlayableCreate(Playable playable)
        {
            DebugLog(playable,"OnPlayableCreate");
        }

        public override void OnPlayableDestroy(Playable playable)
        {
            DebugLog(playable,"OnPlayableDestroy");
        }

        public override void OnBehaviourPlay(Playable playable, FrameData info)
        {
            DebugLog(playable,"OnBehaviourPlay");
        }

        public override void OnBehaviourPause(Playable playable, FrameData info)
        {
            DebugLog(playable,"OnBehaviourPause");
        }

        public override void PrepareFrame(Playable playable, FrameData info)
        {
            DebugLog(playable,"PrepareFrame");
        }
        
         public override void ProcessFrame(Playable playable, FrameData info, object playerData)
        {
            DebugLog(playable,"ProcessFrame");
         }
    }

生命周期验证

上面生命周期的理解还比较模糊,尤其父子节点回调执行顺序是什么需要验证。

验证代码如下(给不同节点设置不同端口数能很方便辨析是哪个节点的回调):

using System;
using Scenes.TimelineTest.scripts;
using UnityEditor;
using UnityEngine;
using UnityEngine.Playables;

public class TimelineTestForLifeCycle : MonoBehaviour
{
    public PlayableGraph graph;

    private ScriptPlayable<TimelineTestForAnimationScriptBehaviour> playable3;
    public void CreateGraph()
    {
        DestroyGraph();
        graph = PlayableGraph.Create("TimelineTestForLifeCycle");
        ScriptPlayableOutput output = ScriptPlayableOutput.Create(graph,"LifeCycleTestOutput");
        var playable5 = ScriptPlayable<TimelineTestForAnimationScriptBehaviour>.Create(graph,5);
        var playable4 = ScriptPlayable<TimelineTestForAnimationScriptBehaviour>.Create(graph,4);
        playable3 = ScriptPlayable<TimelineTestForAnimationScriptBehaviour>.Create(graph,3);
        var playable2 = ScriptPlayable<TimelineTestForAnimationScriptBehaviour>.Create(graph,2);
        var playable1 = ScriptPlayable<TimelineTestForAnimationScriptBehaviour>.Create(graph,1);
        output.SetSourcePlayable(playable5,0);
        output.SetWeight(1);
        playable5.ConnectInput(0,playable1,0,1);
        playable5.ConnectInput(1,playable2,0,0.5f);
        playable3.Pause();
        playable5.ConnectInput(2,playable3,0,1);
        playable5.ConnectInput(3,playable4,0,0);
        
        var playable9 = ScriptPlayable<TimelineTestForAnimationScriptBehaviour>.Create(graph,9);
        var playable8 = ScriptPlayable<TimelineTestForAnimationScriptBehaviour>.Create(graph,8);
        playable9.ConnectInput(0,playable8,0,1);
    }

    public void PlayPlayable3()
    {
        playable3.Play();
    }
    public void PausePlayable3()
    {
        playable3.Pause();
    }
    public void DestroyPlayable3()
    {
        playable3.Destroy();
    }

    public void PlayGraph()
    {
        graph.Play();
    }
    private void OnDestroy()
    {
        DestroyGraph();
    }

    public void DestroyGraph()
    {
        if (graph.IsValid())
        {
            Debug.Log("Execute Graph Destroy");
            graph.Destroy();
            Debug.Log("Graph destroyed");
        }
    }
}

[CustomEditor(typeof(TimelineTestForLifeCycle))]
class TimelineTestForLifeCycleEditor : Editor
{
    public override void OnInspectorGUI()
    {
        base.OnInspectorGUI();
        TimelineTestForLifeCycle script = (TimelineTestForLifeCycle) target;
        if (GUILayout.Button("CreateGraph"))
        {
            script.CreateGraph();
        }
        if (GUILayout.Button("PlayGraph"))
        {
            script.PlayGraph();
        }
        if (GUILayout.Button("Destroy graph"))
        {
            script.DestroyGraph();
        }
        if (GUILayout.Button("Play Playable3"))
        {
            script.PlayPlayable3();
        }

        if (GUILayout.Button("Pause Playable3"))
        {
            script.PausePlayable3();
        }
        if (GUILayout.Button("Destroy Playable3"))
        {
            script.DestroyPlayable3();
        }
    }
}

点击 “CreateGraph” 后的 graph 结构图中标注的是节点设置的play state和连接权重

生命周期验证示例的graph图

当前脚本主要测试graph的 create与play除了prepareFrame、processFrame验证

  • graph不playplayable节点是否会运行
  • 没有playableOutputplayable节点是否会运行
  • 输出权重为0playable节点是否会运行
  • pause状态playable节点是否会运行
  • PrepareFrame、ProcessFrame顺序
  • Graph destroy执行效果

打印结果:

// 打印
Playable by in/output cnt_51 Behaviour:OnPlayableCreate  
Playable by in/output cnt_41 Behaviour:OnPlayableCreate  
Playable by in/output cnt_31 Behaviour:OnPlayableCreate  
Playable by in/output cnt_21 Behaviour:OnPlayableCreate  
Playable by in/output cnt_11 Behaviour:OnPlayableCreate  
Playable by in/output cnt_91 Behaviour:OnPlayableCreate  
Playable by in/output cnt_81 Behaviour:OnPlayableCreate  
Playable by in/output cnt_51 Behaviour:OnGraphStart  
Playable by in/output cnt_51 Behaviour:OnBehaviourPlay  
Playable by in/output cnt_11 Behaviour:OnGraphStart  
Playable by in/output cnt_11 Behaviour:OnBehaviourPlay  
Playable by in/output cnt_21 Behaviour:OnGraphStart  
Playable by in/output cnt_21 Behaviour:OnBehaviourPlay  
Playable by in/output cnt_31 Behaviour:OnGraphStart  
Playable by in/output cnt_31 Behaviour:OnBehaviourPause  
Playable by in/output cnt_41 Behaviour:OnGraphStart  
Playable by in/output cnt_41 Behaviour:OnBehaviourPlay  
  
Playable by in/output cnt_51 Behaviour:PrepareFrame  
Playable by in/output cnt_11 Behaviour:PrepareFrame  
Playable by in/output cnt_21 Behaviour:PrepareFrame  
Playable by in/output cnt_41 Behaviour:PrepareFrame  
Playable by in/output cnt_11 Behaviour:ProcessFrame  
Playable by in/output cnt_21 Behaviour:ProcessFrame  
Playable by in/output cnt_41 Behaviour:ProcessFrame  
Playable by in/output cnt_51 Behaviour:ProcessFrame  
  
Graph destroyed  
Playable by in/output cnt_51 Behaviour:OnBehaviourPause  
Playable by in/output cnt_11 Behaviour:OnBehaviourPause  
Playable by in/output cnt_21 Behaviour:OnBehaviourPause  
Playable by in/output cnt_41 Behaviour:OnBehaviourPause  
Playable by in/output cnt_51 Behaviour:OnGraphStop  
Playable by in/output cnt_51 Behaviour:OnPlayableDestroy  
Playable by in/output cnt_41 Behaviour:OnGraphStop  
Playable by in/output cnt_41 Behaviour:OnPlayableDestroy  
Playable by in/output cnt_31 Behaviour:OnGraphStop  
Playable by in/output cnt_31 Behaviour:OnPlayableDestroy  
Playable by in/output cnt_21 Behaviour:OnGraphStop  
Playable by in/output cnt_21 Behaviour:OnPlayableDestroy  
Playable by in/output cnt_11 Behaviour:OnGraphStop  
Playable by in/output cnt_11 Behaviour:OnPlayableDestroy  
Playable by in/output cnt_91 Behaviour:OnPlayableDestroy  
Playable by in/output cnt_81 Behaviour:OnPlayableDestroy

结合Graph Monitor 可以得出结论:

  • graph不playplayable节点不会运行
  • 没有连ScriptPlayableOutputScriptPlayable节点不会运行playable time不随时间增加且除了OnPlayableCreate和OnPlayableDestroy其他回调方法不会触发
  • 输出权重为0playable节点依然运行
  • pause状态playable节点不会运行
  • graph未playplayable执行pause并不会触发OnBehaviourPause
  • graph 执行Play() 后若playable是playing状态时则触发OnBehaviourPlay回调是paused状态时则触发OnBehaviourPause回调
  • 会以前序遍历父节点优先的方式触发OnGraphStart 和 OnBehaviourPlay/Pause
  • 会以前序遍历父节点优先的方式触发PrepareFrame回调
  • 会以后序遍历子节点优先的方式触发ProcessFrame回调
  • 没有连接playableOutput的playable节点不会触发OnGraphStart/OnGraphStop回调
  • 直接Destroy graph时如果graph未stop会先触发OnBehaviourPause、OnGraphStop回调这两个回调的触发逻辑与先Stop Graph后Destroy Graph 略有不同,如下日志:
// 先Stop graph 再 Destroy graph的日志与直接Destroy graph有差异
graph.Stop()

Playable by in/output cnt_51 Behaviour:OnBehaviourPause  
Playable by in/output cnt_51 Behaviour:OnGraphStop  
Playable by in/output cnt_11 Behaviour:OnBehaviourPause  
Playable by in/output cnt_11 Behaviour:OnGraphStop  
Playable by in/output cnt_21 Behaviour:OnBehaviourPause  
Playable by in/output cnt_21 Behaviour:OnGraphStop  
Playable by in/output cnt_31 Behaviour:OnBehaviourPause  
Playable by in/output cnt_31 Behaviour:OnGraphStop  
Playable by in/output cnt_41 Behaviour:OnBehaviourPause  
Playable by in/output cnt_41 Behaviour:OnGraphStop  
   
Graph.Destroy()  
  
Playable by in/output cnt_51 Behaviour:OnPlayableDestroy  
Playable by in/output cnt_41 Behaviour:OnPlayableDestroy  
Playable by in/output cnt_31 Behaviour:OnPlayableDestroy  
Playable by in/output cnt_21 Behaviour:OnPlayableDestroy  
Playable by in/output cnt_11 Behaviour:OnPlayableDestroy  
Playable by in/output cnt_91 Behaviour:OnPlayableDestroy  
Playable by in/output cnt_81 Behaviour:OnPlayableDestroy

完整生命周期:

总结

Timeline和Playable系统本文还是花了不少精力的

  • 详细展示了Timeline和Playable各个类的UML关系
  • 系统梳理了Timeline静态和运行时结构与逻辑
  • 详细分析了Playable、区间树IntervalTree等内部结构
  • 验证了很多细节比如AnimationOutput weight作用、ScriptPlayable生命周期、Passthrough TraversalMode的效果等

核心都在下面这张图中,再回顾下:

吃透了本文自定义Track就是易如反掌了。

下一篇可能写点技能编辑器相关。

记得点赞、关注、收藏哦~