AActor 的生命周期比 UObject 复杂得多——它不仅要处理 UObject 层面的创建销毁,还要处理"进入世界"、"开始游戏"、"离开世界"这些游戏语义。UE 在这条链上挂了十几个虚函数,每个都有明确的职责边界。搞混这些边界是新手崩溃的主要来源:在构造函数里 SpawnActor、在 BeginPlay 里假定组件已注册、在 EndPlay 里访问已销毁的 World。下面基于 UE5.3(Engine/Source/Runtime/Engine/Private/Actor.cpp)把整条链走完。
创建路径:SpawnActor vs 编辑器放置
AActor 的创建有两条路径,生命周期函数的顺序不同:
SpawnActor(运行时动态创建):
UWorld::SpawnActor
→ NewObject(UObject 阶段:构造 → PostInitProperties)
→ PostSpawnInitialize
→ PostActorCreated
→ PreInitializeComponents
→ RegisterAllComponents
→ InitializeComponents
→ PostInitializeComponents
→ BeginPlay(可能延迟)
编辑器放置(关卡加载):
ULevel 反序列化 → Actor 作为 Level 的子对象加载
→ PostLoad(UObject 加载路径)
→ UWorld::InitWorld → RegisterAllActorComponents
→ PreInitializeComponents → InitializeComponents → PostInitializeComponents
→ BeginPlay(World 开始时统一触发)
关键差异:SpawnActor 路径下 BeginPlay 在 Spawn 完成后立即触发(或按 bAllowDeferredBeginPlay 延迟);编辑器放置路径下 BeginPlay 由 World 在 UWorld::BeginPlay 里统一触发,所有 Actor 一起开始。
各阶段详解
构造函数 / PostInitProperties
UObject 阶段,参考前面《UObject 生命周期》。Actor 特有的注意点:
- 构造函数里
CreateDefaultSubobject创建的组件是"模板",每个实例会拿到独立副本。 - 构造函数里不能
GetWorld()——CDO 构造时没有 World,运行时报错。 PostInitProperties里 World 已经有了(SpawnActor 传入了 Outer=World),但组件还没注册。
PostActorCreated
SpawnActor 路径特有,在 Actor 被加入 World 的 Actor 列表后调用。此时 Actor 已属于 World,但组件还没注册。适合:按 World 状态做条件初始化(比如根据 GameMode 设置初始属性)。
PreInitializeComponents / InitializeComponents / PostInitializeComponents
组件初始化的三段式:
PreInitializeComponents:组件注册前的准备。此时GetComponents()能拿到组件列表,但组件还没 OnRegister。RegisterAllComponents:对每个组件调RegisterComponent→OnRegister→ 创建渲染/物理状态。这是组件真正"进入世界"的时刻。InitializeComponents:对每个组件调InitializeComponent。此时组件已注册,可以做跨组件的初始化(比如把 MeshComponent 的引用传给 MovementComponent)。PostInitializeComponents:所有组件初始化完成。此时 Actor 的组件树完全就绪。
这三段式的设计目的是分离"组件创建"和"组件间依赖初始化":如果所有初始化都在一个函数里,组件 A 初始化时引用组件 B,但 B 还没初始化,就会拿到半成品状态。
BeginPlay
游戏逻辑的起点。此时 Actor 完全就绪:组件已注册、物理已创建、渲染已提交。BeginPlay 里可以安全地做:
- 获取 World、GameMode、其他 Actor。
- 绑定输入、启动定时器。
- 访问组件并假定它们已初始化。
不能做的:假定其他 Actor 的 BeginPlay 已跑。BeginPlay 的触发顺序不确定(按 Actor 在 Level 里的数组顺序),如果 Actor A 的 BeginPlay 依赖 Actor B 的 BeginPlay 结果,需要用显式依赖(比如 A 延迟一帧执行,或 B 主动通知 A)。
Tick
每帧执行。PrimaryActorTick.bCanEverTick 控制是否启用,TickGroup 控制执行时机(PrePhysics / DuringPhysics / PostPhysics 等)。Tick 的注册在 RegisterAllComponents 时完成——组件的 Tick 也在此时注册。
EndPlay
Actor 离开世界时触发,原因通过 EEndPlayReason 区分:
Destroyed:被显式 Destroy。LevelTransition:关卡切换。EndPlayInEditor:编辑器停止 PIE。RemovedFromWorld:从 World 移除但对象还在(流送卸载)。
EndPlay 里 World 仍然有效,但不要再操作 World 状态(比如 Spawn 新 Actor 来"替代"自己)——此时 World 正在清理,Spawn 可能触发递归问题。适合做:解绑委托、停止定时器、通知其他系统"我要走了"。
BeginDestroy / FinishDestroy
UObject 阶段,参考前面。Actor 特有的:BeginDestroy 时组件可能还在(如果组件没被显式销毁),可以在 BeginDestroy 里对组件做最后清理。
销毁路径
显式销毁走 AActor::Destroy():
AActor::Destroy
→ UWorld::RemoveActor(从 Actor 列表移除)
→ RouteEndPlay(EEndPlayReason::Destroyed)
→ 销毁所有组件(Component->DestroyComponent)
→ 标记 RF_PendingKill
→ GC 在下一轮回收内存
注意:Destroy 是立即生效的——EndPlay 同步触发,组件同步销毁。但内存释放是 GC 异步做的,Destroy 之后对象内存还在,只是不能再访问。用 IsValid 检查会返回 false(因为 RF_PendingKill),用 IsValidLowLevel 检查会返回 true(内存还没释放)。
生命周期总览
各阶段该干什么
| 阶段 | 该做 | 不该做 |
|---|---|---|
| 构造函数 | CreateDefaultSubobject、设 flag | GetWorld、SpawnActor、读 UPROPERTY |
| PostInitProperties | 自我修正属性 | 假定组件已注册 |
| PostActorCreated | 按 World 状态条件初始化 | 访问组件(未注册) |
| PreInitializeComponents | 组件注册前准备 | 跨组件引用 |
| InitializeComponents | 跨组件依赖初始化 | 假定渲染/物理已就绪 |
| BeginPlay | 游戏逻辑启动、绑定输入 | 依赖其他 Actor 的 BeginPlay |
| EndPlay | 解绑委托、停止定时器 | Spawn 新 Actor、操作 World 状态 |
| BeginDestroy | 组件最后清理 | NewObject、访问 Outer 链 |
实战结论
- "组件是空的"优先查访问时机——构造函数和 PostInitProperties 里组件还没注册。
- "BeginPlay 没触发"查
bAllowDeferredBeginPlay和 World 的bBegunPlay状态。 - "EndPlay 里崩溃"查是不是访问了已销毁的 World 或 Spawn 了新对象。
- 编辑器放置的 Actor 在 PIE 开始时会走一遍完整生命周期(EndPlay → 重新 Spawn),不要假定"构造函数只跑一次"。
理解了 AActor 生命周期,后面讲 Component 的注册时机、Tick 的分组、以及网络模式下的 Actor 同步时就有了坐标系——那些机制都是在生命周期的特定阶段挂上去的。