【Unity博客节选】Timeline 内部结构 IntervalTree 分析
注:软件版本Unity 6.0 + Timeline 1.8.7
作者:CSDN @ RingleaderWang
原文:《Unity第25期——Timeline结构及其源码浅析》
文章首发Github👍:《Timeline结构及其源码浅析》
Bilibili 视频版👍👍
:《Timeline结构及其源码解析》https://www.bilibili.com/video/BV1bHjYzNE35
Unity的 Timeline 本质是一个包含很多可播放片段(Playable Clip)的区间树(IntervalTree),这个区间树可以排序、搜索、以及控制所有 Clip的激活与停止,最后利用底层的Playable系统,运行所激活的Clips,就是这么个东西。
(当然这么定义不够准确,IntervalTree不控制clip激活,只是排序和搜索clip。IntervalTree本质是装clip的容器,不考虑性能的话用数组也行。这么定义只是方便理解Timeline的底层结构罢了)
下面便详细解析下 IntervalTree 的结构。
TimelinePlayable利用IntervalTree来管理RuntimeClip,UML类图如下:
- m_Entries:记录所有 RuntimeClip 和其左右边界的List
- m_Nodes:IntervalTree中的节点
- 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 subdivideint 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, leftint 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 righty = 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 placem_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就构建完成了。
最终结构如下图所示:
RuntimeClip 结构
下面就以 AnimationClip 为例,讲解IntervalTreeNode中RuntimeClip的创建逻辑:
-
创建TimelineClip (主逻辑在TrackAsset)
- 根据TrackClipTypeAttribute的定义获取限定的ClipType
GetType().GetCustomAttributes(typeof(TrackClipTypeAttribute))
, - 创建限定ClipType的 TimelineClip 容器
newClip = CreateNewClipContainerInternal()
- 把特定类型 ClipPlayableAsset 比如AnimationPlayableAsset塞进TimelineClip中asset参数中。(此时AnimationPlayableAsset中clip为null)
- 根据TrackClipTypeAttribute的定义获取限定的ClipType
-
将具体AnimationClip塞进AnimationPlayableAsset的clip变量中
AddClipOnTrack(newClip, parentTrack, candidateTime, assignableObject, state)
-
创建RuntimeClip (主逻辑在TrackAsset 的 CompileClips方法)
- 根据timelineClip和clip对应的Playable创建RuntimeClip
- 将新建的RuntimeClip加入区间树IntervalTree中
-
重新排序区间树节点(可延迟执行)
最终,TimelineClip 与 RuntimeClip结构如下:
运行时 IntervalTree
我们使用区间树 IntervalTree 最主要目的就是能快速搜索RuntimeClip,为什么呢?
因为每帧Timeline都会执行PrepareFrame方法,指明哪些clip在当前时间激活,哪些clip在当前时间停止,如果只是用List结构,搜索时间复杂度 O(n)
,而使用IntervalTree,时间复杂度便降到 O(logn)
了。
TimelinePlayable
PrepareFrame 方法执行逻辑:
- 利用IntervalTree获取当前帧所有激活的RuntimeClip
- disable上一帧激活,这一帧未激活的clip(会执行Playable的Pause()方法 )
- enable这一帧激活的clip(会执行Playable的Play()方法 )
- 根据mixin/mixout curve设置此clip所在的mixer input weight权重