1.2 复制体系的三个层次:Actor、属性、组件

UE 的复制(Replication)是一套声明式系统:你不写「把数据发过去」的代码,而是声明「这个 Actor 需要同步」「这个属性需要复制」「这个组件需要跟随主人」,剩下的由框架自动完成。理解声明的三个层次,是看懂任何联网代码的前提。

层次一:Actor 复制

Actor 是复制的基本单位。一个 Actor 要参与网络,需要两件事:

AProjectile::AProjectile()
{
    bReplicates = true;               // 这个 Actor 需要在网络中存在
    bAlwaysRelevant = true;           // 可选:总是对所有客户端相关(见 1.3)
}

服务器上 spawn 一个 bReplicates 的 Actor,框架会自动通知所有相关客户端各自 spawn 出对应的「远程副本」。销毁也是同步的。这里的「相关」由相关性检查决定(下一节讲)。关键认知:客户端的副本 Actor 生命周期不归客户端管——客户端不能擅自销毁一个复制 Actor,服务器销毁时客户端自然跟着销毁。

层次二:属性复制

在 Actor 内部,逐属性声明复制:

UPROPERTY(Replicated, Transient)
float Health;                        // 服务器修改后自动广播给客户端

UPROPERTY(ReplicatedUsing = OnRep_Ammo)
int32 Ammo;                          // 复制,且客户端收到时回调

UFUNCTION()
void OnRep_Ammo();                   // 客户端回调:刷新 UI、播换弹音

属性复制的三种典型形态(决策表):

需求 手段 说明
数据变化要同步,客户端只需读取渲染 Replicated 最常见,如血量、坐标
客户端收到新值时要执行表现逻辑 ReplicatedUsing(C++)/ OnRep(蓝图) 如血量变化时播受击特效、UI 闪动
只在服务器和 owning client 之间同步 Replicated + 服务器设置(配合 RPC) 如只有自己能看到的技能冷却提示

几个容易踩的坑:

  • 复制方向单一:属性只能「服务器 → 客户端」。客户端要改,必须通过 RPC 请求服务器。蓝图的「设置复制变量」在客户端执行时是无效的(不报错,但不同步)——这是新手最常踩的「为什么我改了没用」。
  • Transient。复制属性通常加 Transient,因为它不该存盘(存档由专门的存档系统做)。
  • 默认值与初始化。客户端首次收到 Actor 时,复制属性的初始值来自服务器 spawn 时的值;构造函数里的默认值只在「未复制」时有意义。
  • 结构体/数组的复制语义:结构体整体复制,数组支持复制但无增量回调(成员级变化感知不到,见 2.4 的 FastArray 方案)。

层次三:组件复制

组件本身也可以是复制组件(bReplicates = true 的 ActorComponent,或 SceneComponent 加 SetIsReplicated)。要点:

  • 组件跟随 Owner Actor 的相关性:Actor 不相关,其复制组件也不会同步。
  • 子对象引用的复制:UE5 支持 Subobject 复制(Iris 体系下更完善),让「Actor 内持有的组件/子对象引用」也能被正确同步,避免客户端拿到空指针。详见「Iris 复制系统」(https://www.wanxiangyundang.top/read/unreal-engine-5-8-docs/iris-replication-system-in-unr-0cc3)。
  • 组件销毁:服务器移除组件,客户端同步移除;客户端单独 Remove 自己机器上的组件,不同步给服务器。

一句话总结

三层从粗到细:Actor 决定「存在性」,属性决定「状态内容」,组件决定「模块化状态的归属」。实际开发中 90% 的工作在第二层,但第一层的「哪些 Actor 该复制」决策往往更影响带宽。

心得:复制声明尽量保持「最小集」。每多一个复制属性,就多一份常驻带宽开销。我做过一个项目,把 40 个复制属性砍到 12 个(其余改成本地计算或 RPC 按需下发),每客户端带宽降了 40%,而玩家体感毫无差别。