动态组件:NetMode不等于Actor的本地角色

自动关联目录:动态组件:NetMode不等于Actor的本地角色

核心判断:NetMode 描述这个 World 运行在哪类进程,LocalRole 描述这个 Actor 在本机的网络角色,IsLocallyControlled 描述控制权。动态组件在哪创建、是否复制、由谁初始化,必须分别回答,不能用一个条件代替三者。

一、业务冲突:同一个 Actor 在不同端需要不同能力

角色、建筑和可攻击物会按玩法配置追加属性、受击、输入、表现或辅助组件。DS 只需要权威逻辑;拥有客户端需要输入与预测;其他客户端只需要表现;某些组件由服务端创建并复制,另一些则应在每个渲染客户端本地创建。

若把全部组件固化在基类,DS 会承担表现依赖,轻量玩法也会加载无关能力;运行时按配置装配是合理方向,但组件分发本质上是一张网络执行矩阵。

二、实际实现:注册顺序正确,角色筛选混在一个枚举里

实际构建器从组件 CDO 读取创建条件,在 Actor 的 PostInitializeComponents 阶段执行。满足条件后,以 Actor 为 Outer 创建组件,先设置 SetIsReplicated,再 RegisterComponent;随后 Actor 的业务 BeginPlay 会重新枚举组件并调用自定义初始化。

值得保留的有三点:

  1. 动态组件在业务 BeginPlay 前创建,后续组件枚举能看到完整集合;
  2. 复制开关在注册前确定,避免组件先以错误模式注册再修改;
  3. 是否创建由组件 CDO 声明,业务 Actor 不需要硬编码每个具体类型。

问题出在条件语义。一条“仅自主端”分支实际只判断 Standalone,没有检查网络 Client 上该 Pawn 是否本地控制;“所有客户端”通过“不是 Dedicated Server”实现,因此也包含 Standalone 和 Listen Server;“多端但不复制”则在所有实例创建,包括 DS 与模拟代理。这里把进程类型、Actor 角色和能力目的压进了同一个枚举。

三、失败机制:看起来都是 Client,实际所有权完全不同

在 Listen Server 中,同一进程既是 Authority,又有本地渲染与输入;在普通 Client 中,本地 Pawn 是 AutonomousProxy,其他 Pawn 是 SimulatedProxy。仅检查 NetMode != DedicatedServer 无法区分拥有者和旁观者,仅检查 ROLE_Authority 又无法区分 DS 与 Listen Server 的表现需求。

装配还只在创建阶段求值一次。Pawn 被重新 Possess、Owner 迁移或进入回放后,原来的 Desired Set 可能已经变化。公开的 CreateAllComponents 若被再次调用,也没有按稳定键检查已有组件;正常 PostInitialize 只调用一次时没问题,但接口本身并不幂等。

四、目标方案:执行域和复制模式正交

enum class EExecutionDomain {
    Authority,
    LocallyControlled,
    RenderingClient,
    EveryInstance
};

enum class EComponentReplication {
    None,
    ReplicatedFromAuthority
};

struct FRuntimeComponentSpec {
    FName StableKey;
    TSubclassOf<UActorComponent> Class;
    TSet<EExecutionDomain> Domains;
    EComponentReplication Replication;
};

FExecutionContext MakeContext(const AActor& Actor) {
    return {
        .NetMode = Actor.GetNetMode(),
        .bAuthority = Actor.HasAuthority(),
        .bLocallyControlled = IsLocallyControlled(Actor),
        .bCanRender = Actor.GetNetMode() != NM_DedicatedServer
    };
}

构建器先把所有 Spec 归约为 Desired Set,再与 Actor 当前由构建器管理的组件做 Diff:创建缺失项、保留相同项、销毁不再需要的项。StableKey 而不是类名充当身份,允许同类组件承担不同槽位,也让重复调用保持幂等。

复制组件只能由 Authority 创建,客户端等待 Actor Channel 创建对应子对象,不能再本地创建一个同类镜像;纯客户端组件则在每个满足执行域的端本地创建,不进入复制列表。SceneComponent 还要在 Register 前建立 Attachment;所有组件完成后再统一开放业务初始化。

五、验证矩阵

运行位置AuthorityLocal ControlRendering典型组件
Standalone是是是权威、输入、表现
Dedicated Server是否否权威逻辑
Listen Server 本地 Pawn是是是权威、输入、表现
Client 自己的 Pawn否是是输入、预测、表现
Client 其他 Pawn否否是表现

自动化应逐格验证组件集合、复制标志、初始化次数和销毁次数,并覆盖重复 Reconcile、Possess 切换、Actor 回池和回放。完成条件是每个 StableKey 最多一个组件,客户端不会同时持有本地镜像与复制实例,角色变化后 Desired Set 能收敛。

六、反思

现有实现选择 CDO 配置、在 PostInitialize 阶段注册、先设置复制标志,这些都是正确的 UE 工程实践。真正需要升级的是“创建条件”这层抽象:进程能力、Actor 权威、玩家控制和复制来源是四个维度,压成一个枚举后,分支越补越难证明。