从硬盘加载
当 UEgine::LoadMap 或者 level streaming 使用 UWorld::AddToWorld 时,任何已经处于 level 的 Actor 是从硬盘加载的。
- 当 Actor 从硬盘加载完毕后,会触发
PostLoad函数,如果需要对旧版本数据做兼容处理或修复,就写在这里。PostLoad和PostActorCreated不会同时触发:前者说明 Actor 时读档进来的,后者说明 Actor 是新诞生的,例如SpawnActor或在编辑器手动拖入场景。 - World 会调用
UAISystemBase::InitializeActorsForPlay,为 Actor 开始运行游戏做准备:- 在这个阶段所有 Actor 已经在内存中就绪,属性也已经通过
PostLoad修复完毕,引擎 现在开始建立 Actor 之间的逻辑关联。
- 在这个阶段所有 Actor 已经在内存中就绪,属性也已经通过
- Level 会对所有尚未初始化的 Actor 与通过 Seamless Travel 携带过来的 Actor 调用
ULevel::RouteActorInitialize.- 非初始化 Actor 是指那些已经存在于 Level 中,但还没有执行游戏逻辑初始化的 Actor.
- Seamless Travel carry-over 是指玩家从关卡 A 切换到关卡 B 时,某些 Actor 不会被销毁,而是被“携带”到了新关卡。
- 初始化组件:
AActor::PreInitializeComponents会在Actor的组件执行初始化之前被调用。UActorComponent::InitializeComponent是一个辅助函数,用于创建并初始化 Actor 上定义的每个组件。AActor::PostInitializeComponents则会在 Actor 的所有组件都完成初始化之后被调用。
- 当关卡开始时,调用
AActor::BeginPlay.
1 | |
在编辑器中运行
在编辑器环境下运行, Actors 是从编辑器复制的,而非从硬盘加载。之后的流程与从硬盘加载的流程类似:
- 当 Actor 从编辑器复制到 World 时,调用
UObject::PostDublicate. UAISystemBase::InitializeActorsForBeginPlay.ULevel::RouteActorInitialize.- 初始化组件:
AActor::PreInitilizeComponents.UActorComponent::InitializeComponent.AActor::PostInitializeComponents.
AActor::BeginPlay.
1 | |
Spawning
UWorld::SpawnActor.- 当 Actor 被创建后,
AActor::PostSpawnInitialize被调用,初始化 Actor 的基础属性,设置 Transform Owner 或 Instigator 等。 AActor::PostActorCreatedis called for spawned Actors after its creation, any constructor implementation behavior should go here.PostActorCreatedis mutually exclusive withPostLoad.- 这句话的意思是,如果有些初始化逻辑,本来想写在构造函数里,但是因为构造函数拿不到运行时信息,就写在
PostActorCreated里。 - 但我个人认为,基本没有初始化逻辑需要放在这个函数,尤其考虑到我可以在
PostInitializeComponents或BeginPlay中完成相关内容的初始化;这个函数更多的意义是区分Spawn和Load.
- 这句话的意思是,如果有些初始化逻辑,本来想写在构造函数里,但是因为构造函数拿不到运行时信息,就写在
AActor::ExecuteConstruction内部调用AActor::OnConstruction.AActor::OnConstruction: Blueprint Actors 在这里:- 创建组件。
- 初始化变量。
- 根据参数动态生成结构。
AActor::PostActorConstruction:Construction 完成。- 初始化组件:
AActor::PreInitilizeComponents.UActorComponent::InitializeComponent.AActor::PostInitializeComponents.
UWorld::OnActorSpawned:广播 Spawn 事件。AActor::BeginPlay.
1 | |
Deferred Spawn
Deferred Spawn 会在 Actor 创建后,但在 Blueprint 构造之前暂停,我们能够在构造逻辑执行前注入必要的数据和依赖,然后通过 FinishSpawning 继续生命周期。
UWorld::SpawnActorDeferred:允许在蓝图构造逻辑前,引入额外的初始化,即允许在OnConstruction前修改 Actor 状态。- 当 Actor 的任何属性被设置为
ExposeOnSpawn时,它就可以被延迟生成;在AActor::PostActorCreated后:- 完成属性初始化,并调用多种初始化函数,最终得到一个不完整的 Actor 实例。
AActor::FinishSpawning在 Spawn 完成后调用,内部调用AActor::ExecuteConstruction.
1 | |
End of Actor Lifecycle
AActor::Destroy:当在游戏运行时,一个 Actor 需要被移除时调用该函数;这个 Actor 被标记为待移除,随后从 Level 的 Actor 数组中移除。AActor::EndPlay: 用来确保 Actor 的生命周期结束,调用的来源如下:- 明确调用
Destroy. - 当编辑器停止游戏运行。
- 关卡切换.
- 包含该 Actor 流关卡被卸载。
- Actor 的生命周期到期时。
- 应用程序关闭。
无论是那种情况,这个 Actor 会被标记为RF_PendingKill,这样 UE 在下一个垃圾回收时,将它从内从中释放。
- 明确调用
AActor::OnDestroyed:这是对 Destroy 的遗留响应接口。最好把这里的逻辑移到EndPlay,因为OnDestroyed只有在显式调用Destroy时才会被触发,而EndPlay还能处理多种“非正常死亡”。
Garbage Collection
有时当物体被标记为 destruction,GC 会将其从内存移除,并释放任何其使用的资源。
UObject::BeginDestroy:这是一个对象释放内存,并处理多线程资源的地方。UObject::IsReadyForFinishDestroy:垃圾回收进程会调用此函数来判断对象是否可以被永久释放,如果返回否,则该函数将对象的实际销毁推迟到下一次垃圾回收循环。UObject::FinishDestroy:释放内部数据结构的最后机会,内存被正式释放前的最后一次调用。
UE 的垃圾回收会构建对象集群,以便将这些对象作为一个整体共同销毁;与逐个删除对象相比,集群化能够减少垃圾回收相关的总耗时和整体内存抖动:当一个对象加载时,它可能会创建子对象,通过将主对象及其子对象组合成一个单一的垃圾回收集群,引擎可以延迟释放集群占用的资源,直到整个对象都准备好被释放,然后一次性释放所有资源。
1 | |
实验
你可以使用 UEGameplayLAB 来进行实验。
PIE 下 Level 中的 Actor
针对 PIE 场景中的 Actor,UE的文档描述的并不准确。
在 PIE 环境下,官方文档将其描述为调用 PostDuplicate,且误导性地忽略了它仍然会调用 PostLoad.
使用 UEGameplayLAB 中的代码,将其中的 BP_LifecycleProbeActor 移到场景中,不要保存场景,观察日志输出:
1 | |
我们发现实际上,它仍然调用了 PostLoad.
在 PostDuplicate 打上断点,并观察堆栈,可以发现 UE 的 Duplicate 操作是在 StaticDuplicateObjectEx 中完成的,其内部通过序列化写入 + 反序列化读取构建新的对象图,在这个过程中仍然需要调用 PostLoad.
此外如果对场景进行保存,再次运行,输出的日志如下:
1 | |
在 PIE 场景中,Actor 的初始化路径并非单一。对于未保存到关卡资产中的 Actor,PIE 会通过 StaticDuplicateObjectEx 将 Editor World 中的对象复制到 PIE World,此时会触发 PostDuplicate,并在内部序列化过程中执行类似反序列化初始化逻辑,从而表现为 PostLoad 也被调用。
而对于已经保存到关卡(.umap)的 Actor,PIE 可以直接通过加载关卡资产构建对象,此时仅触发 PostLoad,不会经过 PostDuplicate。
因此,是否调用 PostDuplicate 取决于 PIE 在构建运行时 World 时选择的是“对象复制路径”还是“资源加载路径”。
文档所描述的流程过于简化了。
序列化与反序列化
本质上 UObject 与 二进制数据流相互转换。
程序里的对象,如 Actor 包含指针、组件、数组等一些列内容,这些东西不能直接存进硬盘,不能跨进程传,不能内存复制,所以必须转成纯数据。
序列化做的就是把对象拆成可存储的数据,而反序列化则是把数据恢复成对象。
序列化不仅仅是存文件用的,而是对象复制的基础。
StaticDuplicateObjectEx
它的逻辑可以归纳为:
- 准备阶段:清理 flags,创建空的 UObject.
- 写阶段:Serialize Write.
- 图结构收集。
- 读阶段:Serialize Read.
- PostDuplicate
- ConditionalPostLoad
- Subobject 修复 + Finalize
在这个函数中,先调用 PostDuplicate 随后调用 ConditionalPostLoad.
ConditionalPostLoad
这是一个为 PostLoad 的调用做安全保证的函数。
在 UE 调用 PostLoad 前,必须保证三件事:
- 避免重复
PostLoad. - 保证顺序正确。
- 支持多来源对象。
ConditionalPostLoad 是 UE 用来统一管理 PostLoad 执行的调度器,它通过 RF_NeedPostLoad 控制是否执行,并确保 Archetype → Subobject → Self 的顺序正确,最终调用真正的 PostLoad.
SpawnTester
为了测试 Spawned Actor 的生命周期,你可以使用 UEGameplayLAB 中的 BP_SpawnTester 来测试。
通过使用 bTestSpawn bTestDeferred bTestDestroy 观察 Actor 的生命周期。
同时启用三个测试,结果与文档一致,输出的日志如下:
1 | |
其中编号为 0 的为正常Spawn,而 1 为 Deferred Spawn.
- 正常 Spawn 的流程遵循:
PostActorCreated->OnConstruction-> 初始化组件 ->BeginPlay的顺序,且对TestValue的修改发生在BeginPlay之后。 - 而 Deferred Spawn 则不一样,在完成
PostActorCreated后,它等待FinishSpawning发生,我们在ATestSpawner中,在FinishSpawning前,修改了TestValue(顺便也修改了DebugTag),在后续的蓝图变量与组件初始化时,相关参数的值已经被修改。
Deferred Spawn 的本质是将 Actor 生命周期拆分为“对象创建”与“构造执行”两个阶段。