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() 把所有轨道重新绑一遍。
7.2 核心要点速览
Timeline 核心概念
Timeline 是 Unity 的时间轴多轨道编排工具,本质是 Playable 系统的上层封装。
| 概念 | 角色 | 职责 |
|---|---|---|
| TimelineAsset | 剧本 | 静态资产,存储轨道、片段、绑定需求,本身不执行 |
| PlayableDirector | 导演 | 运行时入口,读剧本、建图、控制播放 |
| PlayableGraph | 舞台 | 运行时图,由 Director 根据 Asset 编译生成 |
| Playable | 戏 | 图中节点,每帧执行实际逻辑 |
执行链路:Timeline 窗口编辑 → 保存为 .playable → Director 引用并设 Binding → Play() 时实例化 Asset、建 Graph、每帧评估。
Timeline 窗口结构
窗口分五块:播放/预览区、当前 Timeline 标题栏、窗口选项、轨道列表、时间轴内容区。
常用操作:
- 添加轨道:左上角
+下拉选择 - 放片段:拖 Clip 进时间轴,或轨道右键添加
- 调时间:拖播放头、缩放标尺、放大时间轴精细对齐
轨道类型一览
| 轨道 | 绑定对象 | 用途 | 特点 |
|---|---|---|---|
| Activation Track | GameObject | 控制激活/禁用 | Clip 覆盖区间内激活,区间外禁用 |
| Animation Track | Animator | 播动画、录关键帧 | 支持 Extrapolation、Blend Curves |
| Audio Track | AudioSource | 播音频 | 支持淡入淡出、音量调节 |
| Control Track | GameObject | 控粒子、嵌套子 Timeline | 可配 Prefab 实例化时机 |
| Signal Track | SignalReceiver | 时间点事件 | 通过 SignalAsset → UnityEvent 触发回调 |
| Track Group | - | 纯分组容器 | 不参与播放,仅整理轨道 |
Animation Track 关键参数
Clip 级别:
| 参数 | 作用 | 常用场景 |
|---|---|---|
| Clip In | 从源动画第几秒开始截取 | 跳过动画开头 |
| Speed Multiplier | 播放速度倍率 | 加速/减速动画 |
| Pre/Post Extrapolate | 片段外如何补姿势 | Post=Hold 让角色停在最后一帧 |
| Ease In/Out | 淡入淡出时间 | 片段重叠混合时调柔和度 |
| Blend Curves | 权重曲线 | 手动控制混合过渡形状 |
Track 级别:
| 参数 | 作用 |
|---|---|
| Track Offsets | 给整条轨道加位置/旋转偏移 |
| Apply Avatar Mask | 只影响指定骨骼(上半身/下半身分层) |
| Apply Foot IK | 轨道级别脚 IK 开关 |
Signal 工作流
Signal 是 Timeline 里做”时间点事件”的标准方案,链路如下:
SignalAsset(消息类型)→ Signal Emitter(时间点发信)→ SignalReceiver(收信)→ Reaction(UnityEvent 回调)
实现步骤:
- Project 右键
Create → Signal创建 SignalAsset - Signal Track 上放置 Signal Emitter,Inspector 选要发的 SignalAsset
- 轨道绑定对象上挂
SignalReceiver组件 - SignalReceiver 里配置
SignalAsset → UnityEvent映射
运行时换绑注意:Reaction 存在 SignalReceiver 组件上,换绑定对象后新对象上也要配同一条 Reaction,否则收到信号不知道调哪个函数。
PlayableDirector 常用 API
播控方法:
| 方法 | 作用 | 注意 |
|---|---|---|
Play() |
开始播放 | 必要时构建 Graph |
Pause() |
暂停 | Graph 保留,时间停在当前帧 |
Stop() |
停止 | 会销毁 Graph,下次 Play 重建 |
Evaluate() |
立即评估一帧 | Manual 模式必用,跳时间后刷新用 |
关键属性:
| 属性 | 作用 |
|---|---|
time |
当前时间(秒),可读写 |
duration |
总时长 |
state |
播放状态(Playing / Paused / Stopped) |
playableGraph |
当前 Graph 句柄,Stop 后无效 |
Wrap Mode:
| 模式 | 播完行为 |
|---|---|
| Hold | 停在最后一帧 |
| Loop | 从头循环 |
| None | 播完结束,属性恢复播前状态 |
运行时绑定:SetGenericBinding
Timeline 资产是静态的,运行时要动态绑定场景对象。
遍历 outputs 绑定:
foreach (PlayableBinding binding in director.playableAsset.outputs)
{
// binding.sourceObject 是轨道对象(当 key 用)
// binding.outputTargetType 是期望的绑定类型
if (binding.outputTargetType == typeof(Animator))
{
director.SetGenericBinding(binding.sourceObject, myAnimator);
}
}
优势:按对象引用绑定,不受轨道改名、顺序调整影响。
Timeline 建图源码要点
入口:TimelineAsset.CreatePlayable() → TimelinePlayable.Create()
核心流程:
- 创建
TimelinePlayable作为根节点 - 遍历
GetOutputTracks()取所有输出轨道 - 每条轨道调
CreateTrackMixer()建 Mixer 节点 - 轨道内部用
IntervalTree存所有 Clip 的时间区间 - 每帧
PrepareFrame()按当前时间查区间,算权重写到输入端口
IntervalTree 的作用:把”每帧遍历所有 Clip 判断是否活跃”从 O(n) 降到区间查询,Clip 数量多时性能提升明显。
自定义轨道四件套
写自定义轨道需要四个类:
| 类 | 职责 | 关键方法 |
|---|---|---|
| TrackAsset | 轨道定义 | CreateTrackMixer() 创建 Mixer |
| PlayableAsset | Clip 数据描述 | CreatePlayable() 创建运行时节点 |
| PlayableBehaviour | Clip 运行时逻辑 | ProcessFrame() 每帧执行 |
| TrackMixerBehaviour | 轨道混合逻辑 | 处理多 Clip 重叠时的权重混合 |
不需要混合时:Mixer 可简化为遍历输入找权重最大的执行,但四个类的壳子仍需写。
过场编辑器架构
核心思路:一个过场 = 一个 Prefab。
CutsceneRoot 职责:
- 持有
masterDirector(主 PlayableDirector) - 收集所有
CutsceneElement(相机、角色、特效等参与者) - 管理 SubCutsceneGroup(子过场拆分)
播放生命周期:
加载 Prefab → 实例化 → StartLoading() 预加载资源 → Play() 播放 → stopped 事件 → 清理销毁
预加载策略:所有角色不管第几秒出场,启动前全部加载完毕,避免播放中途异步卡顿。
自研表现序列系统
技能表现场景不适合用 Timeline,原因:
- 不需要 Clip 混合
- 节点类型多但逻辑简单
- 配置要轻量可热更
- 新增节点不想写四件套
三层架构:
| 层 | 组成 | 职责 |
|---|---|---|
| 编辑器 | UIToolkit + IMGUI | 可视化编排 |
| 数据层 | Lua table / JSON | 纯文本配置,人眼可读 |
| 运行时 | ActionSequencePlayer → ActionSequence → ActionNode | Update 循环驱动节点 |
ActionNode 生命周期:
| 回调 | 触发时机 | 用途 |
|---|---|---|
| OnCreate | 序列实例化时 | 预加载资源 |
| OnPlay | 时间进入节点区间 | 播动画/特效/音效 |
| OnUpdate | 节点激活期间每帧 | 更新位置、检查条件 |
| OnEnd | 时间离开节点区间 | 停止、销毁 |
| OnDestroy | 序列销毁时 | 释放资源 |
两套方案对比
| 维度 | Timeline 过场 | 自研表现序列 |
|---|---|---|
| 底层 | PlayableGraph 求值 | 自己的 Update 循环 |
| 混合 | 有(权重求值) | 无,纯并行 |
| 数据格式 | .playable(重型序列化) | Lua/JSON(扁平文本) |
| 新增节点 | 四件套 | 继承基类重写方法 |
| 适用场景 | 过场演出、CG | 技能、交互动作 |
共存方案:自研编辑器里加 PlayCutsceneNode,在表现序列中触发 Timeline 过场,两套打通。
常见坑点
| 坑 | 表现 | 解决 |
|---|---|---|
| WrapMode.None | 播完角色跳回原位 | 想停在最后一帧用 Hold |
| Stop() 销毁 Graph | 之后访问 playableGraph 无效 | 想停在当前帧用 Pause |
| ExposedReference.Resolve() 返回 null | 没有 Director 上下文 | 确保在 Director 生命周期内调用 |
| ControlTrack Prefab 实例化卡顿 | 播放开头卡一下 | 过场编辑器预加载所有角色 |
| Binding 丢失 | Prefab 模式换场景后绑定消失 | 运行时 Play 前用 SetGenericBinding 重绑 |
7.3 面试题精选
基础题
1. Timeline 中 TimelineAsset 和 PlayableDirector 的关系是什么?
题目
请简述 TimelineAsset 和 PlayableDirector 各自的角色,以及它们在 Timeline 播放流程中的关系。
深入解析
TimelineAsset 是静态资产(剧本),存储轨道、片段、绑定需求等配置数据,本身不执行任何逻辑。PlayableDirector 是运行时组件(导演),负责读取 TimelineAsset、构建 PlayableGraph、控制播放进度。
两者的关系可以用”剧本和导演”来类比:
- TimelineAsset 定义”什么时候做什么”(剧本内容)
- PlayableDirector 负责”把剧本搬上舞台执行”(导演调度)
一个 TimelineAsset 可以被多个 PlayableDirector 引用,通过不同的 Binding 配置实现复用——比如同一份过场 Timeline,绑不同角色就能让不同角色演同一套动作。
答题示例
TimelineAsset 是静态剧本,存储轨道和片段配置;PlayableDirector 是运行时导演,负责读剧本、建图、控制播放。
一个 TimelineAsset 可被多个 Director 复用,通过不同 Binding 实现同一套 Timeline 让不同对象执行。
参考文章
- 1.概述.md
- 3.PlayableDirector.md
2. PlayableDirector 的 WrapMode 有哪几种?None 模式有什么特殊行为?
题目
PlayableDirector 的 WrapMode 有哪些选项?None 模式播完后会发生什么?
深入解析
WrapMode 决定 Timeline 播到末尾后的行为:
- Hold:停在最后一帧,角色保持最后姿势、镜头停在最后位置
- Loop:从头循环播放
- None:播完即结束,被 Timeline 修改过的属性会恢复成播前状态
None 模式的”属性恢复”行为容易被误解为 Bug。比如过场里角色从 A 点走到 B 点,播完后角色突然跳回 A 点——这是因为 None 模式下 PlayableGraph 销毁时会还原所有被修改的属性。想让角色停在 B 点,应该用 Hold 模式。
答题示例
WrapMode 有三种:Hold 停在最后一帧,Loop 循环播放,None 播完结束。
None 模式的特殊行为是播完后会恢复被 Timeline 修改过的属性到播前状态。想让角色停在最后位置应该用 Hold。
参考文章
- 3.PlayableDirector.md
3. Signal Track 的工作流程是怎样的?
题目
请描述 Timeline 中 Signal 的工作流程,包括 SignalAsset、Signal Emitter、SignalReceiver、Reaction 各自的作用。
深入解析
Signal 是 Timeline 里做”时间点事件”的标准方案,链路如下:
- SignalAsset:消息类型/主题,在 Project 里创建(
Create → Signal) - Signal Emitter:时间轴上的 Marker,持有 SignalAsset 引用,时间走到该点时发出信号
- SignalReceiver:挂在绑定对象上的组件,接收信号
- Reaction:SignalReceiver 里配置的
SignalAsset → UnityEvent映射,决定收到信号后调哪个函数
换绑定时要注意:Reaction 存在 SignalReceiver 组件上,换绑定对象后新对象上也要配同一条 Reaction,否则收到信号不知道调什么。
答题示例
Signal 工作流程:SignalAsset 定义消息类型 → Signal Emitter 在时间点发信 → SignalReceiver 收信 → Reaction(UnityEvent)执行回调。
换绑定时要注意 Reaction 存在 Receiver 组件上,新绑定对象也要配 Reaction。
参考文章
- 2.Timeline轨道.md
进阶题
1. Play()、Pause()、Stop() 三个方法的区别是什么?Stop() 有什么需要注意的?
题目
PlayableDirector 的 Play()、Pause()、Stop() 有什么区别?调用 Stop() 后再访问 playableGraph 会怎样?
深入解析
三个方法的区别:
| 方法 | 行为 | Graph 状态 | 时间 |
|---|---|---|---|
| Play() | 开始/继续播放 | 构建或保持 | 从当前时间继续 |
| Pause() | 暂停 | 保留 | 停在当前帧 |
| Stop() | 停止 | 销毁 | 重置到 InitialTime |
关键点是 Stop() 会销毁 PlayableGraph。调用 Stop() 后再访问 director.playableGraph 会拿到无效句柄,可能导致空引用或崩溃。
需要”停在当前帧但保留 Graph”的场景,应该用 Pause() 而不是 Stop()。Stop() 适合彻底结束播放、需要重建 Graph 的场景。
答题示例
Play() 开始播放并构建 Graph;Pause() 暂停但保留 Graph 和当前时间;Stop() 停止并销毁 Graph。
Stop() 后访问 playableGraph 会拿到无效句柄。想停在当前帧应该用 Pause(),Stop() 用于彻底结束需要重建的场景。
参考文章
- 3.PlayableDirector.md
2. 如何在运行时动态绑定 Timeline 轨道?遍历 outputs 有什么优势?
题目
如何在运行时动态修改 Timeline 的轨道绑定?遍历 playableAsset.outputs 相比按轨道名查找有什么优势?
深入解析
运行时绑定使用 SetGenericBinding(key, value),其中 key 是 PlayableBinding.sourceObject(轨道对象引用),value 是要绑定的场景对象。
遍历 outputs 的标准写法:
foreach (PlayableBinding binding in director.playableAsset.outputs)
{
if (binding.outputTargetType == typeof(Animator))
{
director.SetGenericBinding(binding.sourceObject, myAnimator);
}
}
相比按轨道名查找的优势:
- 不受改名影响:key 是对象引用,不是字符串
- 不受顺序影响:轨道顺序调整不影响绑定逻辑
- 类型安全:可以通过
outputTargetType判断期望的绑定类型
答题示例
运行时用 SetGenericBinding(binding.sourceObject, target) 绑定,遍历 outputs 获取所有 PlayableBinding。
优势是按对象引用绑定,不受轨道改名、顺序调整影响,且可通过 outputTargetType 判断期望类型。
参考文章
- 3.PlayableDirector.md
3. Timeline 运行时如何高效判断哪些 Clip 处于激活状态?
题目
Timeline 每帧需要判断哪些 Clip 在当前时间区间内,源码中是如何高效实现的?
深入解析
Timeline 使用 IntervalTree(区间树) 数据结构来存储所有 Clip 的时间区间。
建图时,每条轨道把自己的 Clip 按 [start, end] 区间存入 IntervalTree。每帧 PrepareFrame() 时,用当前时间作为查询条件,在 IntervalTree 中快速找出所有覆盖该时间的 Clip。
时间复杂度对比:
- 遍历数组判断:O(n),n 为 Clip 数量
- IntervalTree 区间查询:O(log n + k),k 为结果数量
Clip 数量多时(比如长过场有上百个片段),性能差距明显。这就是为什么 Timeline 能高效处理复杂时间轴编排。
答题示例
Timeline 使用 IntervalTree 存储 Clip 的时间区间,每帧 PrepareFrame() 用当前时间做区间查询。
时间复杂度从遍历数组的 O(n) 降到 O(log n + k),Clip 数量多时性能提升明显。
参考文章
- 4.Timeline源码浅析.md
深度题
1. Timeline 和自研表现序列系统分别适合什么场景?为什么技能表现不适合用 Timeline?
题目
请对比 Timeline 过场编辑器和自研表现序列系统的适用场景,分析技能表现为什么不适合用 Timeline。
深入解析
Timeline 适合的场景:过场演出、CG
核心原因是需要混合:
- 两段动画之间的 MixIn/MixOut 过渡
- Cinemachine 镜头之间的 Blend
- 多轨道的时间对齐和权重求值
这些都依赖 PlayableGraph 的权重求值体系,自己从头写一套成本很高。Timeline 窗口本身是成熟的可视化编辑器,策划美术拖 Clip 就能出效果。
技能表现不适合 Timeline 的原因:
| 特点 | Timeline | 技能需求 |
|---|---|---|
| 混合 | 有权重求值体系 | 不需要,纯并行触发 |
| 配置 | .playable 重型序列化 | 需要 Lua/JSON 轻量热更 |
| 新增节点 | 四件套样板代码 | 继承基类重写方法即可 |
| 运行时 | PlayableGraph 生命周期 | 一个 Update 循环足够 |
技能表现本质是”第几毫秒触发什么”,节点之间纯并行,不存在混合需求。用 PlayableGraph 那套显得过重。
两套方案可以共存:过场用 Timeline,技能用自研。自研编辑器里加 PlayCutsceneNode 就能在表现序列里触发 Timeline 过场。
答题示例
Timeline 适合过场演出,核心是需要 Clip 混合和权重求值,PlayableGraph 提供了这套能力。
技能表现不需要混合,节点纯并行触发;配置要轻量可热更;新增节点不想写四件套;一个 Update 循环就够了。用 Timeline 显得过重,自研更合适。
两套可以共存,过场用 Timeline,技能用自研。
参考文章
- 5.过场编辑器设计.md
- 6.自研表现序列系统设计.md
2. 自定义 Timeline 轨道需要写哪些类?如果不需要 Clip 混合,Mixer 可以怎么简化?
题目
自定义 Timeline 轨道需要实现哪四个类?如果 Clip 之间不需要混合(比如纯事件触发轨道),Mixer 可以如何简化?
深入解析
自定义轨道需要四个类:
| 类 | 职责 | 关键点 |
|---|---|---|
| TrackAsset | 轨道定义 | 声明绑定类型、CreateTrackMixer() 创建 Mixer |
| PlayableAsset | Clip 数据 | CreatePlayable() 创建运行时节点 |
| PlayableBehaviour | Clip 运行时逻辑 | ProcessFrame() 每帧执行 |
| TrackMixerBehaviour | 轨道混合 | 处理多 Clip 重叠时的权重混合 |
不需要混合时的简化:
很多游戏逻辑轨道(对话触发、特效播放、事件通知)不需要 Clip 之间的权重混合——同一时刻只有一个 Clip 在起作用。这种情况下,Mixer 可以简化为:
public override void ProcessFrame(Playable playable, FrameData info, object playerData)
{
int inputCount = playable.GetInputCount();
int maxWeightIndex = -1;
float maxWeight = 0f;
// 找权重最大的 Clip
for (int i = 0; i < inputCount; i++)
{
float weight = playable.GetInputWeight(i);
if (weight > maxWeight)
{
maxWeight = weight;
maxWeightIndex = i;
}
}
// 只执行权重最大的那个
if (maxWeightIndex >= 0)
{
// 执行该 Clip 的逻辑
}
}
但四个类的壳子仍需写,这也是自研编辑器想绕过 Timeline 的原因之一——节点类型多时,每种写四件套很繁琐。
答题示例
四件套:TrackAsset 定义轨道、PlayableAsset 描述 Clip 数据、PlayableBehaviour 执行运行时逻辑、TrackMixerBehaviour 处理混合。
不需要混合时,Mixer 可简化为遍历输入找权重最大的 Clip 执行。但四个类的壳子仍需写,节点类型多时样板代码较繁琐。
参考文章
- 2.Timeline轨道.md
- 4.Timeline源码浅析.md
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com