属性复制条件与 RepNotify
自动关联目录:属性复制条件与 RepNotify
"该同步什么"靠注册,DOREPLIFETIME 决定发给谁,OnRep 决定收到后做什么。这三层里任意一层配错,表现都是"客户端数值不对",但原因完全不同。
一句话定位:属性复制是"变化才发",OnRep 只在客户端触发。记住后半句能省掉一半的调试时间。
最小可用写法
UPROPERTY(ReplicatedUsing = OnRep_Health)
float Health;
void AMyActor::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutProps) const
{
Super::GetLifetimeReplicatedProps(OutProps);
DOREPLIFETIME(AMyActor, Health);
}
void AMyActor::OnRep_Health()
{
// 只有客户端会跑
}漏掉 GetLifetimeReplicatedProps 里的注册,属性就完全不复制——这是最常见也最容易漏的一步(没有编译错误,纯运行时失效)。
COND_ 条件
| 条件 | 发给谁 |
|---|---|
COND_None(默认) | 所有相关连接 |
COND_OwnerOnly | 只发给拥有者 |
COND_SkipOwner | 发给所有人除了拥有者(配合本地预测:自己的动作本地已演算,不用回传) |
COND_InitialOnly | 只在初始复制时发一次(名字、配置) |
COND_SimulatedOnly | 只发给模拟端(非自主端) |
COND_AutonomousOnly | 只发给自主端(拥有者) |
COND_ReplayOrOwner | 回放或拥有者 |
COND_SkipReplay | 回放时跳过 |
COND_Custom | 用 SetCustomIsActiveOverride 自己判断 |
void AMyActor::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutProps) const
{
Super::GetLifetimeReplicatedProps(OutProps);
DOREPLIFETIME_CONDITION(AMyActor, AmmoInMag, COND_OwnerOnly); // 弹匣只有自己需要知道
DOREPLIFETIME_CONDITION(AMyActor, DisplayName, COND_InitialOnly);
DOREPLIFETIME_CONDITION(AMyActor, ThirdPersonAnimState, COND_SkipOwner);
}
// 动态条件
void AMyActor::PreReplication(IRepChangedPropertyTracker& ChangedPropertyTracker)
{
Super::PreReplication(ChangedPropertyTracker);
DOREPLIFETIME_ACTIVE_OVERRIDE(AMyActor, SecretData, bHasIntel);
}COND_SkipOwner + 本地预测是标配:自己开枪的表现本地已经播了,服务端再回传一次会造成重复播放。
OnRep 的三种模式
void AMyActor::OnRep_Health(float OldHealth)
{
// UE 5.x 支持带旧值参数,便于做差值表现
}| 模式 | 说明 |
|---|---|
REPNOTIFY_OnChanged(默认) | 值真的变了才触发 |
REPNOTIFY_Always | 每次收到都触发,即使值相同 |
REPNOTIFY_InitialOnly | 只在初始那次触发 |
UPROPERTY(ReplicatedUsing = OnRep_Health)
float Health;
void AMyActor::OnRep_Health()
{
// GAS 的写法,见 [UAttributeSet](/76cb5781.html)
}什么时候用 REPNOTIFY_Always:当客户端做过钳制(PreAttributeChange 改了值)导致与服务端值不一致时,需要强制再触发一次回调来刷新表现。
服务端不会触发 OnRep
这是属性复制最大的认知陷阱:OnRep 是"收到网络包后的回调",服务端没有"收到"这一步。
void AMyActor::SetHealth(float NewValue)
{
Health = FMath::Clamp(NewValue, 0.f, MaxHealth); // 服务端改
if (HasAuthority())
{
OnRep_Health(); // 必须手动调一次,否则服务端没有表现逻辑
}
}HasAuthority() 是判断服务端的正确方式(不是 IsServer(),也不是 Role 枚举)。
数组复制:Fast Array
普通 TArray 复制是整体重发:改一个元素,整个数组序列化一遍。
// 增量复制:只发变化的项
USTRUCT()
struct FMyItem : public FFastArraySerializerItem
{
GENERATED_BODY()
UPROPERTY() int32 Id;
UPROPERTY() int32 Count;
};
USTRUCT()
struct FMyItemList : public FFastArraySerializer
{
GENERATED_BODY()
UPROPERTY() TArray<FMyItem> Items;
bool NetDeltaSerialize(FNetDeltaSerializeInfo& DeltaParms)
{
return FFastArraySerializer::FastDeltaSerialize<FMyItem, FMyItemList>(Items, DeltaParms, *this);
}
};
template<>
struct TStructOpsTypeTraits<FMyItemList> : public TStructOpsTypeTraitsBase2<FMyItemList>
{
enum { WithNetDeltaSerializer = true };
};| 场景 | 用哪个 |
|---|---|
| 少量元素、变化不频繁 | 普通 TArray |
| 背包、Buff 列表、排行榜 | FastArray |
FastArray 的代价:实现复杂、序列化顺序敏感、删除项要标 bIsValid 或走 MarkItemDirty。只有列表真的大/真的频繁才值得上。
Push Model(按需标记脏)
默认引擎每帧比较所有复制属性。Push Model 让你显式标记,省掉比较开销。
// 开启(DefaultEngine.ini 或命令行)
net.IsPushModelEnabled=1
// 代码
#include "Net/Core/PushModel/PushModel.h"
MARK_PROPERTY_DIRTY_FROM_NAME(AMyActor, Health, this);收益与项目规模成正比:几百个复制属性的 Actor 才有明显效果,小项目不值得引入这个复杂度。
能复制什么
| 能 | 说明 |
|---|---|
标量、FString、FName | 直接支持 |
USTRUCT | 成员要 UPROPERTY |
TArray / TMap / TSet | 整体重发 |
UObject* | 必须是可网络寻址的(Stably Named) |
TSubclassOf | 支持 |
FVector_NetQuantize | 量化后更省 |
| 不能 | 说明 |
|---|---|
| 裸指针(非 UObject) | 无网络地址 |
委托 / TFunction | 不可序列化 |
未标 UPROPERTY 的字段 | 对引擎不存在(见 反射) |
带宽控制
| 手段 | 说明 |
|---|---|
NetUpdateFrequency | 每秒复制几次,默认 100 很高 |
MinNetUpdateFrequency | 无变化时的低频 |
bOnlyRelevantToOwner | 只发给拥有者 |
NetCullDistanceSquared | 距离剔除 |
| 量化类型 | FVector_NetQuantize* |
COND_* | 见上文 |
ForceNetUpdate() | 有重要变化立刻发(不要滥用) |
把 NetUpdateFrequency 从默认 100 降到 10–20,通常是收益最大的一次优化(见 Optimization)。
常见坑
| 坑 | 说明 |
|---|---|
忘了 DOREPLIFETIME 注册 | 静默不复制 |
以为服务端也会触发 OnRep | 不会,要手动调 |
| 客户端改复制属性 | 会被服务端覆盖,产生抖动 |
| 用 RPC 同步持续状态 | 应该用属性复制 |
大数组用普通 TArray | 整体重发,带宽爆炸 |
| 复制了不需要的字段 | 降不下来的带宽 |
OnRep 里做权威判定 | 只有客户端跑 |
复制属性忘了 UPROPERTY | 编译可能过,但不复制 |
FastArray 改了元素不 MarkItemDirty | 变化不发 |
许可协议:CC BY
作者:Davids
本文链接:https://hustjjd.github.io/b3e69e6c.html
更新于:2026年10月10日