池化Actor:局部循环Timer会把上一世带进下一世
自动关联目录:池化Actor:局部循环Timer会把上一世带进下一世
核心判断:池化 Actor 回池后仍是有效 UObject。局部句柄创建的循环 Timer 不会因为隐藏、禁 Tick 或“业务 EndPlay”自动停止;每次借出都会再增加一只属于上一世的周期任务。
一、业务冲突:对象要复用,初始化又需要延迟执行
复杂角色从池中借出后,需要恢复物理、外观、AI、重要度和外部索引。有些初始化不能在借出当帧完成,于是最直接的写法是在借出回调里设一个延迟 Timer。普通 Spawn/Destroy 流程下,对象销毁会让 UObject 绑定失效;对象池恰恰取消了这个前提。
二、实际实现:大部分重置很完整,一只 Timer 破坏了代际
实际回池路径已经显式注销空间索引和重要度系统、释放 AIController、解绑状态事件、清理感知、关闭投射运动并通知外部状态表;借出路径则重新注册这些依赖。这说明实现者已经把池化理解成一套替代 BeginPlay/EndPlay 的业务生命周期,而不只是隐藏 Actor。
但基类在每次借出时创建一个局部 FTimerHandle,以 UObject 成员函数绑定启动循环 Timer,用于延迟恢复物理。句柄离开函数后丢失,回池路径也没有清理该 Timer。
这里不会靠 UObject 失效自愈:Actor 只是进入池中,仍被池强引用。第一次复用留下一个循环,第二次复用再加一个;即使回调本身暂时幂等,调度成本仍随复用次数增长,而且隐藏对象也会持续触碰物理状态。
三、为什么通用回池逻辑没有替它收尾
禁用 ActorTick 或组件 Tick 不影响 FTimerManager;销毁 Physics State 也不会删除 Timer;反射拷回默认属性无法恢复 TimerManager 内部的调度记录。CreateUObject/成员函数绑定只保证对象无效时不调用,而池化对象始终有效。
ClearAllTimersForObject(this) 可以作为停机兜底,但它会清掉该对象全部 UObject 绑定 Timer,容易误伤仍需保留的基础设施任务,也无法表达每只 Timer 属于哪个复用代次。最好的所有权仍然是成员句柄加 Generation。
四、目标方案:Timer 必须进入 Reset 契约
如果任务只需延迟一次,首先应把循环标志改为一次性;如果确实要周期维护,则必须让句柄可撤销:
class FPooledRuntime {
uint64 ReuseGeneration = 0;
FTimerHandle DeferredPhysicsHandle;
void OnAcquire() {
CancelDeferredPhysics();
const uint64 MyGeneration = ++ReuseGeneration;
TimerManager.SetTimer(
DeferredPhysicsHandle,
MakeWeakLambda(this, [this, MyGeneration] {
if (!IsAcquired() || MyGeneration != ReuseGeneration) return;
RestoreDeferredPhysics();
}),
Delay,
false);
}
void OnRelease() {
++ReuseGeneration; // 先让旧回调失去提交权
CancelDeferredPhysics();
UnregisterExternalSystems();
QuiescePhysicsAndAI();
}
};“先失效、再清理”很重要:即使某个回调已经进入队列,代际检查也能阻止它提交。EndPlay 和池销毁继续调用同一取消函数,保证真实销毁与逻辑回池不会维护两套规则。
若成百上千个对象都要周期检查,成员 Timer 虽正确,规模上仍不划算。此时应由集中调度器按活跃集合、时间轮或最小堆触发,只为当前借出的对象保存任务票据;回池即撤销票据。
五、验证:复用次数必须与活动任务数解耦
测试应对同一实例连续执行多轮借出/回池,并在每一轮统计该对象的活动 Timer、回调次数、池中回调次数和旧代拒绝次数。稳定条件是:借出时同类任务最多一只,回池后为零,累计复用次数增加不会提高单周期回调频率。
还要覆盖 World 暂停、时间膨胀、借出后立即回池、回池与 Timer 同帧到期、池整体清理以及 DS 无渲染路径。Unreal Insights 中如果同一对象的 Timer Delegate 调用频率随借出次数阶梯上升,问题已经被直接证明。
六、反思
对象池最危险的地方不是忘记显示 Actor,而是 UObject 身份仍在、业务生命已经结束。现有代码对 AI、索引、感知和委托做了大量正确清理,反而让这只局部 Timer 更有代表性:任何拿不到取消令牌的重复任务,都不应该跨进对象池。