作者:reckzhang,腾讯IEG魔方工作室后台开发工程师

前言

参与游戏场景开发中, 需要设计 Tick 机制用以驱动场景中各个单位, 故对 Unreal 的 Tick 机制进行简要分析学习.

Unreal Tick 机制

Tick 通常表示进程循环中的一次执行, 由进程主函数循环驱动. 因此从 Unreal 主函数开始, 分析 Tick 调用栈信息:

// 主函数循环驱动int32 GuardedMain( const TCHAR* CmdLine ) { while( !IsEngineExitRequested() ) { EngineTick(); }}// 主函数驱动从层级关系GuardedMain(const wchar_t * CmdLine) EngineTick() FEngineLoop::Tick() UGameEngine::Tick(float DeltaSeconds, bool bIdleMode) UWorld::Tick(ELevelTick TickType, float DeltaSeconds)

可知 Unreal Tick 机制 是由主函数 GuardedMain 循环调用 EngineTick() 方法, 最终在 UWorld::Tick 内实现具体逻辑. 分析 UWorld::Tick 代码:

void UWorld::Tick( ELevelTick TickType, float DeltaSeconds ) { // UWorld 逐 Level 驱动 for (int32 i = 0; i < LevelCollections.Num(); ++i) { // 1. Actor Ticking RunTickGroup(TG_PrePhysics); RunTickGroup(TG_StartPhysics); RunTickGroup(TG_DuringPhysics, false); RunTickGroup(TG_EndPhysics); RunTickGroup(TG_PostPhysics); RunTickGroup(TG_PostUpdateWork); RunTickGroup(TG_LastDemotable); // 2. Timer Ticking GetTimerManager().Tick(DeltaSeconds); // 3. Tickable Ticking FTickableGameObject::TickObjects(this, TickType, bIsPaused, DeltaSeconds); }}

提取出 UWorld::Tick() 中的核心逻辑, 可以看到 UWorld::Tick() 是以 Level 为单位逐个遍历, 依次调用三种 Tick 框架, 分别为:

  1. Actor Ticking.
  2. Timer Ticking.
  3. Tickable Ticking.

下面对这三种 Tick 框架进行具体分析.

Actor Ticking

Actor Ticking 服务对象为 Actor / Actor Component, 固定时间间隔执行类内的一段代码或者蓝图脚本. Actor Ticking 可以设置 Tick 的开启与关闭, Tick 的时间间隔, 被调用的时机和前置 Tick 依赖.

使用方法

  • Actor Ticking 给业务提供的两个 Tick 回调, 重载 Tick 执行的具体逻辑:
// Actorvirtual void AActor::Tick( float DeltaSeconds );// Actor Componentvirtual void UActorComponent::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction);
  • Actor Ticking 可以设置 Tick 的开启与关闭, Tick 的时间间隔, 被调用的时机和前置 Tick 依赖. 前置 Tick 依赖在本文中不做赘述, 有兴趣的同学可以了解一下前置 Tick 依赖.
// 1. 开启 Tick(::BeforePlay阶段默认开启)void AActor::SetActorTickEnabled(bool bEnabled)// 2. 设置 Tick 时间间隔, 单位为秒, 如果小于等于0则每帧都会调用void AActor::SetActorTickInterval(float TickInterval);// 3. 设置 Tick 组, 即被调用的时机, 默认为TG_PrePhysicsvoid AActor::SetTickGroup(ETickingGroup NewTickGroup)
  • Tick 组 (Tick Group) 表示在一次Tick中的调用时机, 因此可以表示被调度的优先级. 在 UWorld::Tick 中可以看到各个组按照先后顺序依次调用:
RunTickGroup(TG_PrePhysics);RunTickGroup(TG_StartPhysics);RunTickGroup(TG_DuringPhysics, false);RunTickGroup(TG_EndPhysics);RunTickGroup(TG_PostPhysics);RunTickGroup(TG_PostUpdateWork);RunTickGroup(TG_LastDemotable);

Tick 组

引擎活动

TG_PrePhysics

帧的开始。
- Actor 将与物理对象(包括基于物理的附着物)进行交互时使用的 tick 组。如此,actor 的运动便完成,并可被纳入物理模拟因素。
- 此 tick 中的物理模拟数据属于上一帧 — 也就是上一帧渲染到屏幕上的数据。

TG_DuringPhysics

到达此步骤时,物理模拟已开始。Tick 此组时的任意时候,或所有组成员已 tick 后,模拟完成并更新引擎的物理数据。
- 因它在物理模拟的同时运行,无法确定此 tick 中的物理数据来自上一帧或当前帧。物理模拟可在此 tick 组中的任意时候完成,且不会显示信息来表达此结果。
- 因为物理模拟数据可能来自当前帧或上一帧,此 tick 组只推荐用于无视物理数据或允许一帧偏差的逻辑。常见用途为更新物品栏画面或小地图显示。此处物理数据完全无关,或显示要求不精确,一帧延迟不会造成问题。

TG_PostPhysics

此步骤开始时物理模拟已完成,引擎正在使用当前帧的数据。
- 此 tick 组运行时,此帧的物理模拟结果已完成。
- 此组可用于武器或运动追踪。渲染此帧时所有物理对象将位于它们的最终位置。这尤其适用于射击游戏中的激光瞄准器。在此情况中激光束必须从枪的最终位置发出,即便出现一帧延迟也会十分明显。

n/a

处理隐藏操作、tick 世界时间管理器、更新摄像机、更新关卡流送体积域和流送操作。

TG_PostUpdateWork

- 这在 TG_PostPhysics 之后运行。从以往来看,它的基函数是将最靠后的信息送入粒子系统。
- TG_PostUpdateWork 在摄像机更新后发生。如特效必须知晓摄像机朝向的准确位置,可将控制这些特效的 actor 放置于此。
- 这也可用于在帧中绝对最靠后运行的游戏逻辑,如解决格斗游戏中两个角色在同一帧中尝试抓住对方的情况。

n/a

处理之前在帧中创建的 actor 的延迟生成。完成帧并渲染。

以一个游戏来举例说明如何使用以上列出的每种 tick 组:在这个游戏中玩家控制一个动画 actor 进行激光瞄准,在影响的点上放置一个特殊的瞄准标线 actor。只要激光对准特定类型的目标物体,一个特殊的条便会开始填充。一个 HUD actor 将把此条显示在屏幕上。 玩家的动画 actor 将在 TG_PrePhysics 中移动和执行动画。需要在物理之前完成它的动画设置,使物理模拟对象正常跟随并与之交互。 HUD 可在任意 tick 组中更新,但出于两大原因,TG_DuringPhysics 为优选。第一,TG_DuringPhysics 可接受,因为它不直接与游戏的物理模拟产生交互或使用来自物理模拟的数据。第二,没有原因强制物理模拟等待 HUD 完成更新,也没有原因强制 HUD 等待物理模拟完成。注意,HUD 为游戏后的一帧。对准目标物体时,此帧不会反映到条中,下一帧才会。 标线 actor 在 TG_PostPhysics 中更新。如此标线便可了解其对场景的追踪。因为它将在帧的最后渲染,所以它将正常出现在物体表面。它还将基于目标物体的正确位置调整条的数值。 最后,在 TG_PostUpdateWork 中,激光粒子效果将更新到瞄准 actor 和标线的最后位置
Actor Ticking 组件介绍
  • FTickFunction 是 Actor Ticking 调度的基本单元 , 作为成员变量存储在 Actor / Actor Component 中
class AActor { UPROPERTY(EditDefaultsOnly, Category=Tick) struct FActorTickFunction PrimaryActorTick;}class UActorComponent { UPROPERTY(EditDefaultsOnly, Category="ComponentTick") struct FActorComponentTickFunction PrimaryComponentTick;}
  • FTickTaskLevel 管理 Level 内所有 Tick 的单元. SetActorTickEnable / SetComponentTickEnabled 时, 就会**将 FTickFunction 注册到所在 Level 的 FTickTaskLevel **中.
void AActor::BeginPlay() { // 省略跳转, 最终所在 Level 对应 FTickTaskLevel 将 TickFunction 添加 FTickTaskLevel* Level = TickTaskLevelForLevel(InLevel); Level->AddTickFunction(TickFunction);}class FTickTaskLevel { // 包含当前 Level 的所有 Tick 单元. TSet AllEnabledTickFunctions; FCoolingDownTickFunctionList AllCoolingDownTickFunctions; TSet AllDisabledTickFunctions; TArrayWithThreadsafeAdd TickFunctionsToReschedule; TSet NewlySpawnedTickFunctions;}
  • FTickTaskManager 管理所有 FTickTaskLevel, 是 Tick 的管理单例类.
class FTickTaskManager { // 管理所有 FTickTaskLevel TArray LevelList;} Actor Ticking 调度流程 // 主函数驱动从层级关系GuardedMain(const wchar_t * CmdLine) EngineTick() FEngineLoop::Tick() UGameEngine::Tick(float DeltaSeconds, bool bIdleMode) UWorld::Tick(ELevelTick TickType, float DeltaSeconds) for (int32 i = 0; i < LevelCollections.Num(); ++i) { RunTickGroup(TG_PrePhysics); RunTickGroup(TG_StartPhysics); RunTickGroup(TG_DuringPhysics, false); RunTickGroup(TG_EndPhysics); RunTickGroup(TG_PostPhysics); RunTickGroup(TG_PostUpdateWork); RunTickGroup(TG_LastDemotable); }

UWorld 逐 Level 驱动, 按照 Tick 组顺序调用对应 Actor / Actor Component, 即 RunTickGroup(ETickingGroup Group).

Cooldown 机制

考虑到不同的 Actor / Actor Component 业务场景各不相同, 有些组件不会每帧Tick, 而是可能是几秒钟或者几分钟才会 Tick 一次, 因此 Unreal 设计了 Cooldown 机制来满足此种场景:

  1. 遍历所有 Tick 任务(O(n)), 如果时间间隔大于 0(意味着并非每帧都会调用), 就会放到 CoolingDown 队列.
  2. 根据 Tick 间隔对临时队列排序(O(mlogm)), 遍历临时队列判断到达 Tick 时间的加入就绪队列.
  3. 执行处于就绪队列中的 Tick 任务.
  4. 执行结束后, 将执行完的任务重新放回 CoolingDown队列.

Timer Ticking

Timer Ticking 作为 Unreal 的定时器调度机制, 服务对象为 Delegate.

使用方法

调用 TimerManager 的 SetTimer 方法, 传入需要定时执行的 Delegate 和自定义参数.

/** * Sets a timer to call the given native function at a set interval. If a timer is already set * for this handle, it will replace the current timer. * * @param InOutHandle If the passed-in handle refers to an existing timer, it will be cleared before the new timer is added. A new handle to the new timer is returned in either case. * @param InObj Object to call the timer function on. * @param InTimerMethod Method to call when timer fires. * @param InRate The amount of time (in seconds) between set and firing. If <= 0.f, clears existing timers. * @param InbLoop true to keep firing at Rate intervals, false to fire only once. * @param InFirstDelay The time (in seconds) for the first iteration of a looping timer. If < 0.f InRate will be used. */template< class UserClass >FORCEINLINE void SetTimer(FTimerHandle& InOutHandle, UserClass* InObj, typename FTimerDelegate::TUObjectMethodDelegate< UserClass >::FMethodPtr InTimerMethod, float InRate, bool InbLoop = false, float InFirstDelay = -1.f);GetWorldTimerManager().SetTimer(SelectActorTickHandle, this, &UGameplayDebuggerLocalController::OnSelectActorTick, 0.01f, bLooping); Timer Ticking 实现机制

Timer Ticking 定时器基于实现, TimerManager 在每次 Tick 时从检查堆顶元素, 如果到了执行时间就取出并执行, 直到条件不满足停止本次 Tick.

void FTimerManager::Tick(float DeltaTime) { while (ActiveTimerHeap.Num() > 0) { // 堆定定时任务到达执行时间 if (InternalTime > Top->ExpireTime) { // 执行定时任务 ActiveTimerHeap.HeapPop(CurrentlyExecutingTimer, FTimerHeapOrder(Timers), /*bAllowShrinking=*/ false); Top->TimerDelegate.Execute(); } }}

Tickable Ticking

Tickable Ticking 服务对象为 C++ 类, 继承自 FTickableGameObject 基类, 获得 Tick 能力.

使用方法

C++类继承FTickableGameObject, 重载 Tick 和 GetStatId 虚函数.

// 1. 继承 FTickableGameObject 基类classs TickableClass : public FTickableGameObject// 2. 实现函数 Tick(float DeltaTime) 和 GetStatId()// Tick 具体逻辑virtual void Tick(float DeltaTime) override;// stats 性能统计埋点, 如果不希望被统计, return TStatId()即可virtual TStatId GetStatId() const override { RETURN_QUICK_DECLARE_CYCLE_STAT(TickableClass, STATGROUP_Tickables);} Tickable Ticking 实现机制

新 Tickable 对象初始化后会添加到单例 FTickableStatics 集合中, FTickableGameObject 单例在每次 Tick 后遍历 FTickableStatics 集合中的所有 Tickable 对象并执行, 因此 Tickable 对象会在每一帧执行, 不能设置 Tick 的时间间隔.

void FTickableGameObject::TickObjects(UWorld* World, const int32 InTickType, const bool bIsPaused, const float DeltaSeconds) { for (const FTickableObjectEntry& TickableEntry : Statics.TickableObjects) { TickableObject->Tick(DeltaSeconds); }}

总结

横向比较 Unreal Tick 中 Actor Ticking / Timer Ticking / Tickable Ticking 三种实现机制:

Actor Ticking

Timer Ticking

Tickable Ticking

实现机制

Cooldown 机制, 以 Tick 间隔维护有序队列

以 Tick 间隔维护小顶堆

无序数组

插入复杂度

O(1)

O(logn)

O(1)

删除复杂度

O(n)

O(n)

O(n)

Tick 复杂度

O(nlogn)

O(nlogn)

O(n)

服务对象

Actor / Actor Component

Delegate

C++类

设置 Tick 时间间隔

可以

可以

不可以

特点

1. 通过设置 Tick 组, 确定 Tick 被调用的时机.
2. 可以设置 Tick 之间的前置依赖关系.

1. 粒度细化为 Delegate.

1. 每帧调用, 不能设置 Tick 时间间隔.