3、Replication基础:Actor复制、属性复制(Replicated)、组件复制、复制条件
好,咱们今天来啃网络同步里最基础、也最绕不开的一块硬骨头——Replication。说白了,就是「怎么让服务器和客户端看到的东西保持一致」。我刚开始做多人游戏那会儿,以为只要把角色放上去就能自动同步,结果一跑起来,客户端A看到B在天上飞,B看到A卡在墙里……嗯,从那以后我再也不敢小看这个基础了。
3.1 Actor复制:让对象在网络上「活」起来
先问个问题:一个Actor什么时候会出现在客户端?答案是——只有当它被「复制」过去的时候。Actor复制是网络同步的基石,你想想看,如果连对象本身都不存在,那它的位置、血量、动画就更别提了。
在Unreal里,控制Actor是否复制的开关就一个布尔值:bReplicates。把这个设为true,服务器就会尝试把这个Actor广播到所有相关客户端。我个人习惯在构造函数里就把它设好:
// 在构造函数中
bReplicates = true;
但注意,光设这个还不够。你还需要告诉引擎「谁该看到这个Actor」。这就引出了另一个关键属性——bOnlyRelevantToOwner。
核心区别:
bReplicates = true:Actor参与复制,但具体复制给谁,由相关性(Relevance)决定。bOnlyRelevantToOwner = true:只复制给拥有者(Owner),其他客户端看不到。典型例子就是玩家身上的UI提示、个人专属的拾取物。
我在项目中遇到过一个问题:一个爆炸特效Actor,我设了bReplicates = true,结果所有客户端都看到了,但爆炸位置在服务器上是对的,在客户端上却偏移了。后来发现是因为我没有设置SetReplicates的时机——在Spawn之后才设,导致第一帧的位置没同步过去。所以我的建议是:在Spawn之前就把复制属性设好,或者用UWorld::SpawnActor<>的模板参数直接指定。
3.2 属性复制(Replicated):让数据自动「跑」到客户端
Actor复制只是第一步。真正让游戏动起来的,是属性复制。比如玩家的血量、子弹数量、开门状态——这些数据需要从服务器同步到客户端。
在Unreal里,标记一个属性为「需要复制」很简单,用UPROPERTY(Replicated)宏就行:
UPROPERTY(Replicated)
float Health;
UPROPERTY(Replicated)
int32 AmmoCount;
但光标记还不够,你还需要实现一个函数:GetLifetimeReplicatedProps。这个函数告诉引擎「哪些属性要复制,以及怎么复制」。我见过不少新手忘了这一步,结果属性死活不同步。
void AMyCharacter::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(AMyCharacter, Health);
DOREPLIFETIME(AMyCharacter, AmmoCount);
}
我的小技巧:如果你希望某个属性只在变化时才同步(而不是每帧都发),可以用DOREPLIFETIME_CONDITION配合条件枚举。比如血量变化时才通知客户端,能省不少带宽。
属性复制还有一个重要概念——复制通知(RepNotify)。当客户端收到属性更新时,你可以指定一个函数自动被调用。比如血量变了,播放受伤动画:
UPROPERTY(ReplicatedUsing = OnRep_Health)
float Health;
UFUNCTION()
void OnRep_Health()
{
// 客户端上播放受伤特效
PlayDamageEffect();
}
我曾经踩过一个坑:在OnRep_Health里直接修改了Health的值,结果触发了死循环。因为OnRep是在属性变化后调用的,如果你在里面又改了属性,服务器会再次下发……嗯,所以记住:RepNotify里只做表现,别改数据。
3.3 组件复制:让子对象也参与同步
有时候,我们不想复制整个Actor,只想复制它身上的某个组件。比如一个武器组件、一个背包组件。Unreal支持组件级别的复制,但需要手动开启。
首先,在组件的构造函数里设置:
UActorComponent::UActorComponent()
{
bReplicates = true;
}
然后,在Actor的GetLifetimeReplicatedProps里,你不需要为组件属性单独写DOREPLIFETIME——组件自己的属性复制由组件自己管理。但要注意:组件必须被Actor拥有,且Actor本身也要开启复制。
我个人的经验是:组件复制适合「功能模块化」的场景。比如一个UHealthComponent,它管理血量、护盾、回复逻辑。把这个组件复制到客户端,客户端就能直接读取血量数值,而不需要Actor再转发一遍。
注意:组件复制不是万能的。如果组件里持有大量动态数据(比如数组、地图),每次同步都会产生不小的开销。我曾经把一个TArray<FItemData>放在组件里并标记为Replicated,结果每次背包变化都卡顿。后来改成只同步「变化的部分」,用RPC通知客户端增量更新,才解决问题。
3.4 复制条件:控制「谁看到什么」
最后,我们来聊聊复制条件。说白了,就是「什么时候复制、复制给谁」。Unreal提供了几种条件枚举,最常用的有:
| 条件宏 | 含义 | 典型场景 |
|---|---|---|
COND_None |
无条件复制(默认) | 全局状态,如游戏时间 |
COND_OwnerOnly |
只复制给拥有者 | 玩家自己的弹药、分数 |
COND_SkipOwner |
跳过拥有者,复制给其他人 | 其他玩家的位置(自己不需要) |
COND_InitialOnly |
只在初始连接时复制一次 | 地图名称、固定配置 |
使用方式很简单:
DOREPLIFETIME_CONDITION(AMyCharacter, Health, COND_OwnerOnly);
DOREPLIFETIME_CONDITION(AMyCharacter, Location, COND_SkipOwner);
这里有个容易忽略的点:COND_OwnerOnly和bOnlyRelevantToOwner有什么区别?前者是属性级别的控制,后者是Actor级别的控制。如果你只想让某个属性(比如「当前瞄准的敌人」)只发给拥有者,用COND_OwnerOnly;如果你想让整个Actor只出现在拥有者视野里,用bOnlyRelevantToOwner。
避坑指南:我曾经在做一个FPS游戏时,把玩家的「准星位置」属性设成了COND_OwnerOnly,结果其他客户端看不到这个玩家的瞄准方向,导致击杀回放里准星是歪的。后来改成COND_None,但只在回放模式下才同步,平时还是OwnerOnly——用条件分支来平衡性能和功能。
小结
好了,这一章的内容就这些。总结一下:
- Actor复制是基础,
bReplicates决定是否参与复制,bOnlyRelevantToOwner控制可见范围。 - 属性复制用
UPROPERTY(Replicated)标记,配合GetLifetimeReplicatedProps注册,RepNotify做表现。 - 组件复制需要组件自身开启
bReplicates,适合模块化功能。 - 复制条件用
DOREPLIFETIME_CONDITION精细控制,避免带宽浪费。
下一章我们会深入RPC(远程过程调用),看看怎么让客户端主动告诉服务器「我开枪了」。嗯,那才是真正让游戏互动起来的关键。咱们下章见。