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%,而玩家体感毫无差别。
