【Unity博客节选】Timeline 内部结构 IntervalTree 分析

注:软件版本Unity 6.0 + Timeline 1.8.7

作者:优快云 @ 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类图如下:

IntervalTree 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 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权重

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值