Files
obsidian-notes/3Projects/UE/UE-CORE-002 TickTaskManager.md
2025-06-16 18:36:04 +08:00

93 KiB
Raw Blame History

source:UE-CORE-002 TickTaskManager


一、概述

1.1 Tick 任务调度的重要性

在游戏引擎中,Tick(如 UE 的 Tick 以及 Unity 的 Update是驱动动态内容更新的核心机制。每一帧的时间内引擎需要完成大量的计算和逻辑处理例如物理模拟、动画更新、AI 决策、用户输入响应等。这些任务的执行顺序和时机直接决定了游戏世界的稳定性和一致性。因此,Tick 任务调度的设计和实现是游戏引擎架构中的关键部分。

Tick 是一个每帧调用一次的函数,用于更新对象的状态或执行特定的逻辑。在 UE 中,几乎所有动态行为都依赖于 Tick 系统。以下是一些常见的 Tick 使用场景:

  • 输入处理 :如根据玩家输入,更新角色的位置和方向。
  • 物理模拟 :计算刚体动力学、碰撞检测和约束求解。
  • 动画更新 :驱动骨骼动画系统,确保角色动作流畅且与物理状态一致。
  • 网络同步 :将本地状态同步到服务器或其他客户端。
  • UI 更新 :刷新界面元素的状态(如血条、计时器等)。

由于这些任务之间存在复杂的依赖关系(例如,物理结算必须先于动画更新),如果 Tick 任务的调度不当,可能会导致数据竞争、状态不一致甚至崩溃。

另外,由于每帧的时间是有限的,尤其是在高帧率(如 60 FPS 或更高)的情况下,每一帧只有约 16 毫秒的时间来完成所有任务。如果 Tick 系统没有经过良好的设计和优化,可能会导致以下问题:

  • 性能瓶颈 :高频 Tick 任务会占用大量 CPU 时间,影响其他系统的运行效率。
  • 资源浪费 :某些对象可能并不需要每帧都更新(例如远处的静态物体),但仍然被频繁调用。

同时现代游戏引擎通常需要处理大量的异步任务如数据加载与解码为了支持这些异步任务Tick 系统需要提供专门的机制,确保它们能够与其他任务正确协作。

综上所述Tick 调度系统不仅是一个简单的“每帧调用”机制,更是游戏引擎运行时的核心支柱。它承担着以下几个重要职责:

  1. 确保任务按正确的顺序执行 :通过分组和依赖管理,避免逻辑冲突和状态不一致。
  2. 优化性能 :通过按需更新、多线程支持和细粒度控制,最大限度地提高运行效率。
  3. 支持复杂场景 :通过异步任务和动态注册机制,适应现代游戏开发中的多样化需求。

正因为 Tick 调度如此重要UE 提供了一个专门的组件——FTickTaskManager,来管理和调度所有的 Tick 任务。它并非一个简单的功能优化,而是维系整个游戏世界稳定、高效、可预测运行的核心基石

Tick 任务调度核心与挑战

Tick 任务调度核心与挑战

1.2 FTickTaskManager 的角色与定位

FTickTaskManager 并非一个孤立的系统,而是紧密集成在 UWorld 的 Tick 流程中,扮演着一个至关重要的角色。

大致可以从以下三个层面来精确定位 FTickTaskManager 的角色:

  1. UWorld 的中央调度核心UWorld::Tick 函数本身并不直接执行任何 Actor 或 Component 的 Tick 逻辑。它将这一复杂任务完全委托给了 FTickTaskManager。在每一帧UWorld 会首先命令 FTickTaskManager 进行准备 (StartFrame),然后根据预设的 Tick Group 顺序,多次调用 FTickTaskManagerRunTickGroup 来执行相应阶段的任务,最后再通知其进行清理 (EndFrame)。可以说FTickTaskManager 是 UWorld::Tick 的“大脑”和“执行引擎”,负责将 UWorld 的更新意图转化为具体的、有序的任务执行。
  2. 依赖关系解析器FTickTaskManager 的核心能力之一,是理解并处理 FTickFunction 之间通过 AddPrerequisite 建立的依赖关系。在 StartFrame 阶段,它会收集所有需要更新的 Tick 任务,并分析它们之间的前后置依赖,最终构建出一个有向无环图。这个依赖图精确地描述了本帧所有任务的执行约束,是保证逻辑正确性的基础。
  3. 并发任务分发器FTickTaskManager 自身不直接执行并行计算,但它扮演着连接上层游戏逻辑与底层 Task Graph 系统的关键桥梁。在 RunTickGroup 阶段,它会将依赖图中那些没有前置约束、可以并行执行的任务,打包成 FGraphEvent 并提交给 Task Graph 系统。由 Task Graph 系统负责将这些任务高效地分配到不同的 CPU 工作线程上。因此,FTickTaskManager 的角色更像一个“计划者”和“分包商”,它制定了详细的施工计划(依赖图),然后将具体的施工任务(可并行的 Tick分发给专业的施工队Task Graph

总结而言,FTickTaskManager 的定位是一个位于 UWorld 内部负责将海量、无序、且相互依赖的 Tick 任务,转化为一个结构化、有序、且部分可并行的执行计划,并将其提交给底层系统来完成的高级调度器。 它是 UE 解决 Tick 任务调度三大难题(顺序、依赖、性能)的核心技术实现。

FTickTaskManager 分层架构

FTickTaskManager 分层架构

二、源码剖析

2.1 核心数据结构

2.1.1 FTickFunction

源码文件Source/Runtime/Engine/Classes/Engine/EngineBaseTypes.h

FTickFunction 是一个 USTRUCT,这意味着它可以被 UE 的反射系统识别。它本身不是一个可执行的函数,而是一个描述 Tick 任务所有属性和状态的“数据包”或“配置文件”。当一个 Actor 或 Component 需要逐帧更新时,它内部就会包含一个 FTickFunction 的派生实例,并将其注册到 FTickTaskManager 中。


  1. 公开配置属性

这部分成员变量主要用于配置一个 Tick 任务的基本行为。

// 定义了这个Tick任务所属的Tick组决定了其大致的执行阶段
UPROPERTY(...)
TEnumAsByte<enum ETickingGroup> TickGroup;

// 定义了这个Tick任务必须在此Tick组之前完成
UPROPERTY(...)
TEnumAsByte<enum ETickingGroup> EndTickGroup;

// 该任务是否在游戏暂停时也执行
UPROPERTY(...)
uint8 bTickEvenWhenPaused:1;

// 【关键】这个Tick功能是否在代码层面被永久启用。如果为false它永远不会被注册。
UPROPERTY()
uint8 bCanEverTick:1;

// 如果为true该任务在创建时默认是启用的但后续可以动态关闭。
UPROPERTY(...)
uint8 bStartWithTickEnabled:1;

// 是否允许在专用服务器上运行
UPROPERTY(...)
uint8 bAllowTickOnDedicatedServer:1;

// Tick的执行频率。如果大于0引擎会尝试按这个间隔来调用而不是每帧都调。
UPROPERTY(...)
float TickInterval;

分析

这部分定义了 Tick 任务的静态行为蓝图bCanEverTick 是最重要的一个开关,它在编译时就决定了这个 Tick 功能是否存在。TickGroup 和 EndTickGroup 则是向 FTickTaskManager 声明其执行顺序的“意图”。开发者通过调整这些参数,可以精细地控制一个 Tick 任务的基础属性。


  1. 内部状态与控制标志

这部分主要是一些非 UPROPERTY 的 bool 标志,用于更精细地控制 Tick 的行为。

// 是否允许将多个类似的Tick任务打包在一起执行以提升性能
uint8 bAllowTickBatching:1;

// 是否在本Tick组内优先执行用于需要尽早开始的异步任务
uint8 bHighPriority:1;

// 【关键】如果为true该任务可以在任何工作线程上并行执行否则只能在游戏主线程。
uint8 bRunOnAnyThread:1;

// ... 其他一些实验性或特殊用途的标志 ...

// Tick任务的当前状态启用、禁用、冷却中
ETickState TickState : 2;

分析:

bRunOnAnyThread 是实现并行优化的关键。如果一个 Tick 任务的逻辑不依赖于任何游戏线程特有的数据(比如 UI、某些 UObject 操作就可以将此标志设为 trueFTickTaskManager 就会放心地将它交给任意一个空闲的 CPU 核心去执行,从而与游戏主线程并行工作,缩短帧时间。


  1. 核心功能性成员

这部分是 FTickFunction 实现其调度能力的核心数据。

// 【关键】存储该Tick任务的所有前置依赖项。
// FTickTaskManager在构建任务图时会读取这个数组来确定任务间的先后关系。
TArray<struct FTickPrerequisite> Prerequisites;

// 【关键】一个懒加载的、包含所有运行时状态的内部数据结构。
// 只有当Tick函数被注册后这个指针才会被分配内存。
// 这样做可以节省未注册的Tick函数的内存开销。
TUniquePtr<FInternalData> InternalData;

分析:

Prerequisites 是依赖系统的物理实现。任何通过 AddPrerequisite() 添加的依赖,最终都会被存储在这个数组中。InternalData 的设计则体现了性能优化的思想。对于成千上万个可能永远不会Tick的组件引擎不会为它们分配额外的运行时数据只有当 RegisterTickFunction 被调用时,才会按需创建 FInternalData,这是一种非常高效的内存管理策略。

FInternalData 内部包含了诸如 TaskPointer指向底层T ask Graph 任务的指针)、ActualStartTickGroup(因依赖延迟后,实际开始执行的 Tick 组)等运行时动态变化的状态。将这些动态状态与静态配置分离,使得整个结构更加清晰。


  1. 核心功能性函数

这部分是 FTickFunction 对外暴露的主要API以及其虚函数。

// 注册/注销Tick函数到FTickTaskManager
ENGINE_API void RegisterTickFunction(class ULevel* Level);
ENGINE_API void UnRegisterTickFunction();

// 动态启用/禁用Tick
ENGINE_API void SetTickFunctionEnable(bool bInEnabled);

// 【关键】添加/移除依赖项
ENGINE_API void AddPrerequisite(UObject* TargetObject, struct FTickFunction& TargetTickFunction);
ENGINE_API void RemovePrerequisite(UObject* TargetObject, struct FTickFunction& TargetTickFunction);

// 【关键】纯虚函数必须由派生类实现。这里是真正执行Tick逻辑的地方。
ENGINE_API virtual void ExecuteTick(...) PURE_VIRTUAL(,);

// 【关键】纯虚函数,用于在产生循环依赖等错误时,提供可读的诊断信息。
ENGINE_API virtual FString DiagnosticMessage() PURE_VIRTUAL(, ...);

分析:

RegisterTickFunction 和 UnRegisterTickFunction 是 Tick 任务生命周期的起点和终点。AddPrerequisite 是构建复杂依赖关系网的API入口。

最重要的两个纯虚函数PURE_VIRTUAL ExecuteTick 和 DiagnosticMessage,揭示了 FTickFunction 的设计模式:它是一个抽象基类,定义了一个契约。任何想要成为可Tick任务的类FActorTickFunction),都必须实现这两个函数。ExecuteTick 负责“做什么”,DiagnosticMessage 负责“我是谁”。FTickTaskManager 在调度时并不关心具体是谁在Tick它只知道调用 ExecuteTick 这个标准接口即可,这是一种典型的多态应用。


总结

FTickFunction 是一个设计精巧、高度工程化的数据结构。它完美地体现了以下设计思想:

  • 配置与状态分离: 将静态配置UPROPERTY与运行时动态数据InternalData分离。
  • 懒加载: 通过 TUniquePtr 按需分配 InternalData,优化内存使用。
  • 抽象与多态: 通过纯虚函数定义了一个统一的执行契约,让上层调度代码可以与具体实现解耦。
  • 数据驱动: 整个结构就是一个数据容器,其行为完全由内部的成员变量配置来驱动。

FTickFunction 结构类图

FTickFunction 结构类图

2.1.2 FActorTickFunction & FActorComponentTickFunction

源码文件Source/Runtime/Engine/Classes/Engine/EngineBaseTypes.h

/** 
* Tick function that calls AActor::TickActor
**/
USTRUCT()
struct FActorTickFunction : public FTickFunction
{
	GENERATED_USTRUCT_BODY()

	/**  AActor  that is the target of this tick **/
#if UE_WITH_REMOTE_OBJECT_HANDLE
	TObjectPtr<AActor> Target;
#else
	class AActor*	Target;
#endif

	/** 
		* Abstract function actually execute the tick. 
		* @param DeltaTime - frame time to advance, in seconds
		* @param TickType - kind of tick for this frame
		* @param CurrentThread - thread we are executing on, useful to pass along as new tasks are created
		* @param MyCompletionGraphEvent - completion event for this task. Useful for holding the completetion of this task until certain child tasks are complete.
	**/
	ENGINE_API virtual void ExecuteTick(float DeltaTime, ELevelTick TickType, ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent) override;
	/** Abstract function to describe this tick. Used to print messages about illegal cycles in the dependency graph **/
	ENGINE_API virtual FString DiagnosticMessage() override;
	ENGINE_API virtual FName DiagnosticContext(bool bDetailed) override;

#if UE_WITH_REMOTE_OBJECT_HANDLE
	/** Exposes references to GC system */
	ENGINE_API void AddStructReferencedObjects(FReferenceCollector& Collector);
#endif
};
/** 
* Tick function that calls UActorComponent::ConditionalTick
**/
USTRUCT()
struct FActorComponentTickFunction : public FTickFunction
{
	GENERATED_USTRUCT_BODY()

#if UE_WITH_REMOTE_OBJECT_HANDLE
	TObjectPtr<class UActorComponent> Target;
#else
	/**  AActor  component that is the target of this tick **/
	class UActorComponent*	Target;
#endif

	/** 
		* Abstract function actually execute the tick. 
		* @param DeltaTime - frame time to advance, in seconds
		* @param TickType - kind of tick for this frame
		* @param CurrentThread - thread we are executing on, useful to pass along as new tasks are created
		* @param MyCompletionGraphEvent - completion event for this task. Useful for holding the completetion of this task until certain child tasks are complete.
	**/
	ENGINE_API virtual void ExecuteTick(float DeltaTime, ELevelTick TickType, ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent) override;
	/** Abstract function to describe this tick. Used to print messages about illegal cycles in the dependency graph **/
	ENGINE_API virtual FString DiagnosticMessage() override;
	ENGINE_API virtual FName DiagnosticContext(bool bDetailed) override;

	/**
	 * Conditionally calls ExecuteTickFunc if registered and a bunch of other criteria are met
	 * @param Target - the actor component we are ticking
	 * @param bTickInEditor - whether the target wants to tick in the editor
	 * @param DeltaTime - The time since the last tick.
	 * @param TickType - Type of tick that we are running
	 * @param ExecuteTickFunc - the lambda that ultimately calls tick on the actor component
	 */

	//NOTE: This already creates a UObject stat so don't double count in your own functions

	template <typename ExecuteTickLambda>
	static void ExecuteTickHelper(UActorComponent* Target, bool bTickInEditor, float DeltaTime, ELevelTick TickType, const ExecuteTickLambda& ExecuteTickFunc);	

#if UE_WITH_REMOTE_OBJECT_HANDLE
	/** Exposes references to GC system */
	ENGINE_API void AddStructReferencedObjects(FReferenceCollector& Collector);
#endif
};

由上面的源码可知,FActorTickFunction & FActorComponentTickFunction 这两个结构体都直接继承自 FTickFunction。它们的核心作用是将抽象的 Tick 任务具体化,使其与一个特定的 AActorUActorComponent 实例绑定,并负责在 ExecuteTick 被调用时,真正地去执行那个实例的 TickTickComponent 方法。

在分别解析之前,我们先看它们完全相同的设计模式:

  • 共同的核心设计
    1. 继承 FTickFunction: 它们都继承了基类所有的配置属性(TickGroup等)、状态管理(InternalData和APIRegisterTickFunction等)。
    2. 实现纯虚函数: 它们都用 override 关键字实现了基类中定义的两个纯虚函数 ExecuteTickDiagnosticMessage,从而满足了基类定义的“契约”。
    3. 持有目标指针: 它们都包含一个名为 Target 的成员变量,用于指向它们所服务的具体 UObject 实例。

  1. struct FActorTickFunction 解析
  • Target 成员
    • 职责: 这个指针就是 FActorTickFunction 的“服务对象”。它明确地指出了这个 Tick 任务是属于哪一个 AActor 实例的。
    • 类型选择:
      • TObjectPtr<AActor>: 在较新的 UE 版本中,使用 TObjectPtr 是一种更现代、更安全的指针类型,它能被 GC 系统更好地追踪。
      • AActor*: 在旧版本或特定编译配置下,使用裸指针。
  • ExecuteTick() 实现
    • 职责: 这是连接调度与执行的最后一环。当 FTickTaskManager 调度到这个任务时,就会调用这个函数。
    • 内部实现(在 .cpp 文件中): 这个函数的实现非常直接,其核心逻辑为:检查 Target 指针是否有效,然后调用 AActor 类中真正的 TickActor 方法(TickActor 内部会再调用我们熟悉的 virtual void Tick(float DeltaTime))。
void FActorTickFunction::ExecuteTick(float DeltaTime, enum ELevelTick TickType, ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent)
{
	if (IsValid(Target))
	{
		if (TickType != LEVELTICK_ViewportsOnly || Target->ShouldTickIfViewportsOnly())
		{
			FScopeCycleCounterUObject ActorScope(Target);
			Target->TickActor(DeltaTime*Target->CustomTimeDilation, TickType, *this);
		}
	}
}
  • DiagnosticMessage() 实现
    • 职责:FTickTaskManager 检测到循环依赖等问题时,会调用这个函数来获取一段可读的错误信息,用于日志输出。
    • 内部实现: 它通常会返回类似 FString::Printf(TEXT("FActorTickFunction for %s"), *Target->GetFullName()) 的字符串,清晰地指明是哪个 Actor 的 Tick 出了问题。

  1. struct FActorComponentTickFunction 解析

它与 FActorTickFunction 的结构和原理几乎完全相同,只是服务对象不同。

  • Target 成员
    • 职责: 指向其所服务的具体 UActorComponent 实例。
  • ExecuteTick() 实现
    • 内部实现: 其核心逻辑与 FActorTickFunction 类似,但它最终调用的是 Target 组件的 TickComponent 方法:
void FActorComponentTickFunction::ExecuteTick(float DeltaTime, enum ELevelTick TickType, ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent)
{
	TRACE_CPUPROFILER_EVENT_SCOPE(FActorComponentTickFunction::ExecuteTick);

	ExecuteTickHelper(Target, Target->bTickInEditor, DeltaTime, TickType, [this, TickType](float DilatedTime)
	{
		Target->TickComponent(DilatedTime, TickType, this);
	});
}

总结

FActorTickFunctionFActorComponentTickFunctionFTickFunction 抽象概念的具体化实现。它们扮演着“适配器”Adapter的角色FTickTaskManager 的泛型调度指令,精准地“翻译”为对特定 AActorUActorComponent 实例的成员函数调用。

它们的设计体现了面向对象中多态继承的核心思想,使得上层调度系统无需关心 Tick 任务的具体类型,从而实现了高度的解耦和可扩展性。

2.1.3 FInternalData

源码文件Source/Runtime/Engine/Classes/Engine/EngineBaseTypes.h

FInternalData 是一个专门用于存储 已注册FTickFunction运行时动态变化的状态的内部数据结构。

它的存在本身就是一个性能优化设计FTickFunction 通过 TUniquePtr<FInternalData> InternalData; 来持有它。这意味着,只有当一个 Tick 任务通过 RegisterTickFunction() 被激活时,引擎才会为其分配 FInternalData 的内存。对于成千上万个可能永远不会被注册的 Tick 任务(例如,一个从未被使用的组件),引擎不会浪费任何内存来存储它们的运行时状态。这是一种典型的 懒加载 策略。


  1. 核心状态与注册信息
// 标志位表示该Tick函数是否已向FTickTaskManager注册。
bool bRegistered : 1;

// 内部状态,决定了 TaskPointer 指针的类型和含义。
// ETickTaskState 枚举的可能值NotQueued, Pending, HasTask, HasCompletionEvent
ETickTaskState TaskState;

// 指向其所属的 FTickTaskLevel 的反向指针。
// 这使得FTickTaskManager可以快速地从一个FTickFunction找到它所在的Level。
class FTickTaskLevel* TickTaskLevel;

分析:

bRegistered 是生命周期的起点。TaskState 是整个 Tick 调度过程中最重要的状态机,FTickTaskManager 通过改变这个状态来管理一个 Tick 任务从“待处理”到“已排队”再到“已完成”的整个流程。TickTaskLevel 则提供了上下文信息。


  1. 运行时 Tick Group 信息
// 由于依赖关系一个Tick任务实际开始执行的Tick组可能晚于其配置的TickGroup。
// 这个变量记录了它在本帧实际开始的Tick组。
TEnumAsByte<enum ETickingGroup> ActualStartTickGroup;

// 类似地记录了它在本帧保证会结束的Tick组。
TEnumAsByte<enum ETickingGroup> ActualEndTickGroup;

分析: 这组变量体现了依赖系统的动态性。一个配置为 TG_PrePhysics 的任务,如果它依赖的另一个任务直到 TG_EndPhysics 才完成,那么它自己的 ActualStartTickGroup 就会被动态调整为 TG_EndPhysics 之后的某个组。这是 FTickTaskManagerStartFrame 阶段进行依赖图解析后得出的动态调度计划


  1. 并发与任务图相关数据
// 【关键】一个void*指针,其具体指向的类型由 TaskState 决定。
// 它可能指向一个 FGraphEventRef代表一个独立的Task Graph任务// 其他内部任务结构。这是FTickFunction与底层Task Graph系统连接的纽带。
void* TaskPointer;

// 用于多线程同步的原子计数器记录该Tick任务在哪一帧被访问过以防止重复处理。
std::atomic<uint32> TickVisitedGFrameCounter;

// 记录该Tick任务在哪一帧被排入队列。
std::atomic<uint32> TickQueuedGFrameCounter;

分析:

TaskPointer 是与底层并行系统交互的核心。FTickTaskManager 将一个 FTickFunction 包装成一个 Task Graph 任务后会将任务的句柄Handle存储在这里。std::atomic 的使用则表明这些变量会在多线程环境下被访问,用于保证在并行调度时,每个任务在一帧内只被处理一次,避免了竞态条件。


  1. Tick 间隔与冷却管理

这部分成员专门用于实现 TickInterval 功能。

// 标志位缓存该函数是否因设置了TickInterval而被重新调度。
bool bWasInterval:1;

// 指向冷却列表中的下一个FTickFunction。
// FTickTaskManager内部维护了一个按冷却时间排序的链表来管理所有设置了TickInterval的任务。
FTickFunction* Next;

// 剩余的冷却时间(秒)。
// 它存储的是相对于链表中前一个元素的相对时间,这种设计可以高效地更新整个链表。
float RelativeTickCooldown;

// 上一次该函数被Tick时的游戏时间。
// 用于计算下一次应该在何时Tick。
float LastTickGameTimeSeconds;

分析: 这组数据揭示了 TickInterval 的实现原理。引擎并非为每个有间隔的 Tick 都创建一个独立的计时器,而是将它们组织成一个高效的排序链表(冷却列表)。每一帧,引擎只需要检查链表头部的任务,看看它的 RelativeTickCooldown 是否已经到期。这种设计避免了每帧遍历所有任务来检查时间,极大地提升了效率。


总结

FInternalData Mindmap

FInternalData Mindmap

FInternalData 是一个纯粹的运行时数据容器,它的设计充满了对性能和效率的考量:

  • 懒加载: 只有在需要时才分配内存。
  • 状态驱动: 通过 ETickTaskState 状态机来管理复杂的任务生命周期。
  • 动态调度: 通过 ActualStartTickGroup 等变量记录依赖解析后的动态结果。
  • 并发安全: 使用原子变量来确保多线程环境下的数据一致性。
  • 高效算法: 通过精巧的链表和相对时间来管理 Tick 间隔,避免了不必要的计算。

FTickFunction 主体定义了“我是谁以及我想做什么”,而 FInternalData 则记录了“我现在正在做什么以及我接下来要去哪”。两者结合,构成了一个完整的、功能强大的 Tick 任务单元。

2.1.4 FTickPrerequisite

源码文件Source/Runtime/Engine/Classes/Engine/EngineBaseTypes.h

FTickPrerequisite 是一个专门用于安全地存储和引用一个前置依赖 Tick 任务USTRUCT。当我们在 FTickFunction 中调用 AddPrerequisite 时,实际上就是在其内部的 Prerequisites 数组中添加了一个 FTickPrerequisite 的实例。FTickPrerequisite 这个结构体虽然小,但却是实现 FTickTaskManager 依赖系统的关键“连接件”。它的核心设计目标是解决一个问题:如何在保证内存安全的前提下,持有一个指向另一个 FTickFunction 的指针?


/**
 * This is small structure to hold prerequisite tick functions
 */
USTRUCT()
struct FTickPrerequisite
{
	GENERATED_USTRUCT_BODY()

	/** Tick functions live inside of UObjects, so we need a separate weak pointer to the UObject solely for the purpose of determining if PrerequisiteTickFunction is still valid. */
	TWeakObjectPtr<class UObject> PrerequisiteObject;

	/** Pointer to the actual tick function and must be completed prior to our tick running. */
	struct FTickFunction*		PrerequisiteTickFunction;

	/** Noop constructor. */
	FTickPrerequisite()
	: PrerequisiteTickFunction(nullptr)
	{
	}
	/** 
		* Constructor
		* @param TargetObject - UObject containing this tick function. Only used to verify that the other pointer is still usable
		* @param TargetTickFunction - Actual tick function to use as a prerequisite
	**/
	FTickPrerequisite(UObject* TargetObject, struct FTickFunction& TargetTickFunction)
	: PrerequisiteObject(TargetObject)
	, PrerequisiteTickFunction(&TargetTickFunction)
	{
		check(PrerequisiteTickFunction);
	}
	/** Equality operator, used to prevent duplicates and allow removal by value. */
	bool operator==(const FTickPrerequisite& Other) const
	{
		return PrerequisiteObject == Other.PrerequisiteObject &&
			PrerequisiteTickFunction == Other.PrerequisiteTickFunction;
	}
	/** Return the tick function, if it is still valid. Can be null if the tick function was null or the containing UObject has been garbage collected. */
	struct FTickFunction* Get()
	{
		if (PrerequisiteObject.IsValid(true))
		{
			return PrerequisiteTickFunction;
		}
		return nullptr;
	}
	
	const struct FTickFunction* Get() const
	{
		if (PrerequisiteObject.IsValid(true))
		{
			return PrerequisiteTickFunction;
		}
		return nullptr;
	}
};

  1. 核心成员变量解析

由上面的源码可知,这个结构体只有两个成员变量,但它们的设计配合得很巧妙。

// 用于验证 PrerequisiteTickFunction 指针是否仍然有效的弱引用。
TWeakObjectPtr<class UObject> PrerequisiteObject;

// 指向实际的前置依赖Tick函数的裸指针。
struct FTickFunction* PrerequisiteTickFunction;

分析:为什么定义两个指针?

  • FTickFunction 本身并不是一个 UObject,所以它不受 UE 的垃圾回收GC系统直接管理。它通常是作为 UObject(如 AActorUActorComponent)的成员变量存在的。
  • 如果直接持有一个 FTickFunction* 裸指针,我们无法知道它所依附的 UObject 是否已经被GC销毁。如果其宿主UObject被销毁,这个裸指针就会变成一个危险的悬空指针
  • TWeakObjectPtr 的关键作用:TWeakObjectPtr(弱对象指针)是 UE 提供的一种智能指针,它不会阻止其指向的 UObject 被 GC 回收。它的核心能力是提供了一个 IsValid() 方法。在访问裸指针之前,我们可以通过检查 PrerequisiteObject.IsValid() 来安全地判断那个 UObject 是否还存活。如果 IsValid() 返回 false,就意味着宿主对象已经被销毁,那么 PrerequisiteTickFunction 这个裸指针也必然失效了。
  • 协同工作机制:
    • PrerequisiteObject 扮演着“哨兵”的角色。
    • PrerequisiteTickFunction 存储着实际的目标地址。
    • 只有在“哨兵”确认安全(IsValid())之后,我们才会去使用那个地址。

FTickPrerequisite 安全访问机制

FTickPrerequisite 安全访问机制


  1. 核心功能性函数解析
  • 构造函数
FTickPrerequisite(UObject* TargetObject, struct FTickFunction& TargetTickFunction)
: PrerequisiteObject(TargetObject)
, PrerequisiteTickFunction(&TargetTickFunction)
{
    check(PrerequisiteTickFunction);
}

分析:

构造函数非常直白它同时接收宿主 UObject 的指针和 FTickFunction 的引用并将它们分别存入两个成员变量中。这确保了每次创建一个依赖关系时安全验证所需的信息都被完整地记录下来。


  • Get() 方法
struct FTickFunction* Get()
{
    if (PrerequisiteObject.IsValid(true))
    {
        return PrerequisiteTickFunction;
    }
    return nullptr;
}

分析:

这是该结构体最核心的对外接口。它实践了我们上述提到的“先检查,后访问”的安全模式。

  • 首先调用 PrerequisiteObject.IsValid(true)。参数 true 表示即使对象正在等待被清理Pending Kill也将其视为无效。
  • 只有在弱指针验证通过确认宿主 UObject 仍然存活的情况下它才会返回那个可能有效的 PrerequisiteTickFunction 裸指针。
  • 如果宿主对象已失效它会安全地返回 nullptr避免了任何访问悬空指针的风险。

FTickTaskManager 在构建依赖图时,就会通过调用这个 Get() 方法来安全地获取每一个依赖项。


总结

FTickPrerequisite 虽然定义简单但却是一个展示如何在非 UObject 上下文中安全引用 UObject 成员的优秀范例。它通过将一个裸指针与一个用于生命周期验证的弱指针巧妙地捆绑在一起,构建了一个既高效(直接访问裸指针)又安全(通过 TWeakObjectPtr 验证)的依赖引用机制。


2.1.5 ETickingGroup

源码文件Source/Runtime/Engine/Classes/Engine/EngineBaseTypes.h

ETickingGroup 是一个枚举类型,它定义了一系列离散的、有序的**“时间阶段”。FTickTaskManager 在一帧内会严格按照这个枚举定义的顺序一个接一个地执行属于每个阶段Group的 Tick 任务。**这套机制是 UE 用来解决 Tick 任务执行顺序和依赖关系问题的基石。

接下来,我将按照它们的实际执行顺序来逐一分析每个 Tick Group 的职责和设计目的。

(下面分析中提到的 ”注释描述“ 部分指的是源码中对于当前枚举值的注释。)


  1. TG_PrePhysics - 物理模拟前
  • 注释描述: Any item that needs to be executed before physics simulation starts. (任何需要在物理模拟开始前执行的项)
  • 职责: 这是绝大多数游戏逻辑的默认执行阶段。所有决定物体“本帧想要如何运动”的逻辑都应该放在这里。
  • 典型用例:
    • 玩家输入处理: 根据玩家的键盘/手柄输入,计算角色的移动向量。
    • AI 决策: AI Controller 在此决定本帧的移动目标或攻击行为。
    • 动画蓝图更新(非物理部分): 更新状态机,决定要播放哪个动画序列。
  • 设计原因: 物理引擎需要知道物体的“意图”(如目标速度、施加的力)才能开始计算。此阶段就是为了准备好所有这些输入数据。

  1. TG_StartPhysics - 物理模拟开始
  • 注释描述: Special tick group that starts physics simulation. (启动物理模拟的特殊Tick组)
  • 元数据: UMETA(Hidden) - 表示这个选项通常不在编辑器中对用户暴露。
  • 职责: 一个内部阶段,用于触发物理引擎(如 Chaos开始本帧的模拟计算。它更像是一个命令信号而不是一个让用户注册任务的阶段。

  1. TG_DuringPhysics - 物理模拟中
  • 注释描述: Any item that can be run in parallel with our physics simulation work. (任何可以与物理模拟并行运行的项)
  • 职责: 提供一个时间窗口,用于执行那些不依赖于本帧物理计算最终结果的、且可以并行化的任务。
  • 典型用例:
    • 异步场景查询: 发起一些耗时的射线检测或形状检测,而不阻塞主线程。
    • 一些独立的视觉效果或系统更新。
  • 设计原因: 物理模拟是CPU密集型任务通常在独立的线程上异步进行。TG_DuringPhysics 允许游戏主线程在等待物理计算的同时去“插空”执行其他不相关的任务从而提高CPU的利用率和整体性能。

  1. TG_EndPhysics - 物理模拟结束
  • 注释描述: Special tick group that ends physics simulation. (结束物理模拟的特殊Tick组)
  • 元数据: UMETA(Hidden)
  • 职责: 这是一个关键的同步点。游戏主线程会在此等待,直到异步的物理模拟线程完成其所有计算。完成后,物理引擎会将计算出的最终结果(如新的位置、旋转、碰撞事件)写回到游戏世界中的对象上。

  1. TG_PostPhysics - 物理模拟后
  • 注释描述: Any item that needs rigid body and cloth simulation to be complete before being executed. (任何需要刚体和布料模拟完成才能执行的项)
  • 职责: 执行所有依赖于本帧物理结算结果的逻辑。
  • 典型用例:
    • 骨骼动画更新: 这是骨骼网格体组件的默认 Tick Group。它需要根据角色经过物理计算后的最终位置和速度来调整动画例如使用 IK 将脚踩在正确的地面上,或根据速度混合不同的跑动动画。
    • 相机更新: 相机需要跟随已经移动到最终位置的角色。
    • 碰撞事件响应: 在这里处理 OnComponentHit 等事件是安全的,因为所有的碰撞信息此时都已生成。
  • 设计原因: 这是为了保证**“表现”基于“事实”**。物理结果就是本帧的“事实”,而动画、相机等视觉表现必须基于这个事实来进行调整,否则就会出现“脚滑”、穿模等视觉错误。

  1. TG_PostUpdateWork - 更新工作后
  • 注释描述: Any item that needs the update work to be done before being ticked. (任何需要在更新工作完成后被Tick的项)
  • 职责: 在所有主要的游戏逻辑和视觉表现更新都完成后,执行一些收尾的更新任务。
  • 典型用例:
    • 布料模拟: 布料通常需要附着在已经更新完动画的角色骨骼上,所以放在这里执行。
    • 一些最终的清理或同步逻辑。

  1. TG_LastDemotable - 可降级的最后任务
  • 注释描述: Catchall for anything demoted to the end. (一个用于接收所有被降级到末尾任务的“捕集器”)
  • 元数据: UMETA(Hidden)
  • 职责: 优先级最低的阶段。用于执行那些即使在一帧内被跳过也不会造成严重问题的任务。

  1. TG_NewlySpawned - 特殊:新生成对象
  • 注释描述: ...After every tick group this is repeatedly re-run until there are no more newly spawned items to run. (在每个Tick组之后这个组会重复运行直到没有新生成的对象需要运行)
  • 职责: 这是一个非常特殊的“伪”组。它的目的是处理那些在本帧的Tick过程中被动态生成Spawn出来的新Actor
  • 工作机制: 假设在 TG_PrePhysics 阶段一个Actor A 的 Tick 函数 Spawn 了一个新的 Actor B。Actor B 错过了本帧的 TG_PrePhysics 调度。为了让 B 也能在当前帧就被更新(而不是等到下一帧),FTickTaskManagerTG_PrePhysics 执行完后,会立即检查并执行所有新生成的、属于TG_PrePhysics 的对象的Tick。这个过程会在每个主Tick组后都重复一次。
  • 设计原因: 解决了“帧内生成,帧内更新”的问题,使得动态生成的对象能够更及时地融入游戏世界,避免了一帧的延迟。

总结

ETickingGroup 是 UE 对一帧游戏时间的**“时间切片”**。通过将 Tick 任务强制归入这些有序的“切片”中FTickTaskManager 实现了对整个世界模拟流程的精确控制从根本上保证了复杂系统中数据流的正确性和逻辑的稳定性。这是 UE 高性能和高保真模拟能力的架构基石。

ETickingGroup 流程图

ETickingGroup 流程图

2.2 核心类

2.2.1 FTickTaskManagerInterface

源码文件Source\\Runtime\\Engine\\Public\\TickTaskManagerInterface.h

/** 
 * Interface for the tick task manager
 **/
class FTickTaskManagerInterface
{
public:
	virtual ~FTickTaskManagerInterface()
	{
	}

	/** Allocate a new ticking structure for a ULevel **/
	virtual FTickTaskLevel* AllocateTickTaskLevel() = 0;

	/** Free a ticking structure for a ULevel **/
	virtual void FreeTickTaskLevel(FTickTaskLevel* TickTaskLevel) = 0;

	/**
	 * Queue all of the ticks for a frame
	 *
	 * @param World	- World currently ticking
	 * @param DeltaSeconds - time in seconds since last tick
	 * @param TickType - type of tick (viewports only, time only, etc)
	 */
	virtual void StartFrame(UWorld* InWorld, float DeltaSeconds, ELevelTick TickType, const TArray<ULevel*>& LevelsToTick) = 0;

	/**
	 * Run all of the ticks for a pause frame synchronously on the game thread.
	 * The capability of pause ticks are very limited. There are no dependencies or ordering or tick groups.
	 * @param World	- World currently ticking
	 * @param DeltaSeconds - time in seconds since last tick
	 * @param TickType - type of tick (viewports only, time only, etc)
	 */
	virtual void RunPauseFrame(UWorld* InWorld, float DeltaSeconds, ELevelTick TickType, const TArray<ULevel*>& LevelsToTick) = 0;

	/**
		* Run a tick group, ticking all actors and components
		* @param Group - Ticking group to run
		* @param bBlockTillComplete - if true, do not return until all ticks are complete
	*/
	virtual void RunTickGroup(ETickingGroup Group, bool bBlockTillComplete ) = 0;

	/** Finish a frame of ticks **/
	virtual void EndFrame() = 0;

	/** Dumps all registered tick functions to output device. */
	virtual void DumpAllTickFunctions(FOutputDevice& Ar, UWorld* InWorld, bool bEnabled, bool bDisabled, bool bGrouped) = 0;

	/** Returns a map of enabled ticks, grouped by 'diagnostic context' string, along with count of enabled ticks */
	virtual void GetEnabledTickFunctionCounts(UWorld* InWorld, TSortedMap<FName, int32, FDefaultAllocator, FNameFastLess>& TickContextToCountMap, int32& EnabledCount, bool bDetailed, bool bFilterCoolingDown=false) = 0;

	/**
	 * Singleton to retrieve the GLOBAL tick task manager
	 *
	 * @return Reference to the global cache tick task manager
	 */
	static ENGINE_API FTickTaskManagerInterface& Get();

};

由以上源码可知,FTickTaskManagerInterface 是一个纯抽象基类。它定义了全局唯一的 Tick 任务管理器(FTickTaskManager)所必须提供的一系列公共服务和功能。

它的所有核心方法都被声明为纯虚函数 (= 0),这意味着这个接口本身不能被实例化,任何想要成为一个有效的 Tick Task Manager 的类,都必须继承自这个接口并实现其所有纯虚函数。在UE中这个唯一的实现者就是我们后面会分析的 class FTickTaskManager

这种设计是接口与实现分离设计模式的典型应用,它允许引擎的其他部分(主要是UWorld)依赖于一个稳定的、抽象的接口,而无需关心其底层的具体实现可能会如何变化。

我们可以将 FTickTaskManagerInterface 中的接口函数按照其在 Tick 生命周期中的作用进行分组,下面我们分组进行分析。


  1. 生命周期管理函数

这组函数构成了 FTickTaskManager 在一帧内的主要工作流程,由 UWorld::Tick 严格按顺序调用。

// 在一帧的Tick开始时被调用用于收集和规划所有任务。
virtual void StartFrame(...) = 0;

// 在一帧的Tick结束时被调用用于清理和重置状态。
virtual void EndFrame() = 0;

// 【关键】执行一个指定Tick Group中的所有任务。
// UWorld::Tick 会为每个ETickingGroup如TG_PrePhysics调用一次这个函数。
virtual void RunTickGroup(ETickingGroup Group, bool bBlockTillComplete) = 0;

分析:

StartFrame, RunTickGroup, EndFrame 共同定义了 Tick 管理器的三段式工作模型。这是 UWorldFTickTaskManager 交互的主干道。RunTickGroup 中的 bBlockTillComplete 参数更是暴露了其支持同步/异步执行模式的能力,这是实现高性能并发调度的关键接口。


  1. 特殊帧处理函数
// 在游戏暂停时执行一个简化的、同步的Tick流程。
// 暂停时的Tick功能受限没有依赖、排序或分组。
virtual void RunPauseFrame(...) = 0;

分析: 这个函数提供了一种处理“游戏暂停”这一特殊状态的专用路径。它将暂停时的 Tick 逻辑与正常的游戏 Tick 逻辑分离开来,使得两种情况的处理都更加清晰。


  1. Tick 任务结构管理函数

这组函数负责管理与 ULevel 绑定的底层数据结构。

// 为一个ULevel分配一个新的、用于存储其Tick任务的数据结构。
virtual FTickTaskLevel* AllocateTickTaskLevel() = 0;

// 释放一个ULevel不再使用的Tick任务数据结构。
virtual void FreeTickTaskLevel(FTickTaskLevel* TickTaskLevel) = 0;

分析: 这两个函数揭示了 FTickTaskManager 内部是以 Level 为单位来组织 Tick 任务的。当一个ULevel被加载到世界中时,UWorld会调用AllocateTickTaskLevel来为它创建一个“账本”(FTickTaskLevel),用于记录该关卡内所有 Actor 和 Component 的 Tick 任务。当 Level 被卸载时,则调用FreeTickTaskLevel来销毁这个“账本”。这与 UE 的关卡流送Level Streaming机制紧密相关。


  1. 调试与分析函数

这组函数主要用于开发和调试,提供了查询和输出 Tick 系统内部状态的能力。

// 将所有已注册的Tick函数的信息启用/禁用、分组等)转储到输出设备(如日志文件)。
virtual void DumpAllTickFunctions(...) = 0;

// 获取当前已启用的Tick函数的统计信息按上下文通常是类名分组计数。
// 这是Unreal Insights等性能分析工具获取数据的重要来源。
virtual void GetEnabledTickFunctionCounts(...) = 0;

分析: 这些接口的存在表明,FTickTaskManager 在设计之初就充分考虑到了可观测性可调试性。开发者可以通过这些工具,清晰地看到当前有哪些 Tick 在运行,它们的分组是什么,以及各类 Tick 的数量,这对于性能优化和问题排查至关重要。


  1. 单例访问器
// 全局静态函数用于获取唯一的、全局的FTickTaskManagerInterface实例。
static ENGINE_API FTickTaskManagerInterface& Get();

分析: 这是典型的单例模式。整个引擎中只有一个 FTickTaskManager 实例,任何需要与它交互的代码(如UWorld, AActor等)都通过这个静态 Get() 方法来获取对它的引用。


总结

FTickTaskManagerInterface 如同一份清晰的“产品说明书”,它通过一系列纯虚函数,向整个引擎精确地定义了 Tick 调度系统所能提供的服务:

  • 核心服务: 提供了一套完整的 StartFrame -> RunTickGroup -> EndFrame 的生命周期管理。
  • 组织方式: 明确了其内部是以 Level 为单位来管理 Tick 任务的。
  • 特殊处理: 定义了处理暂停帧的专门逻辑。
  • 可观测性: 提供了强大的调试和性能分析接口。
  • 访问模式: 规定了其作为一个全局单例的存在形式。

通过分析这个接口,我们无需深入其复杂的实现细节,就能从一个很高的层面理解FTickTaskManager设计目标、功能边界和核心职责

FTickTaskManagerInterface 类图

FTickTaskManagerInterface 类图

2.2.2 FTickTaskManager

源码文件Source\\Runtime\\Engine\\Private\\TickTaskManager.cpp

FTickTaskManagerFTickTaskManagerInterface唯一实现。它是一个全局单例,负责接收 UWorld 的指令,并将海量的、无序的 FTickFunction 组织成一个有序的、部分可并行的执行计划,然后提交给底层的 Task Graph 系统去执行。


  1. 核心成员变量
// 对一个更底层的、专门负责任务排序和依赖解析的辅助类的引用。
FTickTaskSequencer& TickTaskSequencer;

// 本帧需要进行Tick的所有Level的FTickTaskLevel的列表。
TArray<FTickTaskLevel*> LevelList;

// 存储本帧Tick的上下文信息如World, DeltaSeconds, TickType等。
FTickContext Context;

// 一个关键的状态标志。当为true时表示正处于一帧的Tick流程中
// 此时任何新注册的Tick函数都需要被特殊处理加入到新生成队列bool bTickNewlySpawned;

分析:

TickTaskSequencer 是一个非常重要的内部辅助类,FTickTaskManager 将大量的复杂排序和依赖解析工作都委托给了它。LevelList 表明了其以Level为单位的管理模式。Context 作为一个上下文结构体,方便地将本帧的所有共享信息传递给各个子系统。bTickNewlySpawned 则是控制“帧内生成,帧内更新”机制的总开关。


  1. 生命周期函数

这是 UWorld::Tick 驱动 FTickTaskManager 工作的主流程。




  1. Tick 函数注册/注销接口
void AddTickFunction(ULevel* InLevel, FTickFunction* TickFunction)
{
    // ...
    FTickTaskLevel* Level = TickTaskLevelForLevel(InLevel); // 找到或创建Level对应的TickTaskLevel
    Level->AddTickFunction(TickFunction); // 在Level的“账本”中添加这个Tick函数
    // ...
}

void RemoveTickFunction(FTickFunction* TickFunction)
{
    // ...
    FTickTaskLevel* Level = TickFunction->InternalData->TickTaskLevel; // 从TickFunction自身找到它的“账本”
    Level->RemoveTickFunction(TickFunction); // 从“账本”中移除
}

分析:

AActorUActorComponent 在创建和销毁时调用的就是这两个函数。它们清晰地展示了 FTickTaskManager 是如何通过 FTickTaskLevel按 Level 组织所有 Tick 函数的。每个 FTickFunction 也通过其 InternalData 反向持有一个指向其所属 FTickTaskLevel 的指针,形成了一个双向链接,方便快速地添加和移除。


总结

FTickTaskManager 工作流程

FTickTaskManager 工作流程

FTickTaskManager 的实现是一个典型的分层委托架构:

  • 它作为高层管理者,定义了 StartFrame -> RunTickGroup -> EndFrame 的宏观流程。
  • 它将复杂的任务收集和排序逻辑,委托给了内部的 FTickTaskLevelFTickTaskSequencer
  • 它将最终的并行执行,委托给了 TickTaskSequencer 背后的 Task Graph 系统。

通过这种层层委托,FTickTaskManager 以一种相对清晰、高内聚的方式,精心安排了整个极其复杂的 Tick 调度过程。

2.2.3 FTickTaskSequencer

源码文件Source\\Runtime\\Engine\\Private\\TickTaskManager.cpp

FTickTaskSequencer 是一个全局单例,是FTickTaskManager核心工作委托对象。它的名字“Sequencer”序列器精准地描述了其核心职责将一堆无序的、带有依赖关系的 Tick 任务,组织成一个可以被Task Graph系统高效执行的任务序列


  1. 核心成员变量
// 存储用于批处理Batching的Tick任务信息
TArray< TPair<FTickGroupCondition, TUniquePtr<FTickBatchInfo> > > TickBatches;

// 【关键】为每个Tick组TG_MAX个存储一个FGraphEventRef数组。
// 当一个Tick任务被创建时它的完成事件Completion Event会被添加到这个数组中。
TArrayWithThreadsafeAdd<FGraphEventRef, TInlineAllocator<4> > TickCompletionEvents[TG_MAX];

// 【关键】一个二维数组用于存储所有Tick任务。
// 第一个维度是任务的开始Tick组第二个维度是结束Tick组。
// 分为高优先级和普通优先级。
TArrayWithThreadsafeAdd<FTickGraphTask*> HiPriTickTasks[TG_MAX][TG_MAX];
TArrayWithThreadsafeAdd<FTickGraphTask*> TickTasks[TG_MAX][TG_MAX];

// 用于存储需要在帧末尾完成的清理任务。
FGraphEventArray CleanupTasks;

// 记录上一个被阻塞等待的Tick组用于确定下一次需要等待哪些组。
ETickingGroup WaitForTickGroup;

// 一系列控制行为的布尔开关由CVar控制。
bool bAllowConcurrentTicks;     // 是否允许并行Tick
bool bAllowBatchedTicksForFrame; // 是否允许Tick批处理
// ...

分析:

  • TickCompletionEventsTickTasks / HiPriTickTasks 是这个类的核心数据容器
  • TickTasks[StartGroup][EndGroup] 这种二维数组的设计非常精巧。它使得Sequencer可以快速地根据开始组来分发任务,并根据结束组来收集它们的完成事件。
  • TArrayWithThreadsafeAdd 的使用,暗示了这些数组可以在多个线程中被安全地添加元素,这对于并行化的StartFrame流程至关重要。
  • TickBatches 则是一种性能优化,它尝试将属性相同(比如都在同一个 Tick 组、都没有依赖)的多个小 Tick 任务,打包成一个大的Task Graph任务来执行,以减少任务调度的开销。

  1. 核心函数

总结

FTickTaskSequencer 工作流程

FTickTaskSequencer 工作流程

FTickTaskSequencer 是一个高度优化的、与底层 Task Graph 系统紧密耦合的精密调度器。

  • 它使用二维数组来高效地组织和索引海量的 Tick 任务,实现了按“开始组”和“结束组”的快速访问。
  • 它的 Queue 系列函数负责将上层的 FTickFunction 翻译成底层的 Task Graph 任务。
  • 它的 ReleaseTickGroup 函数通过“分发”Dispatch和“等待”Wait两个步骤精确地控制了任务的并发执行和必要的同步

可以说,FTickTaskManager 是“战略层”,而 FTickTaskSequencer 则是“战术层”。它处理了所有最棘手的并发、同步和性能优化细节,是 UE Tick 机制能够高性能运行的真正幕后功臣。

2.2.4 FTickTaskLevel

源码文件Source\\Runtime\\Engine\\Private\\TickTaskManager.cpp

FTickTaskLevel 可以被理解为 一个 ULevel 中所有 FTickFunction 的“账本”或“花名册”。它的核心职责是管理隶属于单个 ULevel 的所有 Tick 任务,并对它们进行分类、预处理,为上层的 FTickTaskManager 提供干净、有序的数据。

每一个ULevel对象内部都有一个指向FTickTaskLevel实例的指针(Level->TickTaskLevel),这构成了FTickTaskManager按 Level 组织 Tick 任务的基础。


  1. 核心成员变量

这是 FTickTaskLevel 用来分类和管理 FTickFunction 的内部容器。

// 对全局Sequencer的引用方便直接调用
FTickTaskSequencer& TickTaskSequencer;

// 【关键】存储所有明确启用的、需要每帧Tick的函数。TSet保证了不重复。
TSet<FTickFunction*> AllEnabledTickFunctions;

// 【关键】一个自定义的链表用于存储所有设置了TickInterval且当前处于“冷却”状态的函数。
FCoolingDownTickFunctionList AllCoolingDownTickFunctions;

// 存储所有明确禁用的Tick函数。
TSet<FTickFunction*> AllDisabledTickFunctions;

// 临时数组用于存放本帧结束后需要重新计算冷却时间的Tick函数。
TArrayWithThreadsafeAdd<FTickScheduleDetails> TickFunctionsToReschedule;

// 存储在本帧Tick期间新生成的Tick函数。
TSet<FTickFunction*> NewlySpawnedTickFunctions;

// 当前帧的Tick上下文
FTickContext Context;

// 是否处于Tick流程中的标志
bool bTickNewlySpawned;

分析:

  • FTickTaskLevel 的核心就是这四大容器AllEnabledTickFunctions, AllCoolingDownTickFunctions, AllDisabledTickFunctions, 和 NewlySpawnedTickFunctions。它将一个 Level 内的所有 Tick 函数分门别类地进行管理。
  • AllCoolingDownTickFunctions 的链表结构设计非常巧妙。因为它需要处理基于DeltaTime的动态激活,链表结构比数组或集合更适合进行高效的遍历和中间节点的移除。
  • TickFunctionsToRescheduleNewlySpawnedTickFunctions临时性的容器,用于处理帧内的状态变化,体现了其逻辑的严谨性。

  1. 核心函数

总结

FTickTaskLevel 数据组织与工作流

FTickTaskLevel 数据组织与工作流

FTickTaskLevel 是一个面向单个ULevel的、高度特化的 Tick 任务管理器。它的存在体现了 UE 设计的分而治之思想。

  • 数据组织者: 它将一个ULevel中成百上千的 Tick 函数,通过TSet和自定义链表,分门别类地管理起来,极大地降低了上层FTickTaskManager的管理复杂度。
  • 预处理器: 它在StartFrame阶段就独立处理完了复杂的TickInterval逻辑,为主调度器提供了干净、明确的“待办事项列表”。
  • 执行委托者: 它通过调用FTickFunction::QueueTickFunction,将最终的排队任务委托出去,自身不直接与FTickTaskSequencer深度耦合。

通过引入FTickTaskLevel这一中间层UE 的 Tick 系统架构变得更加清晰、模块化,并且能够与关卡流送系统完美协同工作。

2.3 调用上下文

2.3.1 UWorld::Tick

源码文件Source\\Runtime\\Engine\\Private\\LevelTick.cpp

UWorld::Tick 的源码虽然冗长,但我们只需要聚焦于它与 FTickTaskManager 直接交互的三个关键阶段,就能清晰地看到 FTickTaskManager 是如何被驱动的。


  1. 第一阶段:StartFrame - 规划与准备

UWorld::Tick 的核心循环中,当确定了本帧需要进行 Actor Tick (bDoingActorTickstrue) 之后,第一件重要的事情就是调用 FTickTaskManagerStartFrame 方法。

// in UWorld::Tick, inside the main for-loop over LevelCollections

if (bDoingActorTicks)
{
    // ...
    TickGroup = TG_PrePhysics; // 重置世界的当前Tick组

    // 【关键调用 1】通知TickTaskManager开始新的一帧
    FTickTaskManagerInterface::Get().StartFrame(this, DeltaSeconds, TickType, LevelsToTick);

    // ... 后续的RunTickGroup调用 ...
}

上下文剖析:

  1. 时机: 这个调用发生在所有 RunTickGroup 之前
  2. 传递的参数:
    • this (UWorld*): 告诉 FTickTaskManager 当前工作的世界是哪一个。
    • DeltaSeconds, TickType: 传递本帧的时间和Tick类型上下文。
    • LevelsToTick: 一个 TArray<ULevel*>,明确告知 FTickTaskManager 本帧只需要考虑这些 ULevel 中的 Tick 任务。这对于关卡流送至关重要,它避免了对已卸载或隐藏的 Level 进行不必要的工作。
  3. 作用: UWorld 在这里扮演了**“信息提供者”“启动者”**的角色。它将所有必要的上下文信息打包好,然后按下 FTickTaskManager 的“启动按钮”。StartFrame 接收到指令后会执行我们之前分析过的内部逻辑收集所有相关Level的Tick函数并构建本帧的任务依赖图。

  1. 第二阶段:RunTickGroup - 按部就班地执行

StartFrame 完成规划后,UWorld::Tick 进入了一个高度结构化的执行阶段,它像一个严谨的工序流程单,依次调用 RunTickGroup

// in UWorld::Tick, right after StartFrame

// 【关键调用 2】按严格顺序执行每个Tick组
{ SCOPE_CYCLE_COUNTER(STAT_TG_PrePhysics); RunTickGroup(TG_PrePhysics); }
EnsureCollisionTreeIsBuilt();
{ SCOPE_CYCLE_COUNTER(STAT_TG_StartPhysics); RunTickGroup(TG_StartPhysics); }
{ SCOPE_CYCLE_COUNTER(STAT_TG_DuringPhysics); RunTickGroup(TG_DuringPhysics, false); }
// ...
{ SCOPE_CYCLE_COUNTER(STAT_TG_EndPhysics); RunTickGroup(TG_EndPhysics); }
{ SCOPE_CYCLE_COUNTER(STAT_TG_PostPhysics); RunTickGroup(TG_PostPhysics); }
// ...
{ SCOPE_CYCLE_COUNTER(STAT_TG_PostUpdateWork); RunTickGroup(TG_PostUpdateWork); }
{ SCOPE_CYCLE_COUNTER(STAT_TG_LastDemotable); RunTickGroup(TG_LastDemotable); }

上下文剖析:

  1. 严格的顺序: UWorld 硬编码RunTickGroup 的调用顺序,从 TG_PrePhysics 一直到 TG_LastDemotable。这从根本上保证了不同阶段任务的执行顺序,是整个 Tick 依赖系统能够正常工作的基础。
  2. 阻塞与非阻塞:
    • 大部分 RunTickGroup 调用(如 TG_PrePhysics, TG_EndPhysics)都使用了默认的 bBlockTillComplete = true 参数。这意味着 UWorld 的执行流会在这里暂停,直到该 Tick 组的所有任务完成。这是实现同步点的关键。
    • RunTickGroup(TG_DuringPhysics, false) 是一个特例。false 参数告诉 UWorld 不要等待物理模拟完成。这使得游戏主线程可以在物理计算在后台线程进行的同时,继续执行其他工作,从而实现了并发,提升了性能。
  3. 作用: UWorld 在这里扮演了**“工序调度员”**的角色。它不关心每个TickGroup内部具体有哪些任务,只负责按照预设的蓝图,一步步地命令 FTickTaskManager去执行每一个工序。

  1. 第三阶段:EndFrame - 清理与收尾

在所有 RunTickGroup 都执行完毕后,UWorld::Tick 会调用 FTickTaskManagerEndFrame 来结束本帧的 Tick 调度。

// in UWorld::Tick, after all RunTickGroup calls

if (bDoingActorTicks)
{
    // ...
    // 【关键调用 3】通知TickTaskManager本帧的Tick调度工作全部结束
    FTickTaskManagerInterface::Get().EndFrame();
}

上下文剖析:

  1. 时机: 这个调用发生在所有 RunTickGroup 之后
  2. 作用: UWorld 在这里发出一个**“收工”**的信号。FTickTaskManager 接收到后,会执行其内部的清理逻辑,如重置状态、清空临时容器等,为下一帧做好准备。

总结

UWorld::Tick 与 FTickTaskManager 交互时序图

UWorld::Tick 与 FTickTaskManager 交互时序图

UWorld::Tick 这个调用上下文的视角来看,FTickTaskManager 并非一个自我驱动的系统,而是一个被动响应的、服务于 UWorld 的高级工具

  • UWorld 负责定义“做什么”和“按什么顺序做”(提供 LevelsToTick按序调用 RunTickGroup
  • FTickTaskManager 负责解决“如何高效、正确地做”(收集任务、解析依赖、并行执行)。

这种清晰的职责分离是 UE 架构设计的精髓。UWorld 掌握着宏观的、游戏逻辑驱动的流程控制,而 FTickTaskManager 则封装了所有复杂的、与具体游戏逻辑无关的底层调度技术。

2.3.2 AActor

源码文件Source\\Runtime\\Engine\\Private\\Actor.cpp

AActor 是整个 Tick 调度系统最主要的服务对象。引擎为AActor提供了一套封装良好、易于使用的 API让游戏开发者可以在不了解FTickTaskManager复杂实现的情况下,方便地控制 Actor 的 Tick 行为。


  1. 注册与注销:RegisterAllActorTickFunctions
    • 函数: void AActor::RegisterAllActorTickFunctions(bool bRegister, bool bDoComponents)

    • 调用时机:

      • bRegister = true: 当Actor被创建并添加到世界时如通过 SpawnActor 或关卡加载)。
      • bRegister = false: 当Actor被销毁时。
    • 源码剖析:

      void AActor::RegisterAllActorTickFunctions(bool bRegister, bool bDoComponents)
      {
          // ...
          // 防止重复注册/注销
          if (bTickFunctionsRegistered != bRegister)
          {
              // 【关键】调用RegisterActorTickFunctions来处理自身的PrimaryActorTick
              RegisterActorTickFunctions(bRegister);
              bTickFunctionsRegistered = bRegister;
              // ...
          }
      
          if (bDoComponents)
          {
              // 递归地为所有子组件也执行注册/注销
              for (UActorComponent* Component : GetComponents())
              {
                  if (Component)
                  {
                      Component->RegisterAllComponentTickFunctions(bRegister);
                  }
              }
          }
          // ...
      }
      
      void AActor::RegisterActorTickFunctions(bool bRegister)
      {
          if(bRegister)
          {
              if(PrimaryActorTick.bCanEverTick)
              {
                  // 1. 设置Target让FActorTickFunction知道要Tick哪个Actor
                  PrimaryActorTick.Target = this;
                  // 2. 设置初始启用状态
                  PrimaryActorTick.SetTickFunctionEnable(...);
                  // 3. 【核心调用】将自身的PrimaryActorTick注册到其所在Level的FTickTaskLevel中
                  PrimaryActorTick.RegisterTickFunction(GetLevel());
              }
          }
          else
          {
              if(PrimaryActorTick.IsTickFunctionRegistered())
              {
                  // 【核心调用】从管理器中注销
                  PrimaryActorTick.UnRegisterTickFunction();
              }
          }
      }
      
    • 上下文剖析:

      • 这是AActorFTickTaskManager第一次握手。当一个 Actor 诞生时,RegisterAllActorTickFunctions会被调用。
      • 它首先通过调用RegisterActorTickFunctions,将自己的PrimaryActorTick(一个FActorTickFunction实例)注册到调度系统中。
      • PrimaryActorTick.RegisterTickFunction(GetLevel()) 这个调用最终会触发 FTickTaskManager::AddTickFunction将这个Tick任务添加到正确的FTickTaskLevel中。
      • 同时它还会递归地为所有子组件执行相同的操作确保整个Actor及其所有部分的Tick都能被正确管理。

  1. 执行 Tick 逻辑:从ExecuteTickAActor::Tick
    • 函数: void FActorTickFunction::ExecuteTick(...)

    • 调用时机:FTickTaskSequencer通过Task Graph执行这个FActorTickFunction对应的任务时。

    • 源码剖析:AActor::TickActor是一个内部函数,它会进一步调用我们熟悉的AActor::Tick

      void FActorTickFunction::ExecuteTick(float DeltaTime, ...)
      {
          if (IsValid(Target)) // Target就是之前注册时设置的AActor*
          {
              // ...
              // 【关键】调用Actor自身的TickActor方法
              Target->TickActor(DeltaTime*Target->CustomTimeDilation, TickType, *this);
          }
      }
      
      void AActor::Tick( float DeltaSeconds )
      {
          // 如果是蓝图Actor调用其蓝图事件图中的Tick事件
          if (/* 是蓝图 */)
          {
              ReceiveTick(DeltaSeconds);
          }
          // ...
          // 处理该Actor的Latent Actions
          LatentActionManager.ProcessLatentActions(this, ...);
      }
      
    • 上下文剖析:

      • 这里展示了从底层调度到上层逻辑的完整调用链
      • FTickTaskSequencer 并不知道AActor的存在,它只知道执行一个FTickFunctionExecuteTick虚函数。
      • FActorTickFunction 作为派生类,在ExecuteTick的实现中,调用了其Target(即AActor实例)的Tick方法。
      • 最终控制权传递到了我们游戏开发者所编写的C++ Tick函数或蓝图的Event Tick节点。这是一个典型的策略模式多态的应用。

  1. 动态控制 APISetActorTickEnabled及其伙伴
    • 函数: SetActorTickEnabled, SetActorTickInterval, SetTickGroup, AddTickPrerequisiteActor等。

    • 调用时机: 在游戏运行时的任何时候,由开发者根据逻辑需要调用。

    • 源码剖析:

      void AActor::SetActorTickEnabled(bool bEnabled)
      {
          // 直接调用其PrimaryActorTick的API
          PrimaryActorTick.SetTickFunctionEnable(bEnabled);
      }
      
      void AActor::AddTickPrerequisiteActor(AActor* PrerequisiteActor)
      {
          // 将对Actor的依赖翻译成对该Actor的PrimaryActorTick的依赖
          PrimaryActorTick.AddPrerequisite(PrerequisiteActor, PrerequisiteActor->PrimaryActorTick);
      }
      
    • 上下文剖析:

      • AActor 提供的这一整套API本质上都是对其内部成员PrimaryActorTick简单封装
      • 这种设计非常优雅。它为上层开发者提供了简洁、易于理解的接口(SetActorTickEnabled),同时将所有与底层调度系统交互的复杂性都隐藏在了FTickFunction的实现内部。
      • 开发者只需要与AActor打交道,而无需关心FTickFunctionFTickTaskManager这些底层细节,这大大降低了使用的门槛。

总结

AActor 与 Tick 系统的交互

AActor 与 Tick 系统的交互

AActor 作为 Tick 系统的主要**“客户端”**,通过以下方式与调度系统交互:

  1. 生命周期绑定: 在创建和销毁时,通过RegisterAllActorTickFunctions订阅/退订 Tick 服务。
  2. 执行回调: 通过FActorTickFunction这个适配器Adapter,让底层的ExecuteTick调用能够最终触发到上层的AActor::Tick逻辑。
  3. 接口封装: 提供了一系列简洁的 API将对FTickFunction属性的修改封装起来,为开发者提供了便利。

2.3.3 UActorComponent

源码文件Source\\Runtime\\Engine\\Private\\Components\\ActorComponent.cpp

UActorComponent 作为 Actor 功能的载体,同样需要与底层的 Tick 调度系统进行交互。它的实现方式与AActor如出一辙,都是通过封装其内部的FTickFunction派生实例(FActorComponentTickFunction)来提供 API。


  1. 注册与注销:RegisterAllComponentTickFunctions
    • 函数: void UActorComponent::RegisterAllComponentTickFunctions(bool bRegister)

    • 调用时机: 这个函数通常由其所属的AActorRegisterAllActorTickFunctions中递归调用。

    • 源码剖析:

      void UActorComponent::RegisterAllComponentTickFunctions(bool bRegister)
      {
          // 只有当组件已经注册到世界后才能注册其Tick函数
          if (bRegistered)
          {
              // ... 防止重复注册/注销 ...
              if (bTickFunctionsRegistered != bRegister)
              {
                  // 【关键】调用RegisterComponentTickFunctions来处理自身的PrimaryComponentTick
                  RegisterComponentTickFunctions(bRegister);
                  bTickFunctionsRegistered = bRegister;
                  // ...
              }
          }
      }
      
      void UActorComponent::RegisterComponentTickFunctions(bool bRegister)
      {
          if(bRegister)
          {
              // 【核心调用】SetupActorComponentTickFunction是注册逻辑的核心
              if (SetupActorComponentTickFunction(&PrimaryComponentTick))
              {
                  // 设置Target让FPrimaryComponentTickFunction知道要Tick哪个Component
                  PrimaryComponentTick.Target = this;
              }
          }
          else
          {
              // ... 注销逻辑 ...
          }
      }
      
      bool UActorComponent::SetupActorComponentTickFunction(struct FTickFunction* TickFunction)
      {
          if(TickFunction->bCanEverTick && !IsTemplate())
          {
              // ...
              // 【核心调用】最终还是调用FTickFunction的RegisterTickFunction
              TickFunction->RegisterTickFunction(ComponentLevel);
              return true;
          }
          return false;
      }
      
    • 上下文剖析:

      • 流程与AActor高度相似:RegisterAll... -> Register... -> Setup... -> FTickFunction::RegisterTickFunction
      • 一个重要的区别是Component 的 Tick 注册有一个前提条件:if (bRegistered),即组件必须先附加到 Actor 并注册到世界中,然后才能注册它的 Tick。这体现了 Component 对 Actor 的从属关系。
      • 最终,它同样是通过调用FTickFunction的公共 API RegisterTickFunction,将自己注册到FTickTaskManager中。

  1. 执行 Tick 逻辑:从ExecuteTickUActorComponent::TickComponent
    • 函数: void FActorComponentTickFunction::ExecuteTick(...)

    • 调用时机:FTickTaskSequencer执行这个FPrimaryComponentTickFunction对应的任务时。

    • 源码剖析:

      void FActorComponentTickFunction::ExecuteTick(float DeltaTime, ...)
      {
          // ...
          // 【关键】ExecuteTickHelper是一个辅助函数最终会调用下面的Lambda
          ExecuteTickHelper(Target, ..., [this, TickType](float DilatedTime)
          {
              // 在Lambda中调用Component自身的TickComponent方法
              Target->TickComponent(DilatedTime, TickType, this);
          });
      }
      
      void UActorComponent::TickComponent(float DeltaTime, ...)
      {
          // ...
          // 如果是蓝图组件调用其蓝图事件图中的Tick事件
          if (/* 是蓝图 */)
          {
              ReceiveTick(DeltaTime);
          }
          // ... 处理Latent Actions ...
      }
      
    • 上下文剖析:

      • 这再次展示了从底层调度到上层逻辑的回调机制
      • FTickTaskSequencer 调用 FActorComponentTickFunction::ExecuteTick
      • ExecuteTick 内部通过其 Target 指针,最终调用到我们为组件编写的 TickComponent 函数或蓝图的 Event Tick 节点。
      • 这种设计模式的复用,使得 Actor 和 Component 在 Tick 执行层面遵循着完全一致的逻辑。

  1. 动态控制 API一系列Set...Add...函数
    • 函数: SetComponentTickEnabled, SetTickGroup, AddTickPrerequisiteComponent等。

    • 调用时机: 游戏运行时,由开发者调用。

    • 源码剖析:

      void UActorComponent::SetComponentTickEnabled(bool bEnabled)
      {
          // 直接调用其PrimaryComponentTick的API
          PrimaryComponentTick.SetTickFunctionEnable(bEnabled);
      }
      
      void UActorComponent::AddTickPrerequisiteComponent(UActorComponent* PrerequisiteComponent)
      {
          // 将对Component的依赖翻译成对该Component的PrimaryComponentTick的依赖
          PrimaryComponentTick.AddPrerequisite(PrerequisiteComponent, PrerequisiteComponent->PrimaryComponentTick);
      }
      
    • 上下文剖析:

      • AActor的设计完全一致。所有这些上层 API 都是对内部成员PrimaryComponentTick(一个FPrimaryComponentTickFunction实例)的简单、直接的封装
      • 这种一致性极大地降低了开发者的学习成本。一旦你学会了如何控制 Actor 的 Tick你就自然而然地学会了如何控制 Component 的 Tick。

结论

UActorComponent 与 Tick 系统的交互

UActorComponent 与 Tick 系统的交互

通过剖析UActorComponent的上下文,我们可以得出结论:

UActorComponent在与 Tick 系统交互方面,是**AActor设计模式的完美复刻**。它同样通过生命周期绑定执行回调接口封装这三大手段,实现了与底层FTickTaskManager的解耦和交互。

这种高度一致的设计,体现了 UE 在 API 设计上的泛化和复用思想。它将Tick的能力从一个宏观的AActor,下放到了更细粒度的、可组合的UActorComponent上,同时保持了接口的统一和简洁。这使得开发者可以像组装乐高积木一样,为 Actor 添加各种带有独立更新逻辑的功能模块,而无需关心它们底层的调度细节。

三、总结

经过对FTickTaskManager从宏观定位、核心数据结构,到内部实现,再到外部调用上下文的逐层剖析,我们已经相对完整地构建了 UE 核心 Tick 调度系统的全貌。

3.1 设计思想FTickTaskManager 如何解决复杂性

FTickTaskManager 的核心设计,可以归结为对**“分层、委托与解耦”**这一软件工程黄金法则的极致应用。它通过以下三个层面的设计,成功地将一个极其复杂的调度问题分解为多个可控的子问题:

  1. 宏观分层(UWorld vs. FTickTaskManager: UWorld 作为“战略层”只负责定义“做什么”提供Tick上下文和“按什么顺序做”按序调用RunTickGroup)。而FTickTaskManager作为“战术层”,则完全封装了“如何高效正确地做”的所有技术细节。这种职责分离,使得游戏逻辑与底层调度技术彻底解耦。
  2. 中观委托(FTickTaskManager vs. FTickTaskSequencer: FTickTaskManager 自身也并非单体,它将更底层的、与Task Graph系统直接交互的任务排序、依赖解析和并发提交工作,进一步委托给了内部的FTickTaskSequencer。这使得FTickTaskManager可以更专注于高层的管理和接口封装,而FTickTaskSequencer则可以专注于性能极致的调度算法。
  3. 微观组织(FTickTaskManager vs. FTickTaskLevel: FTickTaskManager 并没有采用一个巨大的全局列表来管理所有Tick任务而是创造性地通过FTickTaskLevel,将任务ULevel进行分组。这种“分而治之”的策略,不仅极大地提升了管理效率,更与 UE 的关卡流送Level Streaming机制完美契合使得动态加载和卸载关卡时的 Tick 管理变得轻而易举。

3.2 性能考量:为何它比简单的循环更高效

FTickTaskManager 的高性能源于其对两大性能瓶颈的精准打击:

  1. 依赖解析与拓扑排序: 通过FTickPrerequisite和对依赖图的构建,FTickTaskManager解决了任务间的执行顺序问题。它能够生成一个保证逻辑正确的拓扑排序,这是高性能调度的前提。
  2. 并发执行: 它的真正威力在于,在保证了依赖关系的前提下,能够识别出依赖图中所有可以并行执行的任务(那些没有前置依赖的节点),并将它们打包成FGraphEvent,提交给底层的Task Graph系统。由Task Graph将这些任务分发到多个 CPU 核心上同时运行,从而将原本漫长的串行for循环,变成了一个高效的并行计算过程,极大地缩短了帧时间。

3.3 启示:如何更好地利用 Tick 系统

通过本次剖析,我们作为上层开发者可以得到以下重要启示:

  1. 善用TickGroup 理解并善用TickGroup是编写健壮、无抖动逻辑的第一步。
  2. 巧用AddPrerequisite 当 Actor 或 Component 之间存在明确的逻辑先后关系时,使用AddTickPrerequisite来明确声明这种依赖,而不是依赖于不确定的执行顺序或使用延迟等“魔法”手段。
  3. 为性能优化着想: 对于那些不依赖游戏线程数据、纯计算密集型的 Tick 逻辑,可以考虑将其封装并尝试开启bRunOnAnyThread(需谨慎评估线程安全),以充分利用引擎的并行调度能力。

FTickTaskManager 机制总结

FTickTaskManager 机制总结

总而言之,FTickTaskManager 是 UE 工程智慧的集中体现。它不仅仅是一个功能模块,更是一套解决大规模实时系统中任务调度问题的完整方案。