启动就绪:Initialize 完成不等于 World 已可用

自动关联目录:启动就绪:Initialize 完成不等于 World 已可用

核心判断:Initialize() 只证明 Subsystem 被构造,不证明 GameInstance、NetDriver、PhysicsScene、端口监听或运行时 VM 已经就绪。

一、业务冲突

客户端和 DS 都需要一个明确的“可以继续”信号。若客户端过早启动,会拿不到 GameInstance 或网络角色;若编辑器过早连接 DS,会撞上尚未绑定的端口;若物理服务过早注册,会面对尚不存在的 PhysicsScene。简单地把所有逻辑推迟到 BeginPlay 又会改变 Actor 出生顺序和网络身份确定性。

二、实际实现

实际代码记录了三种不同的时序:

  • ClientTravel 创建新 World 时,WorldSubsystem 初始化阶段 GetNetMode() 仍可能表现为 Standalone,GetGameInstance() 也可能为空,因此客户端 bootstrap 改为 PostLoadMap 与 Ticker 双触发,等依赖事实满足后只执行一次。
  • 物理 WorldSubsystem 初始化早于 PhysicsScene 创建,真正的物理接线延迟到引擎阶段回调。
  • DS 曾在场景 bootstrap 完成后立刻写 ready 文件,但此时 NetDriver 端口尚未绑定。把整个 bootstrap 挪到地图加载后又破坏了启动 Actor 的确定性网络身份。最终只把“对外可连接”信号移动到地图加载和监听完成之后,保留原有对象出生时序。

三、值得保留

最成熟的地方是没有用一个生命周期回调代表所有 Ready。领域数据准备、Actor 创建、网络监听和外部接入被允许在不同时间完成;修复也只移动错误的就绪信号,没有为了表面对称改乱正确的出生顺序。

四、结构性缺陷

PostLoadMap、Ticker、BeginPlay 和阶段回调仍由各子系统手工组合。双触发能兼容引擎分支差异,却会产生轮询、重复判断和“哪个布尔值才是真正 Ready”的维护成本。进程静态一次性守卫还可能掩盖同进程多 World 或自动化测试重入问题。

五、目标设计

把启动建模成 Readiness DAG,而不是回调列表:

enum class Fact { WorldCreated, GameInstanceBound, RoleKnown,
                  RuntimeDomainReady, PhysicsReady, NetListening };

barrier.Require(Service::ClientRuntime,
    {Fact::GameInstanceBound, Fact::RoleKnown, Fact::RuntimeDomainReady});
barrier.Require(Service::ExternalJoin,
    {Fact::NetListening, Fact::RuntimeDomainReady});

barrier.OnReady(Service::ExternalJoin, [token] {
    if (token.IsCurrent()) PublishReadyEvidence();
});

每个 Fact 必须绑定 World/Session Generation;World 销毁时统一撤销,而不是留下进程级 true。

六、取舍与验证

Readiness DAG 比在 Initialize() 里直接调用更复杂,但可以回答“缺哪一项、等了多久、谁发布的”。测试要打乱 GameInstance、NetDriver、VM、PhysicsScene 和地图回调顺序,并验证重复事件幂等、旧 World 事件无效、外部 Ready 绝不早于端口可连接,同时保持启动 Actor 的网络身份和出生顺序不变。

UE 生命周期回调是观察点,不是业务承诺。业务 Ready 必须由可验证事实组合出来。