UE5 RPC深度解析:从原理到实战,构建多人游戏网络通信基石
1. 项目概述为什么UE5 RPC是多人游戏开发的基石在UE5里折腾多人游戏最绕不开的一个核心概念就是RPC。你可能已经看过官方文档里那句简单的定义“远程过程调用允许客户端和服务器之间相互调用函数”。但真正上手时你会发现从“知道”到“能用”再到“用好”中间隔着无数个坑。比如为什么我客户端调用的函数服务器没反应为什么我看到的物体位置和队友看到的不一样为什么有时候会报一些奇怪的网络错误这些问题本质上都和对RPC的理解深度有关。RPC不是UE5的独创它是分布式系统里一个老生常谈的通信模型。但在游戏里尤其是UE引擎的语境下它被赋予了更多游戏特有的语义和约束。简单来说它解决了游戏世界状态同步中最根本的一个问题如何让一个机器上的逻辑改变安全、可靠、有序地反映到另一个机器上。你按下开火键这个动作发生在你的电脑客户端上但判定是否击中、计算伤害、更新所有玩家血量的权威逻辑必须在服务器上执行。RPC就是连接“按下按钮”这个本地事件和“执行伤害计算”这个远程权威逻辑的桥梁。这个项目我们就从一个最纯粹的“Hello World”级别的RPC示例开始但目标绝不止于让它跑起来。我会带你深入UE5网络层的内部拆解RPC的三种类型Server、Client、Multicast各自的应用场景、执行规则和底层原理并结合实际开发中高频出现的陷阱比如网络条件预测、可靠性与非可靠性的抉择、RPC与属性复制的配合等给出可落地的解决方案。无论你是刚接触UE5网络编程的新手还是已经踩过一些坑想寻求更优解的中级开发者这篇详解都能帮你建立起清晰、稳固的RPC知识体系让你在开发多人功能时心里更有底。2. RPC核心机制与三种类型深度解析理解RPC首先要抛弃“函数调用就是函数调用”的单机思维。在网络上每一次函数调用都是一次“事件”的传递它涉及序列化、网络传输、反序列化、排队和执行等一系列复杂过程。UE5的RPC机制为我们封装了这些复杂性但我们必须理解它定下的规则否则就会写出看似能运行实则漏洞百出的网络代码。2.1 执行权限与网络角色一切的基础在UE5的网络模型中每个连接过来的玩家都有一个APlayerController而每个Actor都有一个NetOwner网络拥有者的概念。但更底层、更决定性的属性是Role角色和RemoteRole远程角色。它们是一个枚举主要有以下几种值ROLE_Authority: 权威端。对于某个Actor来说在哪个机器上拥有这个值哪个机器就对这个Actor的状态有最终决定权。服务器Dedicated Server或Listen Server上几乎所有的Actor都是ROLE_Authority。ROLE_AutonomousProxy: 自主代理。这是本地玩家控制的Pawn所扮演的角色。它在客户端上运行可以接收本地输入并立即做出反应如移动预测但其状态的最终仲裁权仍在服务器。ROLE_SimulatedProxy: 模拟代理。这是其他玩家控制的Pawn或服务器上非权威的Actor在客户端上所扮演的角色。它们的状态完全通过属性复制从服务器同步过来客户端不能直接驱动其逻辑。RPC的调用方向严格受到这些角色的制约。你无法从一个ROLE_SimulatedProxy的Actor上调用一个需要ROLE_Authority权限的Server RPC因为这不合理——一个你只能观看的物体凭什么命令服务器做事2.2 三种RPC类型详解与应用场景UE5的RPC主要分为三类通过在函数声明前添加特定的宏来标识。1. Server RPC (服务端RPC)通过在函数声明前加UFUNCTION(Server, Reliable)或UFUNCTION(Server, Unreliable)来实现。调用者客户端拥有ROLE_AutonomousProxy或ROLE_SimulatedProxy的Actor。执行者服务器该Actor在服务器上拥有ROLE_Authority。核心规则只能在客户端调用且只在服务器上执行。这是客户端向服务器发送“请求”或“通知”的主要方式。应用场景玩家输入处理开火、跳跃、交互、聊天消息发送。客户端捕获输入后调用Server RPC通知服务器执行权威逻辑。请求状态变更玩家请求购买物品、切换武器、发起投票。客户端发起请求服务器验证后执行。关键非即时动作触发一个需要服务器验证的复杂动画或技能。函数命名惯例通常以Server_为前缀例如Server_Fire、Server_RequestRespawn。这只是一个强推荐的约定并非语法强制但遵循它能极大提升代码可读性。2. Client RPC (客户端RPC)通过在函数声明前加UFUNCTION(Client, Reliable)或UFUNCTION(Client, Unreliable)来实现。调用者服务器拥有ROLE_Authority的Actor。执行者特定的一个或多个客户端。核心规则只能在服务器调用。默认情况下它只会在该Actor的NetOwner即控制这个Actor的PlayerController所对应的客户端上执行。你可以通过UFUNCTION的Client修饰符指定Reliable或Unreliable但无法直接通过参数选择目标客户端需要通过APlayerController的RPC来实现向特定客户端发送。应用场景仅对单个客户端的反馈播放只有该玩家能看到的特效如被击中时的屏幕血渍、更新专属的HUD信息、播放私人语音。服务器权威事件通知通知客户端“你的技能冷却完毕”、“任务目标已更新”。这些信息不需要其他玩家知道。函数命名惯例通常以Client_为前缀例如Client_ShowDamageNumber、Client_OnMissionUpdated。3. Multicast RPC (多播RPC)通过在函数声明前加UFUNCTION(NetMulticast, Reliable)或UFUNCTION(NetMulticast, Unreliable)来实现。调用者服务器拥有ROLE_Authority的Actor。执行者服务器和所有已连接的客户端包括调用者的客户端。核心规则只能在服务器调用。调用后函数会在服务器本地执行一次同时通过网络发送到所有客户端在各自的机器上再执行一次。这是实现视觉/听觉效果同步和非权威逻辑同步的核心手段。应用场景爆炸、音效、粒子特效手榴弹爆炸需要在所有玩家的屏幕上同时或近似同时播放爆炸效果和声音。一次性状态变化动画播放一个门的开启动画、一个宝箱的打开动画。虽然门的“开启”状态可以通过属性复制同步但触发动画的时机用Multicast RPC更直接。游戏阶段事件广播通知所有玩家“游戏开始”、“倒计时结束”。函数命名惯例通常以Multicast_为前缀例如Multicast_PlayExplosionFX、Multicast_AnnounceGameStart。一个重要细节Multicast RPC默认是“不可靠”的。对于播放特效这类允许偶尔丢失的事件使用Unreliable可以减少网络负担。但对于“门开启”这种关键状态同步你必须使用Reliable或者将其与一个复制的布尔变量如bDoorOpened结合使用以防RPC丢失导致客户端状态不一致。2.3 可靠性与非可靠性关键的选择这是RPC声明中另一个至关重要的参数。Reliable (可靠)保证送达和顺序。如果网络包丢失底层网络层会重传确保函数最终会被执行并且执行顺序与发送顺序一致。代价是潜在的延迟和带宽占用。重传和排队可能导致“网络延迟尖峰”。何时使用关键的游戏逻辑状态改变。例如Server_PlayerDied玩家死亡、Client_GrantItem授予物品、Multicast_StartMatch开始比赛。这些事件绝对不能丢失。Unreliable (不可靠)不保证送达也不保证顺序。像UDP协议一样发出即忘。如果网络包丢失这个函数调用就永远丢失了。优点是延迟低带宽占用小。何时使用高频、非关键、允许丢失的更新。例如Server_Move每帧的移动输入因为下一帧的输入马上就来、Multicast_PlayFootstepSound脚步声丢一两个无所谓、Unreliable的客户端位置更新用于服务器端的简单感知有更高级的同步机制替代。一个常见的误区是“为了保险全用Reliable”。这会导致在网络状况不佳时大量RPC在队列中堵塞游戏响应变得极其迟钝。正确的做法是根据语义谨慎选择。3. 从零构建一个UE5 RPC示例项目理论讲得再多不如亲手实现一遍。我们接下来构建一个简单的第三人称模板项目实现一个“拍手”功能玩家按下按键在本地立即播放一个拍手动画和音效客户端预测同时通知服务器服务器验证后多播给所有玩家让大家都能看到这个玩家在拍手。3.1 项目初始化与角色设置创建项目打开UE5使用“第三人称”模板带初学者内容包创建一个新项目命名为RPCExample。模板会为我们生成一个基本的可移动角色ThirdPersonCharacter和地图。创建自定义角色类为了不破坏模板我们最好创建一个继承自ThirdPersonCharacter的C类。在内容浏览器中右键选择“新建C类”父类选择ThirdPersonCharacter命名为MyRPCCharacter。编辑角色输入打开项目设置 - 输入我们需要添加一个新的“动作映射”Action Mapping。命名为Clap并分配一个按键例如C键。3.2 C 代码实现声明与定义RPC函数打开我们创建的MyRPCCharacter.h头文件进行以下修改// MyRPCCharacter.h #pragma once #include CoreMinimal.h #include GameFramework/Character.h #include MyRPCCharacter.generated.h UCLASS() class RPCEXAMPLE_API AMyRPCCharacter : public ACharacter // 注意这里继承自ACharacter因为ThirdPersonCharacter的父链最终是ACharacter { GENERATED_BODY() public: AMyRPCCharacter(); protected: virtual void BeginPlay() override; public: virtual void Tick(float DeltaTime) override; virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override; // 输入绑定函数 void OnClapPressed(); // --- RPC 函数声明 --- // Server RPC: 客户端通知服务器“我要拍手” UFUNCTION(Server, Reliable, WithValidation) // WithValidation 用于添加参数验证函数 void Server_Clap(); // 验证函数用于Server RPC。返回false则RPC不会被调用。 bool Server_Clap_Validate(); void Server_Clap_Implementation(); // 实际的实现函数 // Multicast RPC: 服务器通知所有人“播放拍手效果” UFUNCTION(NetMulticast, Unreliable) // 播放效果允许丢失用Unreliable void Multicast_PlayClapEffects(); private: // 用于播放音效的组件 UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category Audio, meta (AllowPrivateAccess true)) class UAudioComponent* ClapAudioComponent; // 拍手音效资源引用 UPROPERTY(EditDefaultsOnly, Category Audio) class USoundBase* ClapSound; // 拍手动画蒙太奇资源引用 UPROPERTY(EditDefaultsOnly, Category Animation) class UAnimMontage* ClapAnimMontage; };接下来打开MyRPCCharacter.cpp文件实现这些函数// MyRPCCharacter.cpp #include MyRPCCharacter.h #include GameFramework/CharacterMovementComponent.h #include Components/AudioComponent.h #include Sound/SoundBase.h #include Animation/AnimMontage.h #include Net/UnrealNetwork.h // 重要必须包含这个头文件以使用网络相关功能 AMyRPCCharacter::AMyRPCCharacter() { PrimaryActorTick.bCanEverTick true; // 创建并附加音频组件 ClapAudioComponent CreateDefaultSubobjectUAudioComponent(TEXT(ClapAudioComponent)); ClapAudioComponent-SetupAttachment(RootComponent); ClapAudioComponent-bAutoActivate false; // 不自动播放 } void AMyRPCCharacter::BeginPlay() { Super::BeginPlay(); } void AMyRPCCharacter::Tick(float DeltaTime) { Super::Tick(DeltaTime); } void AMyRPCCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) { Super::SetupPlayerInputComponent(PlayerInputComponent); // 绑定我们定义的“Clap”输入动作 PlayerInputComponent-BindAction(Clap, IE_Pressed, this, AMyRPCCharacter::OnClapPressed); } void AMyRPCCharacter::OnClapPressed() { // 1. 本地立即播放动画和音效客户端预测提升响应速度 PlayClapEffects_Local(); // 2. 调用Server RPC将拍手请求发送给服务器 if (HasAuthority()) { // 如果我们在服务器上例如在Listen Server模式下自己就是主机直接执行服务器逻辑 Server_Clap_Implementation(); } else { // 在客户端上调用Server RPC Server_Clap(); } } // Server RPC 验证函数 bool AMyRPCCharacter::Server_Clap_Validate() { // 这里可以添加一些简单的验证逻辑防止作弊。 // 例如检查玩家是否还活着、是否处于可以拍手的状态、冷却时间是否已过等。 // 如果返回 falseRPC 调用会被拒绝客户端可能会收到警告。 // 本例中我们做一个最简单的总是通过的验证。 return true; } // Server RPC 实现函数 void AMyRPCCharacter::Server_Clap_Implementation() { // 服务器收到拍手请求进行权威处理。 // 这里可以添加更复杂的逻辑比如消耗体力、触发游戏事件等。 // 然后服务器广播给所有客户端播放拍手效果。 Multicast_PlayClapEffects(); } // Multicast RPC 实现函数 void AMyRPCCharacter::Multicast_PlayClapEffects_Implementation() { // 这个函数会在服务器和所有客户端上执行。 // 但是我们已经在 OnClapPressed 里在本地客户端播放过一次了。 // 直接再播放一次会导致本地播放两次效果。 // 因此我们需要判断如果这个调用是来自服务器的即不是本地客户端预测的那一次才播放效果。 // 更准确地说如果这个Actor的本地角色不是 AutonomousProxy即不是自己控制的才播放。 // 或者我们用一个更简单的方法检查 NetMode。 if (!IsLocallyControlled()) // 如果不是本地控制的角色才播放多播过来的效果 { PlayClapEffects_Local(); } // 注意服务器上也会执行这个函数。如果希望服务器上也播放效果比如用于AI感知可以在这里处理。 } // 本地播放效果的辅助函数 void AMyRPCCharacter::PlayClapEffects_Local() { // 播放动画蒙太奇 if (ClapAnimMontage GetMesh() GetMesh()-GetAnimInstance()) { GetMesh()-GetAnimInstance()-Montage_Play(ClapAnimMontage); } // 播放音效 if (ClapSound ClapAudioComponent) { ClapAudioComponent-SetSound(ClapSound); ClapAudioComponent-Play(); } }3.3 蓝图配置与资源准备代码写好了我们需要在编辑器中配置资源和测试。编译代码在Visual Studio或Rider中编译你的UE5项目。创建蓝图子类在内容浏览器中找到C类MyRPCCharacter右键创建基于它的蓝图类命名为BP_MyRPCCharacter。配置默认Pawn打开你的游戏模式蓝图通常在ThirdPersonBP文件夹下叫ThirdPersonGameMode将“Default Pawn Class”设置为我们的BP_MyRPCCharacter。准备资源音效在内容浏览器中导入或找一个现成的拍手音效.wav文件将其拖拽到BP_MyRPCCharacter的细节面板中赋值给Clap Sound变量。动画你需要一个拍手动画。可以从Mixamo等网站下载一个免费的角色拍手动画导入到UE5中并创建一个动画蒙太奇AnimMontage。然后将这个蒙太奇资产赋值给BP_MyRPCCharacter的Clap Anim Montage变量。测试PIE (Play In Editor) 单机测试直接点击播放按C键应该能正常播放动画和音效。此时所有逻辑都在本地运行。Listen Server 测试这是测试网络功能的关键。在编辑器播放按钮的下拉菜单中选择“Play As Listen Server”玩家数量选2或3。一个窗口会作为服务器和玩家1另外会弹出客户端窗口。用客户端窗口控制另一个角色按C键。你应该观察到在客户端1你控制的角色按下C立即看到自己拍手本地预测。稍后网络延迟在客户端2的窗口里看到客户端1的角色也在拍手Multicast RPC生效。在服务器窗口也是客户端1的视角角色也会拍手一次注意我们代码中通过IsLocallyControlled避免了重复播放。4. 高级主题RPC与游戏网络架构的协同掌握了基础RPC调用我们还需要把它放到更大的游戏网络架构中去理解避免写出低效甚至错误的代码。4.1 RPC 与属性复制Replication的分工与配合这是UE网络同步的两大支柱它们职责不同必须配合使用。属性复制用于持续状态的同步。例如角色的位置Replicated、血量ReplicatedUsing OnRep_Health、弹药数量等。这些状态每一帧或状态改变时都在同步是构成游戏世界的基础。RPC用于离散事件的同步。例如“开火”、“跳跃”、“使用道具”、“播放某段动画”。这些是瞬间发生的一次性动作。最佳实践模式客户端输入-Server RPC-服务器验证并修改权威状态变量-属性复制-所有客户端通过RepNotify更新。例如按下开火键客户端- 调用Server_Fire- 服务器计算弹道、命中减少弹药变量Ammo-Ammo被标记为Replicated自动同步到所有客户端 - 客户端的OnRep_Ammo函数更新UI。服务器触发事件-Multicast RPC-所有客户端播放效果。例如手榴弹爆炸服务器判断- 调用Multicast_Explode- 所有客户端播放爆炸特效和声音。同时爆炸的伤害和范围计算在服务器完成并修改受影响的玩家的Health属性通过属性复制同步。一个常见的错误是试图用RPC来同步一个持续变化的状态比如用每帧调用的UnreliableRPC来同步角色位置。这远不如引擎内置的CharacterMovementComponent的复制高效和稳定。4.2 网络条件预测与客户端权威性权衡在我们的拍手例子中我们使用了“客户端预测”按下键立刻播放效果不用等服务器回包。这极大地提升了操作的响应感是FPS、动作类游戏的标配。但它带来了“预测错误”的风险。什么是预测错误客户端预测了某个动作比如拍手但服务器后来否决了这个动作比如验证函数Server_Clap_Validate返回了false可能因为玩家突然死了。这时客户端已经播放的效果就需要被“回滚”或纠正。如何处理预测错误视觉效果的纠正相对简单对于拍手这种一次性效果即使服务器否决客户端多播一次也无伤大雅或者可以忽略。但对于移动预测就需要复杂的客户端侧回退与服务器侧调和机制这正是CharacterMovementComponent的复杂之处。状态同步的纠正必须严谨如果预测涉及状态改变比如扣血、消耗弹药服务器必须在RPC验证中严格检查并在状态改变后通过属性复制强制将正确的状态同步给客户端覆盖客户端预测的错误状态。客户端需要能够根据服务器的权威状态进行平滑修正。服务器权威 vs 客户端权威UE5默认是服务器权威模型。所有重要的游戏逻辑判定都在服务器。客户端RPC只是“建议”最终决定权在服务器。这是防止作弊的基石。除非在非常受控的环境下如局域网合作游戏否则不要轻易尝试客户端权威逻辑。4.3 RPC性能优化与调试技巧当游戏里的Actor和玩家多起来时不节制的RPC调用会成为性能杀手。优化策略频率限制对于高频事件如移动输入不要每帧调用RPC。可以在客户端进行采样或打包比如每0.1秒打包一次输入序列发送给服务器Server_Move。范围筛选RelevanceUE的网络系统本身就有基于距离和可见性的相关性更新。但对于自定义的Multicast RPC你可以通过NetMulticast函数的unreliable属性和AActor::GetNetMode()来判断是否需要在某些客户端执行。更精细的控制需要使用UNetDriver和Connection相关的高级API。参数优化RPC的参数会被序列化传输。避免传递大型结构体、复杂的UObject引用传递Object引用是危险的通常传递一个唯一的NetGUID或Actor引用。使用简单的数据类型int,float,FVector,FName。Channel选择UE有控制通道Control Channel和语音通道Voice Channel等。默认RPC在控制通道发送。对于大量低优先级的RPC如环境音效可以考虑使用其他通道但这属于高级用法。调试技巧控制台命令net.NetShowCorrections 1显示网络修正有助于查看预测错误。net.PacketSimulationSettings模拟网络丢包和延迟测试RPC在恶劣网络下的表现。stat Net显示详细的网络统计数据包括RPC发送/接收数量、带宽占用。可视化调试在编辑器的“运行”模式下打开“调试”菜单启用“网络仿真器”Network Emulator可以实时设置丢包率和延迟。日志输出在RPC函数实现里添加UE_LOG(LogTemp, Log, TEXT(“Server_Clap called on %s”), HasAuthority() ? TEXT(“Server”) : TEXT(“Client”));。通过输出日志的机器角色和网络模式可以清晰跟踪RPC的执行流。5. 实战避坑指南常见RPC问题与解决方案即使理解了原理在实际开发中还是会遇到各种诡异的问题。下面是我总结的一些高频“坑点”和解决方案。5.1 RPC调用失败原因排查清单当你发现RPC没有按预期执行时请按以下清单排查问题现象可能原因解决方案Server RPC 客户端调用后服务器无反应1. Actor的Role不是ROLE_AutonomousProxy或ROLE_SimulatedProxy。2. Actor还没有被服务器完全初始化BeginPlay还没调用。3. 函数没有标记为UFUNCTION(Server, ...)。4. 验证函数_Validate返回了false。5. 网络连接断开或严重丢包。1. 检查调用RPC的Actor的网络角色。确保在客户端上调用的。2. 在BeginPlay之后或使用定时器延迟调用。3. 检查头文件声明和宏拼写。4. 检查验证函数逻辑或暂时让其返回true测试。5. 检查网络状态和日志。Client RPC 服务器调用后特定客户端没执行1. 目标客户端不是该Actor的NetOwner。Client RPC默认只发送给NetOwner。2. 目标客户端的Actor尚未被创建或复制过去。3. 使用了Unreliable且包丢失。1. 确认调用关系。如果想发给非Owner的客户端需要通过目标客户端的PlayerController调用RPC。2. 确保服务器在Actor复制完成后再调用RPC。3. 对于关键通知改用Reliable。Multicast RPC 调用后部分客户端没效果1. 在客户端上调用了Multicast RPC这是无效的。2. 某些客户端与服务器的网络相关性被切断Actor超出NetCullDistance等。3. 使用了Unreliable且包丢失。4. 客户端执行函数时发生了逻辑错误如资源未加载。1.绝对确保Multicast RPC只在服务器HasAuthority()返回true的代码路径中被调用。2. 检查网络相关性和距离设置。3. 对于关键视觉效果同步考虑使用Reliable或配合一个复制的状态变量。4. 在客户端函数内添加健壮性检查IsValid。RPC执行顺序错乱1. 混合使用Reliable和UnreliableRPC。不可靠RPC不保证顺序。2. 在同一个“网络更新帧”内调用了多个RPC其执行顺序可能与调用顺序不完全一致尽管Reliable会尽力保证。1. 对于有严格顺序依赖的逻辑全部使用ReliableRPC。2. 将多个有顺序的操作合并到一个RPC调用中用一个状态参数或结构体来传递。5.2 验证函数_Validate的合理使用WithValidation生成的_Validate函数是一道重要的安全防线用于防止恶意客户端通过作弊手段调用不该调用的RPC或传入非法参数。应该在_Validate里检查什么参数范围检查坐标是否在合理地图范围内数值是否在合法区间如血量减少值是否为负。冷却时间检查技能是否处于冷却中。通常需要在服务器维护一个LastFireTime变量在_Validate里对比当前时间。状态合法性检查玩家是否存活bIsDead、是否被眩晕bIsStunned、是否有足够的资源法力值、弹药。动作频率限制单位时间内某个RPC的调用次数防止洪水攻击。注意事项_Validate函数应该轻量、快速、无副作用。它不应该修改游戏状态也不应该进行复杂的计算或阻塞操作如文件IO。如果_Validate返回false对应的_Implementation函数将永远不会被调用。客户端可能会在日志中收到一条警告。不要把所有的防作弊逻辑都放在_Validate里。重要的、决定性的逻辑如伤害计算、掉落物生成必须在_Implementation里用服务器的权威数据重新计算一遍。_Validate只是第一道粗略的过滤网。5.3 处理网络延迟与玩家体验网络延迟Ping是客观存在的。好的网络代码要“掩盖”延迟而不是对抗它。客户端预测Client-side Prediction正如我们拍手例子所做的对于玩家自己的操作立即给予视觉/听觉反馈。这是提升“操作手感”最关键的一步。预测的范围可以从简单的动画播放到复杂的移动和射击判定。插值Interpolation与外推Extrapolation对于其他玩家的状态位置、旋转我们收到的是来自服务器的不连续的快照。直接在两个快照之间“硬切”会导致物体抖动。我们需要在客户端对收到的状态进行插值使其平滑移动。有时还需要根据速度和方向进行短暂的外推以减少视觉延迟。CharacterMovementComponent内置了这些功能。延迟补偿Lag Compensation在射击游戏中当玩家A射击玩家B时服务器需要根据A的Ping值将游戏世界“回滚”到A开枪的那个时刻来判断是否命中。这是服务器端的高级技术UE5的Projectile和LineTrace在服务器执行时可以结合玩家的Controller的GetServerTime和Client的Server时间差来进行回滚计算。服务器调和Server Reconciliation当客户端的预测被服务器否决后服务器会发送正确的状态。客户端需要优雅地“纠正”自己的预测状态与服务器同步。对于移动这通常意味着将角色瞬间移动或平滑插值到服务器的位置。处理不好会产生“拉扯”或“抖动”现象。写网络代码尤其是RPC永远要带着“延迟”的思维去思考我发出的指令对方多久后能收到在这段时间里本地应该显示什么收到对方状态后如何平滑过渡把这些想清楚了你的多人游戏体验就不会差。