技术博客

AActor 生命周期:PostInitProperties 到 EndPlay

第 21 / 100 篇 · 2025-11-22 · ue5 · AActor生命周期GameFramework源码分析

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

组件初始化的三段式:

  1. PreInitializeComponents:组件注册前的准备。此时 GetComponents() 能拿到组件列表,但组件还没 OnRegister。
  2. RegisterAllComponents:对每个组件调 RegisterComponent → OnRegister → 创建渲染/物理状态。这是组件真正"进入世界"的时刻。
  3. InitializeComponents:对每个组件调 InitializeComponent。此时组件已注册,可以做跨组件的初始化(比如把 MeshComponent 的引用传给 MovementComponent)。
  4. 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(内存还没释放)。

生命周期总览

AActor 生命周期:SpawnActor 路径(左)与编辑器放置路径(右) SpawnActor 路径 NewObject / 构造函数 PostInitProperties PostActorCreated PreInit → RegisterAll PostInitializeComponents BeginPlay 编辑器放置路径 PostLoad(反序列化) InitWorld → RegisterAll PostInitializeComponents BeginPlay(统一触发) 销毁路径 Destroy → RouteEndPlay → 组件销毁 → RF_PendingKill → GC 异步回收内存 BeginPlay 顺序不确定: 按 Actor 数组顺序,不要依赖 其他 Actor 的 BeginPlay 结果 EndPlay 里不要 Spawn: World 正在清理,可能递归

各阶段该干什么

阶段 该做 不该做
构造函数 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 同步时就有了坐标系——那些机制都是在生命周期的特定阶段挂上去的。