2.1 三种复制声明:Replicated、ReplicatedUsing 与条件复制

声明方式对比

C++ 里一个属性有三种参与复制的写法,蓝图里对应两三个节点:

// ① 纯复制:服务器改了就广播,客户端只读
UPROPERTY(Replicated)
float Shield;

// ② 复制 + 客户端回调
UPROPERTY(ReplicatedUsing = OnRep_Health)
float Health;

UFUNCTION()
void OnRep_Health(float OldHealth);   // 可带旧值参数(UE5 推荐)

// ③ 条件复制:满足条件才发(省带宽)
UPROPERTY(Replicated /* + GetLifetimeReplicatedProps 里设条件 */)
FLinearColor TeamColor;

选择决策表:

场景 用哪个 理由
客户端只是读取渲染(坐标、颜色、状态机枚举) Replicated 简单省事
客户端收到新值要触发表现(血量变化闪红、弹药用尽换弹) ReplicatedUsing 有明确回调时机
高频但可被跳过(远处不可见的细节状态) 条件复制(COND_SimulatedOnly 等) 带宽优化

GetLifetimeReplicatedProps:复制的「配置区」

C++ 复制的新时代写法集中在 GetLifetimeReplicatedProps:

void AMyCharacter::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);

    DOREPLIFETIME(AMyCharacter, Health);
    DOREPLIFETIME_CONDITION(AMyCharacter, TeamColor, COND_SimulatedOnly);
    DOREPLIFETIME_CONDITION_NOTIFY(AMyCharacter, Ammo, COND_None, REPNOTIFY_OnChanged);
}
  • DOREPLIFETIME:最基础,总是复制。
  • DOREPLIFETIME_CONDITION:加条件。常用 COND_SimulatedOnly(只发给模拟代理)、COND_OwnerOnly(只发给拥有者)、COND_InitialOnly(仅初始同步一次,之后不再发——适合「出生时就定死」的属性)。
  • DOREPLIFETIME_CONDITION_NOTIFY:条件 + 变化通知,等价于 ReplicatedUsing + 条件。

蓝图属性面板里的「复制(Replicated)」开关 + 「复制通知(RepNotify)」是 ①② 的图形化版本;条件复制在蓝图里较弱,复杂条件建议转 C++。

条件复制的典型用法

  • COND_SimulatedOnly:远程玩家看到的「其他角色」细节(如装备外观),只在模拟代理上有效,拥有者的本地客户端有自己的本地数据——就不必发。这是最常见的带宽杀手锏,玩家自身机器上别人看你的装备要同步,而你看自己的装备是本地数据。
  • COND_OwnerOnly:自己的击杀数、自己可见的任务提示。
  • COND_InitialOnly:角色名字、职业。出生同步一次后就不再占用带宽。

易错点

  1. 蓝图里 RepNotify 触发条件:蓝图 RepNotify 只在「属性变化」时触发。如果服务器把血量从 100 设成 100(相同值),客户端不会收到回调。想「每次都触发」需要 C++ 的 REPNOTIFY_Always。
  2. Replicated 与 ReplicatedUsing 互斥:同一属性只能选一种声明方式;用 ReplicatedUsing 时不必再加 Replicated。
  3. 条件复制不是隐藏:COND_OwnerOnly 的属性在非拥有者客户端上仍存在(只是不更新),值为默认/初始。别拿它当权限控制——客户端想改照样会改它自己本地的值,只是不会同步。

心得:属性复制的默认选择应该是「Replicated + 必要的 RepNotify」,条件复制作为带宽优化后置处理。我见过有项目一开始就给所有属性加 COND_InitialOnly,结果改 logs 系统时发现「为什么玩家改名后别人看不到新名字」——业务还没稳定就过度优化,是新手通病。