延长 Timeline:实用指南

Unity 在推出 Unity 2017.1 的同时还推出了 Timeline,从那时起,我们就收到了很多关于它的反馈。在与许多开发人员交流并在论坛上回复用户的意见后,我们意识到许多用户希望 Timeline 不仅仅是一个简单的 Sequences 工具。我已经就此发表过几次演讲(例如,在2017 年奥斯汀 Unite 大会上),介绍如何将 Timeline 用于非常规用途。
Timeline 从设计之初就将可扩展性作为主要目标;设计该功能的团队始终考虑到,除了内置的剪辑和音轨外,用户还希望创建自己的剪辑和音轨。因此,有很多关于使用 Timeline 编写脚本的问题。Timeline 所依托的系统功能强大,但对于非初学者来说可能难以操作。
但首先,什么是 Timeline?它是一种线性编辑工具,可对不同元素进行排序:动画片段、音乐、音效、摄像机镜头、粒子效果,甚至其他 Timeline。从本质上讲,它与 Premiere®、After Effects® 或 Final Cut® 等工具非常相似,不同之处在于它是为实时回放而设计的。
如果想更深入地了解 Timeline 的基础知识,我建议您访问 Unity 手册中的Timeline 文档部分,因为我将大量使用这些概念。
Timeline 是在Playables API 的基础上实现的。
它是一套功能强大的 API,可让您读取和混合多个数据源(动画、音频等),并通过一个输出端进行播放。该系统提供精确的程序控制,开销低,性能优越。顺便提一下,驱动 Animator 组件的状态机背后也是同样的框架,如果你为 Animator 编程,你可能会看到一些熟悉的概念。
基本上,当 Timeline 开始播放时,就会建立一个由称为 Playables 的节点组成的图。它们被组织成树状结构,称为PlayableGraph。
请注意:如果您想将场景中任何PlayableGraph(动画、Timeline 等)的树形图可视化,可以下载一个名为PlayableGraph Visualizer 的工具。本帖使用它来直观显示不同自定义片段的图表。
接下来,我将通过三个简单的示例向大家介绍如何扩展 Timeline。为了打好基础,我将从在 Timeline 中添加脚本的最简单方法开始。然后,将逐步增加更多的概念,以利用大部分功能。
我打包了一个小型演示项目,其中包含本帖中使用的所有示例。欢迎下载,以便跟读。否则,您可以单独欣赏这篇文章。
请注意:对于资产,我使用前缀来区分每个示例中的类别("Simple_"、"Track_"、"Mixer_"等)。在下面的代码中,为了便于阅读,省略了这些前缀。
第一个示例非常简单:目的是通过自定义剪辑来改变光组件的颜色和强度。要创建自定义剪辑,您需要两个脚本:
- 一个是数据:继承自PlayableAsset
- 逻辑:继承自PlayableBehaviour
Playable API 的核心原则是逻辑与数据分离。因此,您需要首先创建一个PlayableBehaviour,在其中写入您想做的事情,就像这样:
public class LightControlBehaviour : PlayableBehaviour
{
public Light light = null;
public Color color = Color.white;
public float intensity = 1f;
public override void ProcessFrame(Playable playable, FrameData info, object playerData)
{
if (light != null)
{
light.color = color;
light.intensity = intensity;
}
}
}这是怎么回事?首先,这里有关于您想更改 "光 "的哪些属性的信息。此外,PlayableBehaviour有一个名为ProcessFrame的方法,您可以覆盖该方法。
每次更新都会调用ProcessFrame。在该方法中,您可以设置灯光的属性。以下是PlayableBehaviour 中可以覆盖的 方法列表。 然后,为自定义片段创建PlayableAsset:
public class LightControlAsset : PlayableAsset
{
public ExposedReference<Light> light;
public Color color = Color.white;
public float intensity = 1.0f;
public override Playable CreatePlayable (PlayableGraph graph, GameObject owner)
{
var playable = ScriptPlayable<LightControlBehaviour>.Create(graph);
var lightControlBehaviour = playable.GetBehaviour();
lightControlBehaviour.light = light.Resolve(graph.GetResolver());
lightControlBehaviour.color = color;
lightControlBehaviour.intensity = intensity;
return playable;
}
}PlayableAsset有两个用途。首先,它包含剪辑数据,因为这些数据是在 Timeline 资产本身中序列化的。其次,它会创建PlayableBehaviour,该行为将最终出现在 Playable 图表中。
请看第一行:
var playable = ScriptPlayable<LightControlBehaviour>.Create(graph);这会创建一个新的 Playable,并为其附加一个LightControlBehaviour(我们的自定义行为)。然后,您可以在 PlayableBehaviour 上设置灯光属性。
ExposedReference 又是怎么回事?由于PlayableAsset是一种资产,因此无法直接引用场景中的对象。ExposedReference就像一个承诺,当调用CreatePlayable时,将解析一个对象。
现在,您可以在 Timeline 中添加一个可播放轨道,然后右键单击该新轨道添加自定义片段。为素材指定一个光源组件,查看效果。
在这种情况下,内置的可播放轨道是一个通用轨道,可以接受这些简单的可播放片段,比如你刚刚创建的片段。对于更复杂的情况,您需要在专用轨道上托管剪辑。
第一个示例的一个注意事项是,每次添加自定义剪辑时,都需要为每个剪辑分配一个光源组件,如果剪辑数量较多,这可能会很繁琐。您可以使用轨道的绑定对象来解决这个问题。

轨道可以绑定一个对象或组件,这意味着轨道上的每个片段都可以直接对绑定的对象进行操作。这是非常常见的行为,事实上,动画、激活和 Cinemachine 轨道就是这样工作的。
如果想通过多个剪辑修改灯光的属性,可以创建一个自定义轨迹,要求将灯光组件作为绑定对象。要创建自定义轨道,需要另一个扩展TrackAsset 的脚本:
[TrackClipType(typeof(LightControlAsset))]
[TrackBindingType(typeof(Light))]
public class LightControlTrack : TrackAsset {}这里有两个属性:
- TrackClipType指定轨道将接受的PlayableAsset类型。在这种情况下,您将指定自定义LightControlAsset。
- TrackBindingType指定了跟踪要求的绑定类型(可以是 GameObject、Component 或 Asset)。在这种情况下,您需要一个 "光 "组件。
您还需要对PlayableAsset和PlayableBehaviour稍作修改,以使其与轨道配合使用。为便于参考,我已注释掉了不再需要的行。
public class LightControlAsset : PlayableAsset
{
public LightControlBehaviour template;
public override Playable CreatePlayable (PlayableGraph graph, GameObject owner) {
var playable = ScriptPlayable<LightControlBehaviour>.Create(graph, template);
return playable;
}
}PlayableBehaviour现在不需要 Light 变量了。在这种情况下,ProcessFrame方法会直接提供轨道的绑定对象。您只需将对象转换为适当的类型即可。真漂亮
public class LightControlAsset : PlayableAsset
{
//public ExposedReference<Light> light;
public Color color = Color.white;
public float intensity = 1f;
public override Playable CreatePlayable (PlayableGraph graph, GameObject owner)
{
var playable = ScriptPlayable<LightControlBehaviour>.Create(graph);
var lightControlBehaviour = playable.GetBehaviour();
//lightControlBehaviour.light = light.Resolve(graph.GetResolver());
lightControlBehaviour.color = color;
lightControlBehaviour.intensity = intensity;
return playable;
}
}PlayableAsset不再需要为 Light 组件保存ExposedReference。该引用将由轨道管理,并直接提供给PlayableBehaviour。
在 Timeline 中,我们可以添加一个LightControl轨道,并将一个灯光绑定到该轨道上。现在,我们添加到该轨道的每个片段都将在绑定到该轨道的灯光组件上运行。
如果使用图形展示台来显示这个图形,它看起来就像这样:

不出所料,你会看到右侧的夹子由 5 个块组成。你可以把这个盒子看作是轨道。然后,所有内容都进入 Timeline:紫色方框。
注意:名为 "可播放 "的粉色方框实际上是 Unity 为您创建的礼仪混合器 Playable。这就是为什么它和夹子的颜色一样。什么是混合器?我将在下一个例子中介绍混合器。
Timeline 支持重叠片段,以便在它们之间创建混合或交叉淡入淡出。自定义片段还支持混合。要启用它,您需要创建一个混音器,访问所有片段的数据并进行混合。
混音器源于PlayableBehaviour,就像你之前使用的 LightControlBehaviour 一样。事实上,您仍然需要使用ProcessFrame函数。主要区别在于,通过覆盖CreateTrackMixer函数,轨道脚本将 Playable 明确声明为混音器:
[TrackClipType(typeof(LightControlAsset))]
[TrackBindingType(typeof(Light))]
public class LightControlTrack : TrackAsset
{
public override Playable CreateTrackMixer(PlayableGraph graph, GameObject go, int inputCount) {
return ScriptPlayable<LightControlMixerBehaviour>.Create(graph, inputCount);
}
}
为该音轨创建 "可播放图表 "时,还会创建一个新的行为(混音器),并将其连接到音轨上的所有片段。
您还需要将逻辑从PlayableBehaviour移到混音器中。因此,PlayableBehaviour现在看起来很空:
public class LightControlBehaviour : PlayableBehaviour
{
public Color color = Color.white;
public float intensity = 1f;
}
它基本上只包含运行时来自PlayableAsset 的数据。另一方面,混合器将在其ProcessFrame函数中包含所有逻辑:
public class LightControlMixerBehaviour : PlayableBehaviour
{
// NOTE: This function is called at runtime and edit time. Keep that in mind when setting the values of properties.
public override void ProcessFrame(Playable playable, FrameData info, object playerData)
{
Light trackBinding = playerData as Light;
float finalIntensity = 0f;
Color finalColor = Color.black;
if (!trackBinding)
return;
int inputCount = playable.GetInputCount (); //get the number of all clips on this track
for (int i = 0; i < inputCount; i++)
{
float inputWeight = playable.GetInputWeight(i);
ScriptPlayable<LightControlBehaviour> inputPlayable = (ScriptPlayable<LightControlBehaviour>)playable.GetInput(i);
LightControlBehaviour input = inputPlayable.GetBehaviour();
// Use the above variables to process each frame of this playable.
finalIntensity += input.intensity * inputWeight;
finalColor += input.color * inputWeight;
}
//assign the result to the bound object
trackBinding.intensity = finalIntensity;
trackBinding.color = finalColor;
}
}混音师可以访问轨道上的所有片段。在这种情况下,您需要读取当前参与混合的所有片段的强度和颜色值,因此需要用 for 循环遍历它们。在每个循环中,您都要访问输入(GetInput(i)),并使用每个剪辑的权重(GetInputWeight(i))建立最终值,以获得该剪辑对混合的贡献程度。
因此,想象一下有两个素材正在混合:一个是红色,另一个是白色。当混合完成四分之一时,颜色为0.25 * Color.red + 0.75 * Color.white,这时红色会稍微变淡。
循环结束后,将总数应用于绑定的灯光组件。这样,您就可以创建类似这样的内容:

现在你可以看到,红框正是你编程的混音器 Playable,现在你可以完全控制它。这与上面的示例 2 形成鲜明对比,示例 2 中的混合器是 Unity 提供的默认混合器。
此外,请注意,由于图形处于混合中间,绿色方框 2 和 3 都有一条亮线与混合器相连,表明它们的权重分别为 0.5。
请记住,无论何时在混音器中实现混合,都要由您来决定逻辑是什么。混合两种颜色很容易,但如果要混合(例如野生)人工智能系统中代表不同人工智能状态的两个片段,会发生什么情况呢?用户界面中只有两行对话?如何在定格动画中融合两个静态姿势?也许你的混合不是连续的,而是 "阶梯式 "的(因此姿势会相互变形,但以不连续的方式递增):0, 0.25, 0.5, 0.75, 1).
有了这个功能强大的系统,您将有无穷无尽的选择!
作为本指南的最后一步,让我们回到前面的示例,使用我们称之为 "模板 "的东西,以不同的方式移动数据。这种模式的一大优势是可以对模板的属性进行关键帧设置,从而可以直接在 Timeline 上为自定义片段创建动画。
在上一个示例中,我们在PlayableAsset和PlayableBehaviour 上都引用了光组件、颜色和强度。数据是在检查器中的PlayableAsset上设置的,然后在运行时创建图形时将其复制到PlayableBehaviour 中。
这是一种有效的方法,但它会重复数据,而这些数据需要始终保持同步。这很容易导致错误。因此,首先,像这样重写LightControlAsset:
public class LightControlAsset : PlayableAsset
{
public LightControlBehaviour template;
public override Playable CreatePlayable (PlayableGraph graph, GameObject owner) {
var playable = ScriptPlayable<LightControlBehaviour>.Create(graph, template);
return playable;
}
}LightControlAsset 现在只引用LightControlBehaviour,而不是值本身。代码比以前更少了!
保持LightControlBehaviour不变:
[System.Serializable]
public class LightControlBehaviour : PlayableBehaviour
{
public Color color = Color.white;
public float intensity = 1f;
}现在,当您在 Timeline 中选择素材时,对模板的引用会自动生成此 Inspector:

有了这个脚本,就可以制作动画了。请注意,如果创建新片段,您会在音轨标题上看到一个圆形红色按钮。这意味着,现在无需添加动画师就能对素材进行关键帧处理。你只需单击红色按钮,选择片段,将播放头定位到要创建键的位置,然后更改该属性的值。
您还可以单击白色方框按钮展开 "曲线 "视图,查看关键帧创建的曲线:

还有一个额外的好处:你可以双击 Timeline 片段,Unity 会打开 "动画 "面板并将其链接到 Timeline。当该按钮出现时,您会发现它们已被链接:

当出现这种情况时,你可以在 Timeline 和 "动画 "窗口上进行擦写,播放头将保持同步,因此你可以完全控制关键帧。现在,您可以在 "动画 "窗口中修改动画,在更舒适的环境中工作关键帧:

在这个视图中,你可以充分利用动画曲线和 dopesheet 来真正完善自定义片段的动画效果。
注意:当你用这种方法制作动画时,你就是在创建动画片段。您可以在 Timeline 资产下找到它们:

我希望这篇文章能为大家提供有价值的介绍,让大家了解当使用脚本将 Timeline 提升到更高水平时,它可以提供的无限可能性。
请在 Twitter 上给我留言,提出您的问题、反馈和您的 Timeline 作品!
