7.Timeline系列总结
7.1 总结
系列回顾
这个系列 6 篇,前四篇讲 Timeline 本身,后两篇讲怎么拿它做东西、以及什么时候该绕过它自己做。
基础和轨道
Timeline 的基础用法不算复杂。TimelineAsset 是剧本,PlayableDirector 是导演,运行时 Director 把剧本编译成一张 PlayableGraph 交给 Playable 系统去跑——所以之前花那么多篇幅讲 Playable 的建图和求值,到 Timeline 这里就全对上了。
轨道那篇把常用的几种 Track 都过了一遍:
- Activation Track:控显隐
- Animation Track:播动画和录关键帧
- Audio Track:放音效
- Control Track:管粒子和嵌套子 Timeline
- Signal Track:打时间点事件
每种轨道的 Inspector 参数都过了一遍,重点说了 Animation Clip 的 Extrapolation(Pre/Post)和 Blend Curves——过场里调时间对齐经常要碰这些。
自定义轨道的四件套
写 Timeline 自定义轨道绕不开这四个类:
- TrackAsset:轨道本身,声明绑定类型、支持哪些 Clip,建图时创建 Mixer
- PlayableAsset(ClipAsset):Clip 的数据描述,
CreatePlayable()创建运行时节点 - PlayableBehaviour:Clip 对应的运行时逻辑,
ProcessFrame()里做实际的事 - TrackMixerBehaviour:轨道的 Mixer 逻辑,处理多 Clip 重叠时的权重混合
Clip 之间不需要混合的情况(很多游戏逻辑轨道都是这样),Mixer 可以简化成”遍历输入、找权重最大的那个执行”。但四个类的壳子还是得写,这也是自研编辑器想绕过的一个原因——节点类型一多,每种都要写四个文件就很繁琐。
PlayableDirector
PlayableDirector 是运行时操控 Timeline 的入口,常用 API:
Play()/Pause()/Stop():基础播控Evaluate():Manual 模式下的手动刷新,也可以用来跳帧预览SetGenericBinding()/GetGenericBinding():动态改轨道绑定——同一份 Timeline 换不同角色就靠这个
WrapMode 的三种模式(Hold / Loop / None)要注意,None 模式播完后会把 Timeline 修改过的属性恢复成播前状态,第一次碰到容易懵。
建图源码
源码那篇拆了 Timeline 建图的内部流程。TimelineAsset.CreatePlayable() 是入口,内部创建 TimelinePlayable 作为根节点,然后遍历所有轨道,每条轨道建自己的 Mixer,把 Clip 对应的 Playable 接到 Mixer 下面。
最关键的数据结构是 IntervalTree——把所有 clip 按时间区间存进去,每帧 PrepareFrame() 按当前时间查哪些 clip 活跃,再算权重写到对应输入端口上。”资产 → 建图 → 每帧查区间写权重”,这就是 Timeline 运行时干的全部事情。
过场编辑器
过场编辑器在 Timeline 之上搭了一层:CutsceneRoot 做总控、CutsceneElement 抽象参与者(相机、角色、特效、路径……)、自定义轨道处理对话/字幕/幕帘等游戏逻辑、SubCutscene 把长过场拆成可独立编辑的片段。
核心思路是”一个过场 = 一个 Prefab”,运行时加载 → 实例化 → 预加载资源 → Director.Play() → 播完销毁,干干净净。
自研表现编辑器
最后一篇讲了为什么技能表现没有用 Timeline,而是自研了一套轻量编辑器。技能不需要 Clip 混合、节点类型多但逻辑简单、配置要轻量可热更——这些特点让 PlayableGraph 那套显得太重了。
自研方案分三层:
- 编辑器层:UIToolkit + IMGUI
- 数据层:Lua table / JSON 纯文本
- 运行时:
ActionSequencePlayer→ActionSequence→ActionNode
一个 Update 循环按时间驱动节点就够了。两套方案不互斥,过场用 Timeline,技能用自研,各管各的。
Timeline 做过场 vs 自研做技能
选哪个取决于需求特点。
过场演出离不开 Timeline,核心原因就是混合。两段动画之间的 MixIn / MixOut、Cinemachine 镜头之间的 Blend、多轨道的时间对齐——这些都依赖 PlayableGraph 的权重求值体系,自己从头写一套不现实。再加上 Timeline 窗口本身是个成熟的可视化编辑器,策划和美术拖 Clip 就能出效果。
技能表现的情况不一样——需求就是”第几毫秒触发什么”,节点之间纯并行,不存在混合。配置用纯文本(Lua table / JSON),人眼可读、手改方便、热更友好。新增节点类型继承基类重写几个方法就行,不用写四件套。一个 Update 循环够了,不需要建 Graph。所以技能这边用自研更合适。
两套在同一个项目里共存没问题。自研编辑器里加一种 PlayCutsceneNode,就能在表现序列里触发 Timeline 过场,两边打通。
| Timeline 过场编辑器 | 自研表现编辑器 | |
|---|---|---|
| 底层 | PlayableGraph 求值框架 | 自己的 Update 循环 |
| Clip 混合 | 有(MixIn / MixOut / 权重) | 无,纯并行 |
| 数据格式 | .playable(ScriptableObject 序列化) | Lua table / JSON(纯文本) |
| 新增节点成本 | TrackAsset 四件套 | 继承基类重写几个方法 |
| 适用场景 | 过场演出、CG | 技能表现、交互动作、通用表现序列 |
容易踩的坑
WrapMode.None 播完会恢复属性。 Director 的 Wrap Mode 设成 None,Timeline 播完后被修改过的属性会自动恢复成播前的值。第一次碰到会以为是 Bug——“我的角色播完过场怎么又跳回原位了”。想让角色停在最后一帧用 Hold,想循环用 Loop。
自定义轨道的 ExposedReference 需要 Director 才能 Resolve。 ExposedReference<T> 的 Resolve() 需要传 IExposedPropertyTable,运行时就是 PlayableDirector。在没有 Director 上下文的地方调 Resolve 拿到的是 null,排查的时候容易漏掉这个前提。
Stop() 会销毁 Graph。 Director.Stop() 之后 PlayableGraph 会被销毁,再访问 director.playableGraph 会拿到无效句柄。需要”停在当前帧但保留 Graph”的话,应该用 Pause() 而不是 Stop()。
ControlTrack 的 Prefab 实例化时机。 ControlTrack 配了 Prefab 后,实例化发生在 Timeline 开始播放的时候,不是 Clip 开始的时候。Prefab 比较重的话可能导致播放开头卡一下。过场编辑器里的做法是全部预加载——不管角色是第 1 秒出场还是第 50 秒才出场,启动前就全部加载好,播放过程中就不会有异步卡顿了。
Binding 和场景对象的生命周期不同步。 Timeline 资源是 Asset,Binding 里引用的是场景对象。Prefab 模式下编辑完保存、换场景再打开,Binding 可能丢失。运行时动态创建角色的项目,一般会在 Director.Play() 之前用 SetGenericBinding() 把所有轨道重新绑一遍。
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com