Actor 生命周期与 Tick 顺序
自动关联目录:Actor 生命周期与 Tick 顺序
Actor 的生命周期是一串固定顺序的回调,而绝大多数"为什么我的初始化没生效""为什么这个指针是空的"类问题,本质上都是把代码放错了阶段。
一句话定位:构造函数只设默认值,BeginPlay 才开始有世界,Tick 的顺序必须显式声明。
出生时序
SpawnActor
→ 构造函数(C++ 构造)
→ PostSpawnInitialize
→ PostActorCreated ← 此时组件还没注册
→ ExecuteConstruction(蓝图构造脚本 OnConstruction)
→ (从关卡/磁盘加载时)PostLoad
→ RegisterAllComponents
→ InitializeComponents
→ PostInitializeComponents ← 组件都就绪了
→ BeginPlay ← 世界存在、其它 Actor 可访问
→ (网络)OnRep / 复制到达从关卡里加载的 Actor 走的是同一套,只是多了 PostLoad。
| 回调 | 此时可用什么 | 该做什么 |
|---|---|---|
| 构造函数 | 几乎没有:无 World、无组件、无其它 Actor | 只设默认值、CreateDefaultSubobject |
PostActorCreated | 对象存在,组件未注册 | 很少用 |
OnConstruction / 构造脚本 | 组件已创建,编辑器里改属性会再次触发 | 依赖属性生成组件/布局 |
PostInitializeComponents | 所有组件就绪 | 组件间绑定、取组件指针 |
BeginPlay | World、GameState、其它 Actor 全部可用 | 游戏逻辑初始化 |
PostLogin(Player 相关) | 见 Framwork | 生成 Pawn |
最常见的错误是在构造函数里 GetWorld()——返回 null,因为 CDO 也是在此时构造的(见 UObject)。
SpawnActor
// 生成后立刻改属性 → 应用 Deferred,避免"生成瞬间已经跑了一次 BeginPlay"
AMyActor* A = GetWorld()->SpawnActorDeferred<AMyActor>(Class, Transform);
A->TeamId = 3; // 在 FinishSpawning 之前设置
UGameplayStatics::FinishSpawningActor(A, Transform);| 变体 | 用途 |
|---|---|
SpawnActor | 常规 |
SpawnActorDeferred | 生成后、BeginPlay 前要设属性 |
SpawnActorAbsolute | 忽略父级变换 |
需要先设属性再让 Actor 看见世界时,SpawnActorDeferred 是唯一正确选择。否则 BeginPlay 已经跑完了,你的赋值来晚了一步。
死亡时序
Destroy()
→ EndPlay(EndPlayReason) ← 游戏逻辑清理在这里
→ Destroyed() ← 已不可信,仅通知
→ UnregisterComponents
→ 标记为待销毁 → 下次 GC 真正释放 → BeginDestroyEEndPlayReason | 含义 |
|---|---|
ActorDestroyed | 主动 Destroy() |
LevelTransition | 关卡切换 |
EndPlayInEditor | 编辑器停止 PIE |
RemovedFromWorld | 从世界移除(流式卸载) |
Quit | 退出 |
void AMyActor::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
if (EndPlayReason == EEndPlayReason::LevelTransition) { return; } // 切场景不必清理全局资源
Super::EndPlay(EndPlayReason);
}清理逻辑放 EndPlay,不要放 BeginDestroy——后者是 GC 阶段,其它 UObject 的存活没有保证(见 GC)。
Tick
AMyActor::AMyActor()
{
PrimaryActorTick.bCanEverTick = true;
PrimaryActorTick.TickGroup = TG_PrePhysics;
PrimaryActorTick.bStartWithTickEnabled = false; // 需要时再 Enable
PrimaryActorTick.TickInterval = 0.05f; // 降频,不用每帧
}| TickGroup | 时机 |
|---|---|
TG_PrePhysics | 物理模拟之前(默认,大多数逻辑在这) |
TG_StartPhysics | 物理开始 |
TG_DuringPhysics | 物理步进中 |
TG_EndPhysics | 物理结束 |
TG_PostPhysics | 物理之后(依赖物理结果用这个) |
TG_PostUpdateWork | 相机更新之后 |
TG_LastDemotable / TG_NewlySpawned | 最末 |
相机跟着角色抖、角色跟着物理抖,八成是 TickGroup 选错了:相机应该在 TG_PostPhysics 之后。
依赖顺序
// 显式声明:A 必须在 B 之前 Tick
MyActor->PrimaryActorTick.AddTickPrerequisite(OtherActor, OtherActor->PrimaryActorTick);不要靠 Spawn 顺序或数组顺序来保证 Tick 先后——那是巧合,不是保证。
组件 Tick
| 要点 | 说明 |
|---|---|
PrimaryComponentTick.bCanEverTick | 组件默认不开 Tick |
| 组件 Tick 跟随 Actor 的 TickGroup | 除非单独设 |
RegisterComponent | 手动创建的组件要注册才会 Tick |
| 大量组件 Tick | 开销显著,能合并就合并 |
Actor 的 Tick 与组件的 Tick 是两套独立开关,新手动创建组件后忘记 RegisterComponent 是常见漏项。
常见坑
| 坑 | 说明 |
|---|---|
构造函数里 GetWorld() / 访问其它 Actor | null 或未初始化 |
| 在构造函数里做游戏逻辑 | 会被跑很多次(CDO、PIE、热重载) |
BeginPlay 里假设 GameState/PlayerState 已就绪 | 顺序不保证,用 OnRep 或延迟 |
| 用 SpawnActor 而非 Deferred | 属性设晚了一步 |
清理写在 BeginDestroy | 其它对象可能已销毁 |
| Tick 里做重活 | 拆到定时器或异步 |
| 靠生成顺序保证 Tick 顺序 | 应用 AddTickPrerequisite |
| 默认开 Tick 但不需要 | 白白消耗,bCanEverTick = false |
EndPlay 里不区分 Reason | 切场景时误清了全局资源 |
许可协议:CC BY
作者:Davids
本文链接:https://hustjjd.github.io/b1984985.html
更新于:2026年10月10日