Files
obsidian-notes/InBox/UE-CORE-002 TickTaskManager.md
2025-07-19 16:40:04 +08:00

1949 lines
93 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

source:[UE-CORE-002 TickTaskManager](https://descriptive-cemetery-9e2.notion.site/UE-CORE-002-TickTaskManager-20da28348a39808cb364f311819b06dc#20fa28348a39807596ddc5508ea7b477)
---
# 一、概述
## 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 任务调度核心与挑战](attachment:7bd27db8-d583-45aa-a558-11c3669e4e86:Editor___Mermaid_Chart-2025-06-11-030235.png)
Tick 任务调度核心与挑战
## 1.2 FTickTaskManager 的角色与定位
`FTickTaskManager` 并非一个孤立的系统,而是紧密集成在 UWorld 的 Tick 流程中,扮演着一个至关重要的角色。
大致可以从以下三个层面来精确定位 `FTickTaskManager` 的角色:
1. **UWorld 的中央调度核心**`UWorld::Tick` 函数本身并不直接执行任何 Actor 或 Component 的 Tick 逻辑。它将这一复杂任务完全委托给了 `FTickTaskManager`。在每一帧UWorld 会首先命令 `FTickTaskManager` 进行准备 (StartFrame),然后根据预设的 Tick Group 顺序,多次调用 `FTickTaskManager``RunTickGroup` 来执行相应阶段的任务,最后再通知其进行清理 (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 分层架构](attachment:1b0f4837-c508-43f3-b708-132be492a28f:Editor___Mermaid_Chart-2025-06-11-031205.png)
FTickTaskManager 分层架构
# 二、源码剖析
## 2.1 核心数据结构
### 2.1.1 FTickFunction
> **源码文件**`Source/Runtime/Engine/Classes/Engine/EngineBaseTypes.h`
`FTickFunction` 是一个 **USTRUCT**,这意味着它可以被 UE 的反射系统识别。它本身**不是一个可执行的函数**,而是一个**描述 Tick 任务所有属性和状态的“数据包”或“配置文件”**。当一个 Actor 或 Component 需要逐帧更新时,它内部就会包含一个 `FTickFunction` 的派生实例,并将其注册到 `FTickTaskManager` 中。
---
1. **公开配置属性**
这部分成员变量主要用于配置一个 Tick 任务的基本行为。
```cpp
// 定义了这个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 的行为。
```cpp
// 是否允许将多个类似的Tick任务打包在一起执行以提升性能
uint8 bAllowTickBatching:1;
// 是否在本Tick组内优先执行用于需要尽早开始的异步任务
uint8 bHighPriority:1;
// 【关键】如果为true该任务可以在任何工作线程上并行执行否则只能在游戏主线程。
uint8 bRunOnAnyThread:1;
// ... 其他一些实验性或特殊用途的标志 ...
// Tick任务的当前状态启用、禁用、冷却中
ETickState TickState : 2;
```
**分析:**
`bRunOnAnyThread` 是实现并行优化的关键。如果一个 Tick 任务的逻辑不依赖于任何游戏线程特有的数据(比如 UI、某些 UObject 操作就可以将此标志设为 true`FTickTaskManager` 就会放心地将它交给任意一个空闲的 CPU 核心去执行,从而与游戏主线程并行工作,缩短帧时间。
---
1. **核心功能性成员**
这部分是 `FTickFunction` 实现其调度能力的核心数据。
```cpp
// 【关键】存储该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以及其虚函数。
```cpp
// 注册/注销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 结构类图](attachment:3dbe052f-4415-4baf-8e3f-c0145e11d5d3:Editor___Mermaid_Chart-2025-06-11-084044.png)
FTickFunction 结构类图
### 2.1.2 FActorTickFunction & FActorComponentTickFunction
> **源码文件**`Source/Runtime/Engine/Classes/Engine/EngineBaseTypes.h`
```cpp
/**
* 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
};
```
```cpp
/**
* 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 任务具体化**,使其与一个特定的 `AActor``UActorComponent` 实例绑定,并负责在 `ExecuteTick` 被调用时,真正地去执行那个实例的 `Tick``TickComponent` 方法。
在分别解析之前,我们先看它们完全相同的设计模式:
- **共同的核心设计**
1. **继承 `FTickFunction`:** 它们都继承了基类所有的配置属性(`TickGroup`等)、状态管理(`InternalData`和API`RegisterTickFunction`等)。
2. **实现纯虚函数:** 它们都用 `override` 关键字实现了基类中定义的两个纯虚函数 `ExecuteTick``DiagnosticMessage`,从而满足了基类定义的“契约”。
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)`)。
```cpp
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` 方法:
```cpp
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);
});
}
```
---
**总结**
`FActorTickFunction``FActorComponentTickFunction``FTickFunction` 抽象概念的**具体化实现**。它们扮演着“适配器”Adapter的角色`FTickTaskManager` 的泛型调度指令,精准地“翻译”为对特定 `AActor``UActorComponent` 实例的成员函数调用。
它们的设计体现了面向对象中**多态**和**继承**的核心思想,使得上层调度系统无需关心 Tick 任务的具体类型,从而实现了高度的解耦和可扩展性。
### 2.1.3 FInternalData
> **源码文件**`Source/Runtime/Engine/Classes/Engine/EngineBaseTypes.h`
`FInternalData` 是一个专门用于存储 **已注册**`FTickFunction` 在**运行时动态变化的状态**的内部数据结构。
它的存在本身就是一个**性能优化设计**。`FTickFunction` 通过 `TUniquePtr<FInternalData> InternalData;` 来持有它。这意味着,只有当一个 Tick 任务通过 `RegisterTickFunction()` 被激活时,引擎才会为其分配 `FInternalData` 的内存。对于成千上万个可能永远不会被注册的 Tick 任务(例如,一个从未被使用的组件),引擎不会浪费任何内存来存储它们的运行时状态。这是一种典型的 **懒加载** 策略。
---
1. **核心状态与注册信息**
```cpp
// 标志位表示该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 信息**
```cpp
// 由于依赖关系一个Tick任务实际开始执行的Tick组可能晚于其配置的TickGroup。
// 这个变量记录了它在本帧实际开始的Tick组。
TEnumAsByte<enum ETickingGroup> ActualStartTickGroup;
// 类似地记录了它在本帧保证会结束的Tick组。
TEnumAsByte<enum ETickingGroup> ActualEndTickGroup;
```
**分析:** 这组变量体现了依赖系统的动态性。一个配置为 `TG_PrePhysics` 的任务,如果它依赖的另一个任务直到 `TG_EndPhysics` 才完成,那么它自己的 `ActualStartTickGroup` 就会被动态调整为 `TG_EndPhysics` 之后的某个组。这是 `FTickTaskManager``StartFrame` 阶段进行依赖图解析后得出的**动态调度计划**。
---
1. **并发与任务图相关数据**
```cpp
// 【关键】一个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` 功能。
```cpp
// 标志位缓存该函数是否因设置了TickInterval而被重新调度。
bool bWasInterval:1;
// 指向冷却列表中的下一个FTickFunction。
// FTickTaskManager内部维护了一个按冷却时间排序的链表来管理所有设置了TickInterval的任务。
FTickFunction* Next;
// 剩余的冷却时间(秒)。
// 它存储的是相对于链表中前一个元素的相对时间,这种设计可以高效地更新整个链表。
float RelativeTickCooldown;
// 上一次该函数被Tick时的游戏时间。
// 用于计算下一次应该在何时Tick。
float LastTickGameTimeSeconds;
```
**分析:** 这组数据揭示了 `TickInterval` 的实现原理。引擎并非为每个有间隔的 Tick 都创建一个独立的计时器,而是将它们组织成一个**高效的排序链表(冷却列表)**。每一帧,引擎只需要检查链表头部的任务,看看它的 `RelativeTickCooldown` 是否已经到期。这种设计避免了每帧遍历所有任务来检查时间,极大地提升了效率。
---
**总结**
![FInternalData Mindmap](attachment:7a4ed083-c473-4b10-b01d-db2a3b05c91a:Editor___Mermaid_Chart-2025-06-11-080937.png)
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` 的指针?**
---
```cpp
/**
* 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. **核心成员变量解析**
由上面的源码可知,这个结构体只有两个成员变量,但它们的设计配合得很巧妙。
```cpp
// 用于验证 PrerequisiteTickFunction 指针是否仍然有效的弱引用。
TWeakObjectPtr<class UObject> PrerequisiteObject;
// 指向实际的前置依赖Tick函数的裸指针。
struct FTickFunction* PrerequisiteTickFunction;
```
**分析:为什么定义两个指针?**
- `FTickFunction` 本身并不是一个 `UObject`,所以它不受 UE 的垃圾回收GC系统直接管理。它通常是作为 `UObject`(如 `AActor``UActorComponent`)的成员变量存在的。
- 如果直接持有一个 `FTickFunction*` 裸指针,我们无法知道它所依附的 `UObject` 是否已经被GC销毁。如果其宿主`UObject`被销毁,这个裸指针就会变成一个危险的**悬空指针**。
- **`TWeakObjectPtr` 的关键作用:**`TWeakObjectPtr`(弱对象指针)是 UE 提供的一种智能指针,它**不会**阻止其指向的 `UObject` 被 GC 回收。它的核心能力是提供了一个 `IsValid()` 方法。在访问裸指针之前,我们可以通过检查 `PrerequisiteObject.IsValid()` 来安全地判断那个 `UObject` 是否还存活。如果 `IsValid()` 返回 `false`,就意味着宿主对象已经被销毁,那么 `PrerequisiteTickFunction` 这个裸指针也必然失效了。
- **协同工作机制:**
- `PrerequisiteObject` 扮演着“哨兵”的角色。
- `PrerequisiteTickFunction` 存储着实际的目标地址。
- 只有在“哨兵”确认安全(`IsValid()`)之后,我们才会去使用那个地址。
![FTickPrerequisite 安全访问机制](attachment:7c645043-e873-45c0-b3e2-f341eefbb72f:mermaid-diagram-2025-06-11-151136.png)
FTickPrerequisite 安全访问机制
---
1. **核心功能性函数解析**
- **构造函数**
```cpp
FTickPrerequisite(UObject* TargetObject, struct FTickFunction& TargetTickFunction)
: PrerequisiteObject(TargetObject)
, PrerequisiteTickFunction(&TargetTickFunction)
{
check(PrerequisiteTickFunction);
}
```
**分析:**
构造函数非常直白它同时接收宿主 UObject 的指针和 FTickFunction 的引用并将它们分别存入两个成员变量中。这确保了每次创建一个依赖关系时安全验证所需的信息都被完整地记录下来。
---
- **Get() 方法**
```cpp
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 也能在**当前帧**就被更新(而不是等到下一帧),`FTickTaskManager``TG_PrePhysics` 执行完后,会立即检查并执行所有新生成的、属于`TG_PrePhysics` 的对象的Tick。这个过程会在每个主Tick组后都重复一次。
- **设计原因:** 解决了“帧内生成,帧内更新”的问题,使得动态生成的对象能够更及时地融入游戏世界,避免了一帧的延迟。
---
**总结**
ETickingGroup  UE 对一帧游戏时间的**“时间切片”**。通过将 Tick 任务强制归入这些有序的“切片”中FTickTaskManager 实现了对整个世界模拟流程的精确控制从根本上保证了复杂系统中数据流的正确性和逻辑的稳定性。这是 UE 高性能和高保真模拟能力的架构基石。
![ETickingGroup 流程图](attachment:c79803ca-b0d3-4f5f-bbae-c6c9b68a267c:Editor___Mermaid_Chart-2025-06-11-074346.png)
**ETickingGroup 流程图**
## 2.2 核心类
### 2.2.1 FTickTaskManagerInterface
> **源码文件**`Source\\Runtime\\Engine\\Public\\TickTaskManagerInterface.h`
```cpp
/**
* 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` 严格按顺序调用。
```cpp
// 在一帧的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 管理器的**三段式工作模型**。这是 `UWorld``FTickTaskManager` 交互的主干道。`RunTickGroup` 中的 `bBlockTillComplete` 参数更是暴露了其支持同步/异步执行模式的能力,这是实现高性能并发调度的关键接口。
---
1. **特殊帧处理函数**
```cpp
// 在游戏暂停时执行一个简化的、同步的Tick流程。
// 暂停时的Tick功能受限没有依赖、排序或分组。
virtual void RunPauseFrame(...) = 0;
```
**分析:** 这个函数提供了一种处理“游戏暂停”这一特殊状态的专用路径。它将暂停时的 Tick 逻辑与正常的游戏 Tick 逻辑分离开来,使得两种情况的处理都更加清晰。
---
1. **Tick 任务结构管理函数**
这组函数负责管理与 `ULevel` 绑定的底层数据结构。
```cpp
// 为一个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 系统内部状态的能力。
```cpp
// 将所有已注册的Tick函数的信息启用/禁用、分组等)转储到输出设备(如日志文件)。
virtual void DumpAllTickFunctions(...) = 0;
// 获取当前已启用的Tick函数的统计信息按上下文通常是类名分组计数。
// 这是Unreal Insights等性能分析工具获取数据的重要来源。
virtual void GetEnabledTickFunctionCounts(...) = 0;
```
**分析:** 这些接口的存在表明,`FTickTaskManager` 在设计之初就充分考虑到了**可观测性**和**可调试性**。开发者可以通过这些工具,清晰地看到当前有哪些 Tick 在运行,它们的分组是什么,以及各类 Tick 的数量,这对于性能优化和问题排查至关重要。
---
1. **单例访问器**
```cpp
// 全局静态函数用于获取唯一的、全局的FTickTaskManagerInterface实例。
static ENGINE_API FTickTaskManagerInterface& Get();
```
**分析:** 这是典型的**单例模式**。整个引擎中只有一个 `FTickTaskManager` 实例,任何需要与它交互的代码(如`UWorld`, `AActor`等)都通过这个静态 `Get()` 方法来获取对它的引用。
---
**总结**
`FTickTaskManagerInterface` 如同一份清晰的“产品说明书”,它通过一系列纯虚函数,向整个引擎精确地定义了 Tick 调度系统所能提供的服务:
- **核心服务:** 提供了一套完整的 `StartFrame` -> `RunTickGroup` -> `EndFrame` 的生命周期管理。
- **组织方式:** 明确了其内部是以 `Level` 为单位来管理 Tick 任务的。
- **特殊处理:** 定义了处理暂停帧的专门逻辑。
- **可观测性:** 提供了强大的调试和性能分析接口。
- **访问模式:** 规定了其作为一个全局单例的存在形式。
通过分析这个接口,我们无需深入其复杂的实现细节,就能从一个很高的层面理解`FTickTaskManager`的**设计目标、功能边界和核心职责**。
![FTickTaskManagerInterface 类图](attachment:4183eddf-e8df-42fa-b083-8b06068e7bcf:image.png)
FTickTaskManagerInterface 类图
### 2.2.2 FTickTaskManager
> **源码文件**`Source\\Runtime\\Engine\\Private\\TickTaskManager.cpp`
`FTickTaskManager``FTickTaskManagerInterface` 的**唯一实现**。它是一个全局单例,负责接收 `UWorld` 的指令,并将海量的、无序的 `FTickFunction` 组织成一个有序的、部分可并行的执行计划,然后提交给底层的 `Task Graph` 系统去执行。
---
1. **核心成员变量**
```cpp
// 对一个更底层的、专门负责任务排序和依赖解析的辅助类的引用。
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` 工作的主流程。
<aside> 1
**`StartFrame()` - 规划阶段**
```cpp
virtual void StartFrame(UWorld* InWorld, float InDeltaSeconds, ELevelTick InTickType, const TArray<ULevel*>& LevelsToTick) override
{
// 1. 设置上下文和状态
Context.World = InWorld;
// ... 其他上下文设置 ...
bTickNewlySpawned = true; // 打开“新生成”处理开关
TickTaskSequencer.StartFrame(); // 通知Sequencer准备新的一帧
// 2. 填充LevelList
FillLevelList(LevelsToTick); // 根据传入的Levels构建本帧要处理的FTickTaskLevel列表
// 3. 【关键】选择串行或并行模式来收集和排队Tick任务
if (!bConcurrentQueue) // 默认通常是串行模式
{
// 串行路径:
// a. 遍历每个Level调用其StartFrame这会收集该Level的所有Tick函数并初步处理依赖
for (FTickTaskLevel* Level : LevelList)
{
Level->StartFrame(Context);
}
// b. 再次遍历,将所有准备好的任务正式排入队列
for (FTickTaskLevel* Level : LevelList)
{
Level->QueueAllTicks();
}
}
else // 并行路径 (由CVar控制)
{
// a. 先将所有Level的所有Tick函数收集到一个大的总列表中
for (FTickTaskLevel* Level : LevelList)
{
Level->StartFrameParallel(Context, AllTickFunctions);
}
// b. 使用ParallelFor在多个工作线程上并行地为每个Tick函数排队
ParallelFor(AllTickFunctions.Num(), ...);
}
}
```
**分析:**
StartFrame 的核心职责是**“收集所有任务并完成初步排队”**。它首先设置好本帧的上下文环境,然后遍历所有需要 Tick 的 Level。有趣的是它提供了两种收集任务的路径
- **串行路径(默认):** 更稳定按部就班地处理每个 Level。
- **并行路径(实验性/特定场景):** 将任务收集过程本身也并行化,以期在拥有海量 Tick 任务的场景下提升 StartFrame 阶段的性能。
无论哪种路径最终目的都是将所有 FTickFunction 及其依赖关系交给底层的 TickTaskSequencer 去构建任务图。
</aside>
---
<aside> 2
**`RunTickGroup()` - 执行阶段**
```cpp
virtual void RunTickGroup(ETickingGroup Group, bool bBlockTillComplete) override
{
check(Context.TickGroup == Group); // 检查UWorld的调用顺序是否正确
// 【关键】将执行权完全委托给底层的Sequencer
TickTaskSequencer.ReleaseTickGroup(Group, bBlockTillComplete, TicksToManualDispatch);
Context.TickGroup = ETickingGroup(Context.TickGroup + 1); // 将上下文中的组推进到下一个
if (bBlockTillComplete)
{
// 【关键】处理“新生成”对象的逻辑
bool bFinished = false;
for (int32 Iterations = 0; Iterations < 101; Iterations++) // 最多循环101次以防死循环
{
int32 Num = 0;
// 遍历所有Level看是否有新生成的、属于当前Tick组的Actor
for (FTickTaskLevel* Level : LevelList)
{
Num += Level->QueueNewlySpawned(Context.TickGroup);
}
if (Num > 0) // 如果有
{
// 再次调用ReleaseTickGroup但这次传入TG_NewlySpawned
// 来执行这些新任务
TickTaskSequencer.ReleaseTickGroup(TG_NewlySpawned, true, TicksToManualDispatch);
}
else
{
bFinished = true; // 没有新任务了,退出循环
break;
}
}
// ... 处理 runaway (无限生成) 的异常情况 ...
}
}
```
**分析:**
`RunTickGroup` 的实现揭示了两个核心秘密:
1. **委托执行:** 它自身不执行任务,而是直接调用 `TickTaskSequencer.ReleaseTickGroup`,让 Sequencer 去和 `Task Graph` 系统打交道。它扮演的是一个“发令员”的角色。
2. **`TG_NewlySpawned` 的实现:** 这里清晰地展示了我们之前讨论的“新生成对象处理”机制。在一个主 Tick Group (`Group`) 执行完毕后,它会**立即进入一个 `for` 循环**反复检查并执行那些在本轮中新生成的、且其Tick组已经“错过”的任务。这个循环保证了“帧内生成帧内更新”的效果。 </aside>
---
<aside> 3
**`EndFrame()` - 清理阶段**
```cpp
virtual void EndFrame() override
{
TickTaskSequencer.EndFrame(); // 通知Sequencer清理
bTickNewlySpawned = false; // 关闭“新生成”处理开关
for (FTickTaskLevel* Level : LevelList)
{
Level->EndFrame(); // 通知每个Level进行清理
}
Context.World = nullptr; // 清空上下文
LevelList.Reset(); // 清空本帧的Level列表
}
```
**分析:**
`EndFrame` 的逻辑非常清晰,就是一系列的**状态重置和资源清理**操作,将 `FTickTaskManager` 恢复到一个干净的状态,为下一帧的 `StartFrame` 调用做好准备。
</aside>
---
1. **Tick 函数注册/注销接口**
```cpp
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); // 从“账本”中移除
}
```
**分析:**
`AActor``UActorComponent` 在创建和销毁时调用的就是这两个函数。它们清晰地展示了 `FTickTaskManager` 是如何通过 `FTickTaskLevel` 来**按 Level 组织**所有 Tick 函数的。每个 `FTickFunction` 也通过其 `InternalData` 反向持有一个指向其所属 `FTickTaskLevel` 的指针,形成了一个双向链接,方便快速地添加和移除。
---
**总结**
![FTickTaskManager 工作流程](attachment:6f9c6260-4c36-4e42-b495-449f11e15aef:Editor___Mermaid_Chart-2025-06-11-100647.png)
FTickTaskManager 工作流程
`FTickTaskManager` 的实现是一个典型的**分层委托**架构:
- 它作为**高层管理者**,定义了 `StartFrame` -> `RunTickGroup` -> `EndFrame` 的宏观流程。
- 它将**复杂的任务收集和排序逻辑**,委托给了内部的 `FTickTaskLevel``FTickTaskSequencer`
- 它将**最终的并行执行**,委托给了 `TickTaskSequencer` 背后的 `Task Graph` 系统。
通过这种层层委托,`FTickTaskManager` 以一种相对清晰、高内聚的方式,精心安排了整个极其复杂的 Tick 调度过程。
### 2.2.3 FTickTaskSequencer
> **源码文件**`Source\\Runtime\\Engine\\Private\\TickTaskManager.cpp`
`FTickTaskSequencer` 是一个全局单例,是`FTickTaskManager`的**核心工作委托对象**。它的名字“Sequencer”序列器精准地描述了其核心职责将一堆无序的、带有依赖关系的 Tick 任务,组织成一个可以被`Task Graph`系统高效执行的**任务序列**。
---
1. **核心成员变量**
```cpp
// 存储用于批处理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批处理
// ...
```
**分析:**
- **`TickCompletionEvents`** 和 **`TickTasks` / `HiPriTickTasks`** 是这个类的**核心数据容器**。
- `TickTasks[StartGroup][EndGroup]` 这种二维数组的设计非常精巧。它使得`Sequencer`可以快速地根据**开始组**来分发任务,并根据**结束组**来收集它们的完成事件。
- `TArrayWithThreadsafeAdd` 的使用,暗示了这些数组可以在**多个线程中被安全地添加元素**,这对于并行化的`StartFrame`流程至关重要。
- `TickBatches` 则是一种性能优化,它尝试将属性相同(比如都在同一个 Tick 组、都没有依赖)的多个小 Tick 任务,打包成一个大的`Task Graph`任务来执行,以减少任务调度的开销。
---
1. **核心函数**
<aside> 1
**`StartFrame()` - 准备工作台**
```cpp
void StartFrame()
{
// 1. 从控制台变量CVar读取并设置本帧的行为开关
bAllowConcurrentTicks = !!CVarAllowAsyncComponentTicks.GetValueOnGameThread();
bAllowBatchedTicksForFrame = !!CVarAllowBatchedTicks.GetValueOnGameThread();
// ...
// 2. 等待上一帧的清理任务完成
WaitForCleanup();
// 3. 【关键】重置所有核心容器
// 为新的一帧清空所有任务列表和完成事件列表
for (int32 Index = 0; Index < TG_MAX; Index++)
{
TickCompletionEvents[Index].Reset();
for (int32 IndexInner = 0; IndexInner < TG_MAX; IndexInner++)
{
TickTasks[Index][IndexInner].Reset();
HiPriTickTasks[Index][IndexInner].Reset();
}
}
WaitForTickGroup = (ETickingGroup)0;
}
```
**分析:**
`StartFrame` 的职责非常纯粹:**清理工作台,为新一轮的任务做准备**。它确保了上一帧的所有遗留任务(特别是异步清理任务)都已完成,然后将所有核心的数组容器重置为空。
</aside>
<aside> 2
**`QueueTickTask()` / `QueueOrBatchTickTask()` - 创建并注册任务**
```cpp
// QueueTickTask是基础版本
void QueueTickTask(const FGraphEventArray* Prerequisites, FTickFunction* TickFunction, const FTickContext& TickContext)
{
// 1. 创建一个Task Graph任务
FTickGraphTask* Task = TGraphTask<FTickFunctionTask>::CreateTask(Prerequisites, ...).ConstructAndHold(...);
// 2. 将Task指针存回FTickFunction方便后续查找
TickFunction->SetTaskPointer(FTickFunction::ETickTaskState::HasTask, Task);
// 3. 将任务的完成事件添加到对应的Tick组列表中
AddTickTaskCompletion(..., Task, ...);
}
```
**分析:** 这组函数是 **`FTickTaskManager` 调用 `FTickTaskLevel` 后,最终被执行**的地方。它的核心工作是:
1. **实例化一个`Task Graph`任务** (`TGraphTask<FTickFunctionTask>`),并将`Prerequisites`(前置依赖的完成事件)传递给它。
2. 将这个创建好的任务实例,通过`AddTickTaskCompletion`函数,**注册到** `TickTasks``TickCompletionEvents` 这两个核心二维数组中,分类存放。 `QueueOrBatchTickTask` 则是在此基础上增加了尝试**批处理**的逻辑,如果条件满足,它会把多个 Tick 任务打包到一个`FBatchTickFunctionTask`中,否则就退化为调用`QueueTickTask`</aside>
<aside> 3
**`ReleaseTickGroup()` - 发令执行**
```cpp
void ReleaseTickGroup(ETickingGroup WorldTickGroup, bool bBlockTillComplete)
{
// 1. 【关键】分发任务
{
if (SingleThreadedMode() || ...)
{
// 单线程模式下直接在游戏线程调用DispatchTickGroup
DispatchTickGroup(ENamedThreads::GameThread, WorldTickGroup);
}
else
{
// 多线程模式下将DispatchTickGroup本身也作为一个任务扔到Task Graph里去异步执行
// 这样做可以进一步利用并行,让分发过程不阻塞主线程
TGraphTask<FDipatchTickGroupTask>::CreateTask(...).ConstructAndDispatchWhenReady(...);
}
}
// 2. 【关键】等待任务完成
if (bBlockTillComplete || SingleThreadedMode())
{
// 遍历从上次等待点到当前组的所有Tick组
for (ETickingGroup Block = WaitForTickGroup; Block <= WorldTickGroup; ...)
{
if (TickCompletionEvents[Block].Num())
{
// 使用Task Graph接口等待该组所有的完成事件被触发
FTaskGraphInterface::Get().WaitUntilTasksComplete(TickCompletionEvents[Block], ...);
// 等待完成后,重置该组
ResetTickGroup(Block);
}
}
// 更新等待点
WaitForTickGroup = ETickingGroup(WorldTickGroup + 1);
}
}
```
**分析:**
`ReleaseTickGroup` 是**命令执行和同步**的核心。
1. 它首先通过 `DispatchTickGroup` 函数,**“解锁”**所有属于当前 `WorldTickGroup` 的任务,允许`Task Graph`系统开始执行它们。这个解锁过程甚至可以被异步化。
2. 如果 `bBlockTillComplete``true`,它就会进入等待逻辑,使用 `FTaskGraphInterface::Get().WaitUntilTasksComplete` **阻塞游戏主线程**,直到 `TickCompletionEvents` 数组中记录的、属于当前及之前所有需要等待的组的`FGraphEvent`全部完成。这就是实现**同步点**的机制。 </aside>
<aside> 4
**`DispatchTickGroup()` - 底层分发**
```cpp
void DispatchTickGroup(ENamedThreads::Type CurrentThread, ETickingGroup WorldTickGroup)
{
// 遍历 HiPriTickTasks[WorldTickGroup][...]
for (...)
{
// 调用每个Task的Unlock允许它开始执行
TickArray[Index]->Unlock(CurrentThread);
}
// 遍历 TickTasks[WorldTickGroup][...]
for (...)
{
TickArray[Index]->Unlock(CurrentThread);
}
}
```
**分析:** 这是最底层的分发函数。它做的事情非常简单:遍历 `TickTasks` 二维数组中,所有**起始组**为当前 `WorldTickGroup` 的任务,并调用它们的 `Unlock` 方法。`Unlock` 会通知 `Task Graph` 系统:“这个任务的所有前置条件都已满足,可以开始执行了!”
</aside>
---
**总结**
![FTickTaskSequencer 工作流程](attachment:5a98047a-d98f-4e09-b89f-a4d15db8ba90:Editor___Mermaid_Chart-2025-06-11-125137.png)
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` 的内部容器。
```cpp
// 对全局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`的动态激活,链表结构比数组或集合更适合进行高效的遍历和中间节点的移除。
- `TickFunctionsToReschedule``NewlySpawnedTickFunctions` 是**临时性**的容器,用于处理帧内的状态变化,体现了其逻辑的严谨性。
---
1. **核心函数**
<aside> 1
**`AddTickFunction()` / `RemoveTickFunction()`**
```cpp
void AddTickFunction(FTickFunction* TickFunction)
{
// ...
if (TickFunction->TickState == FTickFunction::ETickState::Enabled)
{
AllEnabledTickFunctions.Add(TickFunction); // 添加到启用集合
if (bTickNewlySpawned)
{
NewlySpawnedTickFunctions.Add(TickFunction); // 如果在Tick中额外添加到新生成集合
}
}
else
{
AllDisabledTickFunctions.Add(TickFunction); // 添加到禁用集合
}
}
```
**分析:**
`AddTickFunction` 的逻辑清晰地展示了其**分类管理**的职责。它会根据 `TickFunction` 的当前状态,将其放入不同的“抽屉”(`TSet`)中。特别地,它处理了“帧内生成”的特殊情况,确保这些新任务不会被错过。`RemoveTickFunction` 也是类似的反向操作。
</aside>
<aside> 2
**`StartFrame()` - 准备本 Level 的任务**
```cpp
int32 StartFrame(const FTickContext& InContext)
{
// ... 设置上下文 ...
// 1. 将待调度的函数放入冷却列表
ScheduleTickFunctionCooldowns();
// 2. 遍历冷却列表激活到期的Tick函数
FTickFunction* TickFunction = AllCoolingDownTickFunctions.Head;
while (TickFunction)
{
// ... 计算是否应该激活 ...
if (/* 冷却时间已到 */)
{
// 将其状态设为Enabled这样在QueueAllTicks时就会被处理
TickFunction->TickState = FTickFunction::ETickState::Enabled;
// ...
}
}
// ...
return AllEnabledTickFunctions.Num() + CooldownTicksEnabled;
}
```
**分析:**
`StartFrame` 的核心职责是**处理带有`TickInterval`的函数**。它会检查 `AllCoolingDownTickFunctions` 链表,看哪些函数的冷却时间已经在本帧 `DeltaSeconds` 内结束。如果结束了,就将其状态标记为 `Enabled`,使其有资格在本帧被 Tick。这个过程确保了`TickInterval`功能的正确实现。
</aside>
<aside> 3
**`QueueAllTicks()` - 将任务提交给 Sequencer**
```cpp
void QueueAllTicks()
{
FTickTaskSequencer& TTS = FTickTaskSequencer::Get();
// 1. 遍历所有启用的Tick函数
for (TSet<FTickFunction*>::TIterator It(AllEnabledTickFunctions); It; ++It)
{
FTickFunction* TickFunction = *It;
// 【关键】调用TickFunction自己的排队方法
TickFunction->QueueTickFunction(TTS, Context);
// 2. 如果有TickInterval则将其移出启用列表并准备重新调度冷却
if (TickFunction->TickInterval > 0.f)
{
It.RemoveCurrent();
RescheduleForInterval(TickFunction, TickFunction->TickInterval);
}
}
// 3. 遍历冷却列表中本帧被激活的函数
while (FTickFunction* TickFunction = AllCoolingDownTickFunctions.Head)
{
if (TickFunction->TickState == FTickFunction::ETickState::Enabled)
{
TickFunction->QueueTickFunction(TTS, Context);
RescheduleForInterval(...); // 重新调度冷却
AllCoolingDownTickFunctions.Head = TickFunction->InternalData->Next; // 从冷却列表头部移除
}
else
{
break;
}
}
}
```
**分析:**
`QueueAllTicks`**`FTickTaskLevel``FTickTaskSequencer` 之间的桥梁**。它遍历自己管理的所有“本帧需要执行”的函数(包括一直启用的和冷却结束的),然后调用每个 `FTickFunction` 自己的 `QueueTickFunction` 方法。`FTickFunction::QueueTickFunction` 内部会进一步调用 `FTickTaskSequencer``QueueTickTask`,从而将任务正式提交给调度器。
这个函数还负责**循环利用**带有`TickInterval`的函数:在它们被 Tick 后,立即将它们重新放入“待调度冷却”的列表(`TickFunctionsToReschedule`)中。
</aside>
<aside> 4
**`QueueNewlySpawned()` - 处理“插队”的任务**
```cpp
int32 QueueNewlySpawned(ETickingGroup CurrentTickGroup)
{
// ...
for (TSet<FTickFunction*>::TIterator It(NewlySpawnedTickFunctions); It; ++It)
{
// 同样调用QueueTickFunction将新任务提交给Sequencer
(*It)->QueueTickFunction(TTS, Context);
// ...
}
NewlySpawnedTickFunctions.Empty(); // 处理完后清空
// ...
}
```
**分析:** 这个函数由 `FTickTaskManager::RunTickGroup` 调用专门用于处理那些在帧中途新生成的Actor的Tick任务。它遍历 `NewlySpawnedTickFunctions` 集合,并将这些“插队”的任务也提交给`Sequencer`执行,从而实现了“帧内生成,帧内更新”的机制。
</aside>
---
**总结**
![FTickTaskLevel 数据组织与工作流](attachment:7280ffcf-e8ba-4fb7-a182-3a9f9fd1b764:Editor___Mermaid_Chart-2025-06-11-132305.png)
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 (`bDoingActorTicks``true`) 之后,第一件重要的事情就是调用 `FTickTaskManager``StartFrame` 方法。
```cpp
// 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`
```cpp
// 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` 会调用 `FTickTaskManager``EndFrame` 来结束本帧的 Tick 调度。
```cpp
// in UWorld::Tick, after all RunTickGroup calls
if (bDoingActorTicks)
{
// ...
// 【关键调用 3】通知TickTaskManager本帧的Tick调度工作全部结束
FTickTaskManagerInterface::Get().EndFrame();
}
```
**上下文剖析:**
1. **时机:** 这个调用发生在所有 `RunTickGroup` **之后**
2. **作用:** `UWorld` 在这里发出一个**“收工”**的信号。`FTickTaskManager` 接收到后,会执行其内部的清理逻辑,如重置状态、清空临时容器等,为下一帧做好准备。
---
**总结**
![UWorld::Tick 与 FTickTaskManager 交互时序图](attachment:de5ca54d-82ca-4c26-bf55-01f5e0c0f00c:Editor___Mermaid_Chart-2025-06-11-133444.png)
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被销毁时。
- **源码剖析:**
```cpp
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);
}
}
}
// ...
}
```
```cpp
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();
}
}
}
```
- **上下文剖析:**
- 这是`AActor`与`FTickTaskManager`的**第一次握手**。当一个 Actor 诞生时,`RegisterAllActorTickFunctions`会被调用。
- 它首先通过调用`RegisterActorTickFunctions`,将自己的`PrimaryActorTick`(一个`FActorTickFunction`实例)注册到调度系统中。
- `PrimaryActorTick.RegisterTickFunction(GetLevel())` 这个调用最终会触发 `FTickTaskManager::AddTickFunction`将这个Tick任务添加到正确的`FTickTaskLevel`中。
- 同时它还会递归地为所有子组件执行相同的操作确保整个Actor及其所有部分的Tick都能被正确管理。
---
1. **执行 Tick 逻辑:从`ExecuteTick`到`AActor::Tick`**
- **函数:** `void FActorTickFunction::ExecuteTick(...)`
- **调用时机:** 当`FTickTaskSequencer`通过`Task Graph`执行这个`FActorTickFunction`对应的任务时。
- **源码剖析:**`AActor::TickActor`是一个内部函数,它会进一步调用我们熟悉的`AActor::Tick`。
```cpp
void FActorTickFunction::ExecuteTick(float DeltaTime, ...)
{
if (IsValid(Target)) // Target就是之前注册时设置的AActor*
{
// ...
// 【关键】调用Actor自身的TickActor方法
Target->TickActor(DeltaTime*Target->CustomTimeDilation, TickType, *this);
}
}
```
```cpp
void AActor::Tick( float DeltaSeconds )
{
// 如果是蓝图Actor调用其蓝图事件图中的Tick事件
if (/* 是蓝图 */)
{
ReceiveTick(DeltaSeconds);
}
// ...
// 处理该Actor的Latent Actions
LatentActionManager.ProcessLatentActions(this, ...);
}
```
- **上下文剖析:**
- 这里展示了**从底层调度到上层逻辑的完整调用链**。
- `FTickTaskSequencer` 并不知道`AActor`的存在,它只知道执行一个`FTickFunction`的`ExecuteTick`虚函数。
- `FActorTickFunction` 作为派生类,在`ExecuteTick`的实现中,调用了其`Target`(即`AActor`实例)的`Tick`方法。
- 最终控制权传递到了我们游戏开发者所编写的C++ `Tick`函数或蓝图的`Event Tick`节点。这是一个典型的**策略模式**和**多态**的应用。
---
1. **动态控制 API`SetActorTickEnabled`及其伙伴**
- **函数:** `SetActorTickEnabled`, `SetActorTickInterval`, `SetTickGroup`, `AddTickPrerequisiteActor`等。
- **调用时机:** 在游戏运行时的任何时候,由开发者根据逻辑需要调用。
- **源码剖析:**
```cpp
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`打交道,而无需关心`FTickFunction`、`FTickTaskManager`这些底层细节,这大大降低了使用的门槛。
---
**总结**
![AActor 与 Tick 系统的交互](attachment:77dae94b-ae07-434b-8118-2e4366fb884a:Editor___Mermaid_Chart-2025-06-11-140839.png)
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)`
- **调用时机:** 这个函数通常由其所属的`AActor`在`RegisterAllActorTickFunctions`中递归调用。
- **源码剖析:**
```cpp
void UActorComponent::RegisterAllComponentTickFunctions(bool bRegister)
{
// 只有当组件已经注册到世界后才能注册其Tick函数
if (bRegistered)
{
// ... 防止重复注册/注销 ...
if (bTickFunctionsRegistered != bRegister)
{
// 【关键】调用RegisterComponentTickFunctions来处理自身的PrimaryComponentTick
RegisterComponentTickFunctions(bRegister);
bTickFunctionsRegistered = bRegister;
// ...
}
}
}
```
```cpp
void UActorComponent::RegisterComponentTickFunctions(bool bRegister)
{
if(bRegister)
{
// 【核心调用】SetupActorComponentTickFunction是注册逻辑的核心
if (SetupActorComponentTickFunction(&PrimaryComponentTick))
{
// 设置Target让FPrimaryComponentTickFunction知道要Tick哪个Component
PrimaryComponentTick.Target = this;
}
}
else
{
// ... 注销逻辑 ...
}
}
```
```cpp
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 逻辑:从`ExecuteTick`到`UActorComponent::TickComponent`**
- **函数:** `void FActorComponentTickFunction::ExecuteTick(...)`
- **调用时机:** 当`FTickTaskSequencer`执行这个`FPrimaryComponentTickFunction`对应的任务时。
- **源码剖析:**
```cpp
void FActorComponentTickFunction::ExecuteTick(float DeltaTime, ...)
{
// ...
// 【关键】ExecuteTickHelper是一个辅助函数最终会调用下面的Lambda
ExecuteTickHelper(Target, ..., [this, TickType](float DilatedTime)
{
// 在Lambda中调用Component自身的TickComponent方法
Target->TickComponent(DilatedTime, TickType, this);
});
}
```
```cpp
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`等。
- **调用时机:** 游戏运行时,由开发者调用。
- **源码剖析:**
```cpp
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 系统的交互](attachment:f40fd509-be3a-4923-a41d-6cdaa7510b75:Editor___Mermaid_Chart-2025-06-11-142214.png)
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 机制总结](attachment:3ef28401-8182-47c8-8d55-700873bf47b9:Mermaid_Chart_-_Create_complex_visual_diagrams_with_text._A_smarter_way_of_creating_diagrams.-2025-06-11-144606.png)
FTickTaskManager 机制总结
总而言之,`FTickTaskManager` 是 UE 工程智慧的集中体现。它不仅仅是一个功能模块,更是一套解决大规模实时系统中任务调度问题的完整方案。