尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
UE网络同步实战:RPC与属性同步核心机制及避坑指南
1. 从一次联机延迟说起为什么RPC和属性同步是UE网络层的两根支柱做过UE联机项目的人大概都经历过这种场景四个人开黑打Boss主机玩家看到Boss已经倒地客机玩家那边Boss还在挥刀等了两秒才“啪”一下瞬移倒地。或者更离谱的队友明明站在你左边你开枪打他身后的敌人结果判定你打中了队友。这类问题十有八九不是逻辑写错了而是RPC调用时机和属性同步策略没配对。UE的网络模型本质上是服务器权威Server Authoritative的。也就是说服务器是唯一说了算的那台机器客户端所有的操作都要先告诉服务器服务器算完了再把结果同步回来。这个“告诉”的过程一部分靠RPCRemote Procedure Call远程过程调用一部分靠属性同步Property Replication。前者负责“事件”的传递比如开火、放技能、捡东西后者负责“状态”的持续同步比如血量、位置、弹药数。很多人刚接触这块的时候会有一个误区觉得RPC和属性同步是两套可以互相替代的东西。实际上它们解决的是完全不同的问题。RPC是一次性、瞬时的属性同步是持续性、状态性的。你把开火做成属性同步那每次开火都要改一个bool再改回来网络开销大不说还容易丢事件你把血量做成RPC那新加入的玩家就不知道当前血量是多少因为RPC只发一次不会补发历史。这篇文章我会从实际项目出发把RPC的几种类型、调用规则、可靠性机制以及属性同步的注册流程、条件同步、带宽优化全部拆开讲一遍。中间会穿插我自己踩过的坑比如在客户端调用Server RPC导致崩溃、属性同步没加Replicated导致联机看不到、RepNotify里做重逻辑导致卡顿等等。适合已经写过一些UE联机逻辑、但对底层机制还不太清楚的开发者也适合想系统梳理一遍网络同步知识的人。2. RPC全解析三种类型、调用规则与可靠性选择2.1 RPC的三种类型到底怎么选UE把RPC分成了三种按“谁调用、谁执行”来区分RPC类型声明关键字调用方执行方典型场景Server RPCServer客户端服务器开火、使用道具、请求交互Client RPCClient服务器指定客户端播放特效、显示UI提示、本地反馈Multicast RPCNetMulticast服务器服务器所有客户端爆炸特效、全局音效、公告这里有一个非常关键的规则Server RPC只能由客户端调用在服务器执行Client RPC只能由服务器调用在客户端执行Multicast只能由服务器调用在所有端执行。如果你在服务器上调用一个Server RPC它不会报错但会直接本地执行相当于普通函数。如果你在客户端调用Client RPC同样不会报错也是本地执行。这种“静默失败”是最坑的因为逻辑看起来跑了但网络同步完全没发生。我见过一个典型错误某开发者在客户端写了一个Server_OpenDoor然后在服务器上又调了一次结果服务器自己开门了客户端那边也开门了看起来没问题。但后来发现如果服务器不主动调客户端调用根本传不到服务器因为客户端调用的Server RPC在服务器上执行时如果那个Actor在客户端没有权限就会被丢弃。所以一定要记住RPC的调用端和执行端是固定的不能乱来。2.2 RPC的声明与实现规范在UE里声明RPC需要在函数声明前加关键字并且函数必须声明在UCLASS里通常是AActor或UActorComponent的子类。一个标准的Server RPC长这样UFUNCTION(Server, Reliable, WithValidation) void Server_FireWeapon();这里有三个关键点Server声明这是Server RPC。Reliable声明这是可靠RPC保证一定到达但会增加网络开销。WithValidation声明需要验证函数服务器会先调用Server_FireWeapon_Validate()返回true才执行实际逻辑。实现的时候你需要在.cpp里写两个函数bool AMyWeapon::Server_FireWeapon_Validate() { return true; // 这里可以加校验比如检查弹药是否足够 } void AMyWeapon::Server_FireWeapon_Implementation() { // 实际的开火逻辑比如生成弹道、扣弹药 }注意_Implementation后缀是必须的_Validate只有在WithValidation时才需要。如果你忘了写_Implementation编译会报错这个还好。但如果你忘了写_Validate编译也会报错但错误信息可能不太直观会提示你找不到验证函数。2.3 Reliable还是Unreliable一个容易被忽视的决策RPC的可靠性选择直接影响网络带宽和游戏体验。很多人图省事所有RPC都加Reliable结果带宽爆了或者因为可靠RPC的排队机制导致延迟。Reliable RPC的特点是保证到达按顺序执行如果丢包会重传。但代价是如果某个可靠RPC因为网络问题卡住了后面所有的可靠RPC都会排队等它这就是所谓的队头阻塞。比如你发了一个“开火”的可靠RPC结果丢包了服务器没收到客户端会一直重传直到收到为止。这期间如果玩家又发了“换弹”的可靠RPC它必须等“开火”重传成功后才能发出去。Unreliable RPC的特点是不保证到达不保证顺序丢了就丢了。适合那些“丢了也无所谓”的场景比如播放一个特效、更新一个临时的UI提示。我的经验是影响游戏逻辑的用Reliable纯表现的用Unreliable。比如开火、造成伤害、拾取物品这些必须可靠而播放枪口火焰、播放音效、显示飘字这些可以用Unreliable丢了最多是视觉上少一下不影响胜负。还有一个细节Multicast RPC默认是Unreliable的如果你需要可靠的Multicast必须显式加Reliable。但可靠的Multicast开销很大因为它要保证每个客户端都收到服务器会为每个客户端维护一个可靠队列。所以除非是“游戏结束”、“Boss死亡”这种全局事件否则不要用可靠的Multicast。2.4 RPC的调用时机与Actor权限RPC能不能调用成功还取决于Actor的权限Role。UE里每个Actor都有一个Role属性常见的有ROLE_Authority服务器上的Actor或者客户端上被服务器授权控制的Actor。ROLE_AutonomousProxy客户端上自己控制的Pawn。ROLE_SimulatedProxy客户端上其他玩家控制的Pawn。Server RPC只能从ROLE_AutonomousProxy或ROLE_Authority调用。也就是说你只能控制自己的角色去调用Server RPC不能控制别人的角色。如果你在客户端上试图让一个ROLE_SimulatedProxy的Actor调用Server RPC服务器会直接忽略。Client RPC只能从ROLE_Authority调用也就是只能在服务器上调用。如果你在客户端调用Client RPC它会在本地执行但不会传到其他客户端。Multicast RPC也只能从ROLE_Authority调用也就是只能在服务器上调用。这里有一个常见的坑在客户端上GetOwner()-HasAuthority()返回false但GetOwner()-GetLocalRole()可能是ROLE_AutonomousProxy。所以判断能不能调用Server RPC不能只看HasAuthority()还要看GetLocalRole()。我一般会写一个辅助函数bool CanCallServerRPC() const { return GetLocalRole() ROLE_AutonomousProxy || GetLocalRole() ROLE_Authority; }2.5 RPC的序列化与参数传递RPC的参数会被序列化后通过网络发送所以参数类型必须支持序列化。基本类型int、float、bool、FString、FVector等都支持但自定义的USTRUCT需要加USTRUCT标记并且成员变量要是UPROPERTY。有一个容易忽略的点RPC的参数不能是指针也不能是UObject*的裸指针。如果你需要传递一个对象应该传AActor*或UActorComponent*UE会自动处理引用。但要注意如果那个对象在客户端不存在参数会是nullptr。另外RPC的参数数量不宜过多因为每个参数都会增加序列化开销。我见过一个RPC传了十几个参数结果带宽直接爆炸。如果参数太多可以考虑打包成一个USTRUCT或者只传关键参数其他靠属性同步。3. 属性同步从注册到条件同步的完整链路3.1 属性同步的基本流程属性同步的核心是UPROPERTY(Replicated)。你只需要在变量声明前加上这个标记UE就会自动把这个属性同步到所有客户端。但前提是这个属性所在的Actor必须满足Actor本身是Replicated的也就是在BeginPlay里调用了SetReplicates(true)或者在蓝图里勾选了“Replicates”。Actor的NetOwner正确设置通常是玩家控制器。属性在服务器上被修改并且修改发生在ROLE_Authority上。一个最简单的例子UCLASS() class AMyCharacter : public ACharacter { GENERATED_BODY() public: UPROPERTY(Replicated) float Health; virtual void GetLifetimeReplicatedProps(TArrayFLifetimeProperty OutLifetimeProps) const override; }; void AMyCharacter::GetLifetimeReplicatedProps(TArrayFLifetimeProperty OutLifetimeProps) const { Super::GetLifetimeReplicatedProps(OutLifetimeProps); DOREPLIFETIME(AMyCharacter, Health); }这里GetLifetimeReplicatedProps是必须重写的否则属性不会同步。DOREPLIFETIME宏会把属性注册到同步列表里。3.2 Replicated vs ReplicatedUsing什么时候用哪个Replicated只是单纯同步值客户端收到新值后不会触发任何回调。如果你需要在客户端收到新值时做一些事情比如更新UI、播放特效就需要用ReplicatedUsingUPROPERTY(ReplicatedUsing OnRep_Health) float Health; UFUNCTION() void OnRep_Health();OnRep_Health会在客户端收到新值时被调用。注意OnRep函数只在客户端调用服务器不会调用。而且OnRep函数不能有参数除非你用了ReplicatedUsing的高级用法。这里有一个大坑OnRep函数里不要做重逻辑。因为属性同步是每帧可能发生的如果OnRep里做了复杂的计算或者生成了大量Actor会导致客户端卡顿。我见过有人在OnRep_Health里播放死亡动画、生成掉落物、更新整个UI结果一局下来客户端帧率掉到20。正确的做法是OnRep里只做轻量的标记比如设置一个bHealthChanged标志然后在Tick里统一处理。3.3 条件同步COND_OwnerOnly、COND_SkipOwner等默认情况下属性会同步给所有客户端。但有些属性只需要同步给特定客户端比如玩家的手牌、私有背包。这时候就需要条件同步。UE提供了多种条件常用的有条件含义典型场景COND_None默认同步给所有客户端血量、位置COND_OwnerOnly只同步给Owner私有背包、手牌COND_SkipOwner同步给除Owner外的所有客户端第三人称模型、脚步声COND_SimulatedOnly只同步给模拟代理其他玩家的动画状态COND_AutonomousOnly只同步给自主代理自己的输入状态COND_InitialOnly只在初始同步时发送一次玩家名字、队伍ID使用方式是在GetLifetimeReplicatedProps里用DOREPLIFETIME_CONDITIONDOREPLIFETIME_CONDITION(AMyCharacter, PrivateInventory, COND_OwnerOnly);这里有一个经验COND_OwnerOnly和COND_SkipOwner要成对使用。比如你的角色有一个“是否隐身”的属性Owner自己不需要看到隐身特效但其他玩家需要看到。那么你可以把“隐身状态”用COND_SkipOwner同步给其他玩家同时用COND_OwnerOnly同步一个“自己是否知道自己在隐身”的标志。3.4 属性同步的频率与带宽优化属性同步不是每帧都发的UE有一个同步频率的概念。默认情况下属性会在NetUpdateFrequency控制的频率下同步通常是每秒10-30次。你可以在DefaultEngine.ini里调整[/Script/Engine.Actor] NetUpdateFrequency30.0但更精细的控制是在Actor级别AMyCharacter::AMyCharacter() { NetUpdateFrequency 15.0f; MinNetUpdateFrequency 2.0f; }NetUpdateFrequency是最大频率MinNetUpdateFrequency是最小频率。UE会根据网络状况和Actor的重要性动态调整。带宽优化的另一个关键是只同步必要的数据。比如位置如果你用FVector同步那是12字节如果你用FVector_NetQuantize可以压缩到更少。UE提供了多种量化类型FVector_NetQuantize压缩到1厘米精度。FVector_NetQuantize10压缩到10厘米精度。FVector_NetQuantize100压缩到1米精度。对于角色位置FVector_NetQuantize就够了对于远处的道具FVector_NetQuantize100也可以接受。还有一个技巧用Replicated的COND_InitialOnly来同步那些不会变的属性比如玩家名字、角色皮肤ID。这样只在初始同步时发一次后续不再占用带宽。4. 实操从零搭建一个可联机的开火与血量同步4.1 项目准备与基础设置假设我们有一个简单的射击游戏玩家可以开火开火会扣自己的弹药命中敌人会扣敌人血量。我们需要一个AMyCharacter有Health和Ammo属性。一个AMyWeapon有Fire()函数。服务器权威的开火逻辑。首先在AMyCharacter的构造函数里AMyCharacter::AMyCharacter() { bReplicates true; NetUpdateFrequency 30.0f; }然后声明属性UPROPERTY(ReplicatedUsing OnRep_Health) float Health; UPROPERTY(Replicated) int32 Ammo; UFUNCTION() void OnRep_Health();在GetLifetimeReplicatedProps里注册void AMyCharacter::GetLifetimeReplicatedProps(TArrayFLifetimeProperty OutLifetimeProps) const { Super::GetLifetimeReplicatedProps(OutLifetimeProps); DOREPLIFETIME(AMyCharacter, Health); DOREPLIFETIME(AMyCharacter, Ammo); }4.2 开火逻辑Server RPC 属性同步开火是一个“事件”所以用Server RPC。在AMyWeapon里UFUNCTION(Server, Reliable, WithValidation) void Server_Fire(); bool AMyWeapon::Server_Fire_Validate() { return true; } void AMyWeapon::Server_Fire_Implementation() { // 检查弹药 AMyCharacter* OwnerCharacter CastAMyCharacter(GetOwner()); if (!OwnerCharacter || OwnerCharacter-Ammo 0) { return; } // 扣弹药 OwnerCharacter-Ammo--; // 生成弹道做射线检测 FHitResult Hit; FVector Start GetActorLocation(); FVector End Start GetActorForwardVector() * 10000.0f; FCollisionQueryParams Params; Params.AddIgnoredActor(OwnerCharacter); Params.AddIgnoredActor(this); if (GetWorld()-LineTraceSingleByChannel(Hit, Start, End, ECC_Visibility, Params)) { AMyCharacter* HitCharacter CastAMyCharacter(Hit.GetActor()); if (HitCharacter) { HitCharacter-Health - 10.0f; } } // 播放开火特效用Multicast Multicast_PlayFireEffect(); }这里Ammo是属性同步服务器扣了之后会自动同步到客户端。Health也是属性同步服务器扣了之后会自动同步到被击中的客户端。4.3 客户端预测与服务器校正上面的逻辑有一个问题客户端按下开火键后要等服务器确认才能看到弹药减少。如果延迟高玩家会感觉“按了没反应”。这时候就需要客户端预测。客户端预测的基本思路是客户端先本地执行一次然后等服务器确认。如果服务器结果和本地不一致就回滚。在UE里最简单的预测是预测弹药消耗void AMyWeapon::Fire() { if (GetLocalRole() ROLE_AutonomousProxy) { // 本地预测扣弹药 AMyCharacter* OwnerCharacter CastAMyCharacter(GetOwner()); if (OwnerCharacter OwnerCharacter-Ammo 0) { OwnerCharacter-Ammo--; } } // 调用Server RPC Server_Fire(); }但这样会有问题如果服务器拒绝开火比如弹药不足客户端的预测就错了。所以需要服务器在Server_Fire_Implementation里检查如果弹药不足就发一个Client RPC让客户端回滚void AMyWeapon::Server_Fire_Implementation() { AMyCharacter* OwnerCharacter CastAMyCharacter(GetOwner()); if (!OwnerCharacter || OwnerCharacter-Ammo 0) { Client_ResetAmmo(OwnerCharacter-Ammo); return; } // ... 正常开火逻辑 } UFUNCTION(Client, Reliable) void Client_ResetAmmo(int32 ServerAmmo); void AMyWeapon::Client_ResetAmmo_Implementation(int32 ServerAmmo) { AMyCharacter* OwnerCharacter CastAMyCharacter(GetOwner()); if (OwnerCharacter) { OwnerCharacter-Ammo ServerAmmo; } }这样如果服务器发现弹药不足就会把正确的弹药数发回客户端客户端覆盖本地的预测值。4.4 血量同步与死亡处理血量同步用ReplicatedUsing在OnRep_Health里检查是否死亡void AMyCharacter::OnRep_Health() { if (Health 0.0f) { // 播放死亡动画 PlayDeathAnimation(); } else { // 更新UI UpdateHealthUI(); } }注意OnRep_Health只在客户端调用服务器不会调用。所以服务器上的死亡处理需要在扣血的地方直接调用void AMyCharacter::ApplyDamage(float Damage) { if (!HasAuthority()) { return; } Health - Damage; if (Health 0.0f) { // 服务器上的死亡处理 Die(); } }这里Die()是服务器上的逻辑比如掉落物品、通知游戏模式。客户端上的死亡表现由OnRep_Health触发。5. 常见问题与排查技巧实录5.1 RPC不执行先查这五个地方RPC不执行是最常见的问题我整理了一个排查清单问题现象可能原因排查方法Server RPC不执行调用方不是AutonomousProxy打印GetLocalRole()确认是ROLE_AutonomousProxyServer RPC不执行Actor没有Replicate检查bReplicates是否为trueClient RPC不执行调用方不是Authority打印HasAuthority()确认是服务器Multicast不执行调用方不是Authority同上RPC执行了但没效果参数序列化失败检查参数类型是否支持序列化还有一个隐藏问题RPC在Actor的BeginPlay之前调用会失败。因为RPC需要Actor已经注册到网络系统。如果你在构造函数里调用RPC那是无效的。5.2 属性同步不生效检查这三个步骤属性同步不生效通常是这三个原因忘了重写GetLifetimeReplicatedProps。这是最常见的很多人加了Replicated标记但忘了注册。忘了调用SetReplicates(true)。Actor本身不Replicate属性也不会同步。属性在客户端修改了。属性同步是单向的只能从服务器同步到客户端。如果你在客户端改了属性服务器不会收到也不会同步给其他客户端。我见过一个案例某开发者在客户端改了Health然后发现其他客户端看不到变化。这是因为客户端没有权限修改同步属性改了也只是本地生效不会传到服务器。5.3 带宽爆炸试试这些优化手段带宽爆炸通常是因为同步了太多不必要的数据。我常用的优化手段用COND_OwnerOnly和COND_SkipOwner减少同步范围。用FVector_NetQuantize压缩位置。降低NetUpdateFrequency对于不重要的Actor可以降到5甚至2。用COND_InitialOnly同步不变属性。避免在OnRep里做重逻辑减少客户端的计算开销。还有一个技巧用NetPriority调整Actor的同步优先级。比如玩家角色的优先级是1.0道具的优先级是0.5这样网络拥堵时玩家角色的同步会更及时。5.4 预测与校正的常见坑客户端预测最怕的是预测错误后没有校正。比如你预测开火成功扣了弹药但服务器说弹药不足这时候如果不校正客户端就会显示错误的弹药数。另一个坑是预测和服务器结果不一致时的抖动。比如你预测移动到了A点服务器说你在B点如果你直接瞬移到B点玩家会看到角色“闪一下”。这时候需要用平滑插值让角色从A点平滑移动到B点。UE提供了ClientAdjustPosition和ServerMove等机制来处理移动预测但对于自定义的属性预测需要自己实现平滑校正。我的经验是对于弹药、血量这种离散值直接覆盖就行对于位置、旋转这种连续值一定要做插值。5.5 一个容易被忽略的细节NetOwnerNetOwner决定了COND_OwnerOnly和COND_SkipOwner的“Owner”是谁。对于玩家角色NetOwner通常是PlayerController。如果你没有正确设置NetOwner条件同步就会失效。设置NetOwner的方式是重写GetNetOwner()AActor* AMyCharacter::GetNetOwner() const { return GetController(); }对于PawnUE默认会返回Controller所以一般不用手动设置。但如果你有自定义的Actor比如一个“宠物”它的NetOwner应该是主人这时候就需要手动设置。6. 进阶RPC与属性同步的混合使用策略6.1 什么时候用RPC什么时候用属性同步这个问题我思考了很久最后总结出一个简单的判断标准如果是一个“事件”用RPC。比如开火、跳跃、使用技能。如果是一个“状态”用属性同步。比如血量、弹药、位置。如果是一个“状态变化”用属性同步OnRep。比如血量变化后播放受击特效。如果是一个“瞬时表现”用Unreliable Multicast。比如枪口火焰、弹壳掉落。但有些情况是模糊的。比如“开门”这个动作它既是一个事件门开了也是一个状态门是开着的。我的做法是用属性同步同步门的开关状态用RPC播放开门动画。这样新加入的玩家能看到门是开着的而开门动画只在开门瞬间播放。6.2 属性同步的“批量更新”技巧如果你有多个属性需要同时更新比如一个“角色状态”结构体包含血量、弹药、能量你可以把它们打包成一个USTRUCT然后用一个Replicated同步USTRUCT() struct FCharacterState { GENERATED_BODY() UPROPERTY() float Health; UPROPERTY() int32 Ammo; UPROPERTY() float Energy; }; UPROPERTY(Replicated) FCharacterState CharacterState;这样只需要一个同步通道减少开销。但缺点是任何一个字段变化都会同步整个结构体。所以如果字段变化频率差异很大比如血量变化频繁能量变化很少那还是分开同步更好。6.3 用RPC做“可靠事件”用属性同步做“最终一致”在一些对一致性要求很高的场景比如“拾取物品”我通常会用RPC做可靠事件用属性同步做最终一致。具体做法是客户端调用Server_PickupItem这是一个Reliable RPC。服务器验证后把物品的Owner改成玩家这是一个属性同步。服务器发一个Multicast_PlayPickupEffect播放拾取特效。这样即使RPC丢了虽然Reliable不会丢属性同步也会保证最终物品的归属是正确的。而特效丢了也无所谓不影响逻辑。6.4 网络调试的实用工具UE提供了一些网络调试工具我常用的有NetShowCorrections显示预测校正的信息。Stat Net显示网络统计信息包括带宽、RPC数量、属性同步数量。NetDebug打开网络调试面板可以查看每个Actor的同步状态。LogNet打开网络日志可以看到RPC调用和属性同步的详细日志。我一般在DefaultEngine.ini里加[Core.Log] LogNetVerbose这样可以在输出日志里看到RPC的调用和属性同步的细节。但注意Verbose日志量很大只在调试时开。7. 个人经验那些文档里不会写的坑7.1 RPC的“静默失败”最致命UE的RPC在调用失败时不会报错也不会打日志就是静默地什么都不做。这导致调试非常困难。我的做法是在RPC的_Implementation里加一个日志void AMyWeapon::Server_Fire_Implementation() { UE_LOG(LogTemp, Warning, TEXT(Server_Fire called on %s), *GetName()); // ... }这样至少能知道RPC有没有执行。如果日志没打那就是RPC没传到。7.2 属性同步的“初始同步”陷阱新加入的客户端会收到所有Replicated属性的初始值。但如果你在BeginPlay里修改了属性而BeginPlay在初始同步之后执行那客户端收到的就是修改前的值。所以属性的初始值应该在构造函数里设置而不是BeginPlay。我见过一个案例某开发者在BeginPlay里设置Health 100结果新加入的客户端看到的是Health 0因为初始同步发生在BeginPlay之前。后来他把Health 100移到构造函数里问题就解决了。7.3 Multicast的“顺序”问题Multicast RPC不保证顺序也不保证到达。如果你发了两个Multicast一个播放特效A一个播放特效B客户端可能先收到B再收到A。如果你的逻辑依赖顺序比如先播放开门动画再播放门开的声音那就需要用Reliable Multicast或者用属性同步来保证顺序。7.4 属性同步的“频率”与“延迟”权衡属性同步的频率越高延迟越低但带宽越大。我一般会根据Actor的重要性来调整玩家角色NetUpdateFrequency 30重要道具NetUpdateFrequency 15背景装饰NetUpdateFrequency 2这样可以在带宽和体验之间找到平衡。7.5 一个实用的小技巧用NetMulticast做“本地反馈”有时候你需要在客户端立即看到反馈但又不想等服务器。比如开火时客户端可以立即播放枪口火焰同时发Server RPC。如果服务器确认开火成功再发Multicast让其他客户端也看到。这样自己感觉是“即时”的其他玩家看到的是“同步”的。void AMyWeapon::Fire() { // 本地立即播放特效 PlayLocalFireEffect(); // 发Server RPC Server_Fire(); } void AMyWeapon::Server_Fire_Implementation() { // 服务器逻辑 // ... // 让其他客户端播放特效 Multicast_PlayFireEffect(); }这样自己看到的特效是本地预测的其他玩家看到的特效是服务器同步的。如果服务器拒绝开火自己的特效已经播了但其他玩家看不到这算是一个小瑕疵但可以接受。7.6 关于ReplicatedUsing的“重复调用”问题OnRep函数在属性值变化时会被调用但如果属性值没有变化OnRep不会被调用。但有时候你希望即使值没变也执行一次OnRep比如初始化UI。这时候可以在BeginPlay里手动调用一次OnRepvoid AMyCharacter::BeginPlay() { Super::BeginPlay(); if (IsLocallyControlled()) { OnRep_Health(); } }但注意OnRep_Health里不要做依赖网络状态的逻辑因为手动调用时可能网络还没准备好。7.7 最后再分享一个调试技巧如果你怀疑某个属性没有同步可以在客户端用DisplayAll命令查看所有Actor的同步属性DisplayAll AMyCharacter Health这个命令会显示所有AMyCharacter的Health值包括服务器和客户端的。如果服务器和客户端的值不一致那就是同步出了问题。另外NetDebug面板里的“Replication”标签可以查看每个Actor的同步状态包括哪些属性被同步了同步频率是多少。这个面板在调试网络问题时非常有用。网络同步这块说到底就是“服务器说了算客户端做预测RPC传事件属性传状态”。把这两条线理清楚大部分联机问题都能定位到。我自己的经验是遇到同步问题先别急着改代码先用Stat Net和NetDebug看看数据到底有没有发出去往往问题就出在“以为发了但没发”上。
RELATED

相关推荐

Envoy + Go 轻量级控制面实战:从配置模型到动态下发

Envoy + Go 轻量级控制面实战:从配置模型到动态下发

1. 项目背景与整体思路拆解1.1 为什么是 Envoy Go最近在做一个跨部门的基础能力整合,微服务数量从几十个涨到几百个之后,东西向流量治理这件事被提到了台面上。以前靠网关统一收口南北流量就能应付,但服务间直接调用的场景越来越多&#xff…

📅 2026/10/12 5:52:42
覆盖率跑了三轮还是不累加:DevEco Studio 26.0 增量覆盖率怎么防止构建身份混用

覆盖率跑了三轮还是不累加:DevEco Studio 26.0 增量覆盖率怎么防止构建身份混用

覆盖率跑了三轮还是不累加:DevEco Studio 26.0 增量覆盖率怎么防止构建身份混用 DevEco Studio 26.0 Release 新增 Instrument、Local 和黑盒测试的增量覆盖率统计。增量并不等于把任意三份报告相加:源码、构建、插桩模式、过滤规则或用例集合改变后&am…

📅 2026/10/12 5:52:42
2026年网络安全5大高薪方向:云安全、AI安全、数据安全等全解析

2026年网络安全5大高薪方向:云安全、AI安全、数据安全等全解析

转眼又到年底,朋友圈里同行们聊得最多的已经不是“哪个新漏洞又刷屏了”,而是“明年该往哪个方向使劲”。我在这行摸爬滚打了十几年,从最早的杀毒软件、防火墙,到后来的渗透测试、安全合规,再到这几年频繁接触云原生和…

📅 2026/10/12 5:52:42
MORE NEWS

更多资讯

📰

第39天打卡复盘:习惯半成品期如何稳住不崩

2026年3月2日,Day39。早上六点半,我在打卡表上划下第39个勾,顺手把昨晚写好的一篇短文归档。窗外天亮得比一个月前早了不少,能明显感觉到春天在靠近。这个时刻其实挺微妙的——既不是刚刚开始那种打鸡血的第7天,也不是…

📰

大模型权重开源上线前自查清单:六项关键检查与实操指南

1. 权重上线前为什么需要一份自查清单模型权重开源这件事,看起来只是把文件打包上传,实际上它更像是一次面向全世界的"交付"。你交出去的不只是几十上百GB的参数文件,还有一整套隐含的契约:别人下载之后能不能顺利加载、…

📰

企业级智能客服系统实战:Spring AI下RAG、工具调用与流式输出架构全复盘

做了十期企业智能客服项目,终于到收官篇了。回头数数,这个系统从最初只能接一条问答,到后来能翻知识库、查订单、记上下文、逐字打字,再到各种异常情况下的兜底策略,每一步拆出来都值得单独聊聊。这十期里我最大的感受…

📰

OpenClaw 提示词全集:把 Prompt Collection 改到 TaoToken 的配置清单

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

HarmonyOS 6从零开发简易计数器

HarmonyOS 6 的开发者生态起来之后,我身边不少转鸿蒙开发的朋友都在问同一个问题:该从哪个项目下手最合适?我的建议通常很简单——先做一个简易计数器。别小看这个项目,它几乎覆盖了 ArkTS 状态管理的基础玩法、ArkUI 声明式写法的…

📰

DB2 V11.1下载安装避坑指南:老版本为何仍是运维必选项

简介:DB2 V11.1 是 IBM 推出的企业级关系型数据库管理系统,这份 Linux 版安装压缩包专为需要稳定、安全数据存储环境的中大型企业及系统管理员、DBA 设计,可用于生产或测试环境的快速部署。包内共 405 个文件,包含 174 个 cat 消息…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬