Photon Fusion 2 Shared Mode避坑指南:输入处理与状态同步实战

📅 发布时间:2026/7/15 23:10:01
Photon Fusion 2 Shared Mode避坑指南:输入处理与状态同步实战 1. 项目概述为什么Fusion的共享模式是多人游戏开发的“深水区”如果你正在用Unity开发一款强调竞技性、需要低延迟和精确判定的多人游戏比如一款俯视角射击或者格斗游戏那么Photon Fusion 2大概率已经进入了你的技术选型视野。Fusion以其独特的“状态同步”和“客户端预测”机制在解决网络延迟带来的“橡皮筋”和“瞬移”问题上确实比传统的Mirror或PUN要优雅得多。然而当你兴冲冲地打开官方文档准备大干一场时可能会发现一个令人头疼的岔路口Shared Mode共享模式。与传统的Client-Host或Dedicated Server模式不同Shared Mode有时也叫Shared Authority Mode是一种更灵活但也更复杂的网络拓扑。简单来说它没有单一的、绝对的“主机”。游戏逻辑运行在一个由Photon Cloud托管的“房间服务器”上这个服务器拥有状态的最终权威但它允许将特定游戏对象比如玩家角色的“输入权限”和“状态权限”动态地分配给连接进来的客户端。这听起来很美——既保证了服务器的权威性以防止作弊又让客户端能对自己的角色进行低延迟的预测和响应。但正是这种“权力下放”让输入处理和网络同步变成了一个布满暗礁的雷区。我经历过不止一个项目团队在Shared Mode下栽了大跟头。最常见的情况是本地测试时一切顺滑角色移动、射击反馈都无比跟手一旦上线进行多地区联机测试各种灵异事件就出现了——角色偶尔会抽搐、射击判定时有时无、两个玩家看到的对方位置不一致。这些问题追根溯源十有八九都出在对输入流的处理和网络状态的同步策略理解不透彻上。Fusion的API设计得很强大但如果你只是照猫画虎而不理解其底层Tick逻辑帧的运作机制、输入缓冲、状态补偿的原理那么Shared Mode就会从利器变成钝刀让你的开发进度举步维艰。这篇内容就是我结合多个实战项目的血泪教训为你梳理的一份Photon Fusion 2在Shared Mode下的“避坑指南”。我们将不局限于API调用而是深入到Tick的生命周期、NetworkInput的结构设计、NetworkTransform的配置玄学以及如何利用Fusion强大的调试工具来定位那些令人抓狂的同步问题。目标只有一个让你在Shared Mode下也能构建出既流畅又公平的多人游戏体验。2. 核心概念与架构拆解理解Shared Mode的权力游戏在深入代码之前我们必须像理解宪法一样搞清楚Shared Mode下的权力分配规则。这是所有后续设计和避坑的基础。2.1 三种关键权限Input, State, Object在Fusion的Shared Mode中每个网络对象NetworkObject的权限被精细地划分为三种理解它们的关系至关重要输入权限 (Input Authority)谁有权为这个对象提供输入拥有输入权限的客户端其本地玩家的操作按键、鼠标会被收集并发送到网络。通常一个玩家角色对象的输入权限会被固定分配给对应的客户端玩家这确保了你的操作能立刻被本地预测执行带来流畅的体验。状态权限 (State Authority)谁有权决定这个对象的网络状态Networked Properties的“正确”值在Shared Mode下房间服务器Room Server永远拥有所有对象的最终状态权限。这是防作弊的基石。客户端可以对有输入权限的对象进行预测但最终状态由服务器裁决和同步。对象权限 (Object Authority)谁拥有这个NetworkObject实例的生命周期控制权如生成、销毁在Shared Mode下通常也由房间服务器控制。关键点一个客户端可以拥有某个对象的输入权限但不拥有其状态权限。你的客户端可以“驱动”你的角色移动输入权限但服务器会校验你的移动是否合法比如是否穿墙并用它计算出的“权威状态”覆盖或纠正你的预测状态。这种分离是Shared Mode一切复杂性的根源。2.2 Tick驱动一切的时钟脉搏Fusion是一个基于Tick逻辑帧的模拟系统。你可以把它想象成一个节拍器所有联网的客户端和服务器都以相同的频率如每秒30次即30Hz推进这个Tick。固定速率Tick的推进是固定的不受渲染帧率FPS影响。这保证了所有机器上游戏逻辑的确定性基础。Tick编号每个Tick都有一个唯一的编号如Tick 100, 101, 102...。所有的输入、状态、事件都绑定在特定的Tick上。生命周期对于每个TickFusion会按固定顺序执行一系列生命周期回调如FixedUpdateNetwork,Render等。我们的代码主要插在FixedUpdateNetwork里。在Shared Mode下服务器是Tick的“主时钟”。客户端会努力追赶服务器的Tick进度但因为网络延迟客户端本地的Tick通常会比服务器慢几个。Fusion的客户端预测机制就是让客户端在尚未收到服务器对当前Tick的权威状态时基于自己的输入和之前的已知状态先“预测”执行未来几个Tick的逻辑。2.3 预测与调和 (Prediction Reconciliation)这是Fusion的核心魔法也是Shared Mode下输入处理必须配合的舞蹈。客户端预测假设你在Tick 100按下了“前进”键。你的客户端拥有输入权限会立刻将这个输入应用到本地角色状态上并在Tick 101、102...继续预测移动即使还没收到服务器对Tick 100的回应。这带来了零延迟的本地操作感。服务器裁决服务器在Tick 100收到你的输入运行权威的游戏逻辑包括碰撞检测、规则校验计算出Tick 100的权威状态。状态同步服务器将Tick 100的权威状态发送给所有客户端。调和你的客户端在稍晚的时候收到了服务器发来的、针对Tick 100的权威状态。此时你的本地可能已经预测执行到了Tick 105。Fusion会进行“调和”它用服务器的权威状态Tick 100覆盖本地预测的状态然后从Tick 101开始用已经缓存下来的本地输入你确实按了的键重新模拟Re-simulate一遍直到当前Tick。如果网络状况良好且游戏逻辑确定重新模拟后的结果应该与预测结果一致玩家无感。如果不一致比如服务器判定你撞墙了角色状态会被“纠正”这就是可能偶尔看到的“回退”或“抖动”。避坑提示1输入必须可重放预测与调和机制要求你的游戏逻辑是确定性的且输入处理必须是纯函数。给定相同的初始状态和相同的输入序列必须计算出完全相同的结果。任何在FixedUpdateNetwork中引入随机性如UnityEngine.Random或依赖本地实时时间Time.time的逻辑都会导致重新模拟时结果不一致引发严重的视觉抖动和逻辑错误。务必使用Fusion提供的Runner.GetRandom()或NetworkBehaviour的Object.Random来进行网络确定的随机。3. 输入处理实战从数据采集到网络发送输入处理是Shared Mode下客户端体验的生命线。目标是将玩家操作精准、及时地转化为网络输入数据。3.1 定义网络输入结构首先你需要定义一个结构体来承载输入数据它必须实现INetworkInput接口。using Fusion; using UnityEngine; public struct PlayerInput : INetworkInput { public Vector2 MoveDirection; // 移动方向 public NetworkButtons Buttons; // 按钮状态Fusion提供的位掩码工具高效 public Angle Yaw; // 偏航角Fusion的类型支持网络序列化 public Angle Pitch; // 俯仰角 // 注意避免在此结构中使用非确定性的类型如普通的float直接表示角度。 }为什么用NetworkButtons和AngleNetworkButtons内部使用uint位掩码存储按钮按下/释放状态非常节省带宽且自带状态变化检测。Angle是Fusion提供的类型能很好地处理角度环绕从359度到0度的网络序列化问题比直接发送float更可靠。3.2 采集输入与GetInput回调输入采集发生在拥有输入权限的客户端上。你需要在一个继承自NetworkBehaviour的脚本中通常是玩家控制脚本重写GetInput方法。public class PlayerController : NetworkBehaviour { public override void GetInput(ref PlayerInput input) { // 只有拥有输入权限的客户端才需要采集输入 if (Object.HasInputAuthority) { input.MoveDirection new Vector2(Input.GetAxisRaw(Horizontal), Input.GetAxisRaw(Vertical)).normalized; // 使用NetworkButtons管理按钮 input.Buttons.Set(MyButton.Jump, Input.GetKey(KeyCode.Space)); input.Buttons.Set(MyButton.Fire, Input.GetMouseButton(0)); input.Buttons.Set(MyButton.Sprint, Input.GetKey(KeyCode.LeftShift)); // 假设鼠标控制视角 // 注意这里只是采集原始输入数据复杂的视角计算可能需要在FixedUpdateNetwork中进行 float mouseX Input.GetAxisRaw(Mouse X) * sensitivity; float mouseY Input.GetAxisRaw(Mouse Y) * sensitivity; // 通常我们会累积一个本地的旋转值然后赋值给input。这里简化处理。 // 更佳实践是在OnInput中更新一个本地变量再在这里赋值。 } } }关键细节GetInput方法在每个本地预测的Tick都会被调用为这个Tick生成输入数据。它运行在拥有输入权限的客户端上。服务器或其他客户端不会执行这段代码。输入数据应尽可能“原始”。例如移动方向用-1, 0, 1表示而不是直接应用了速度后的位移。复杂的转换和计算应留在确定的FixedUpdateNetwork中。3.3 在FixedUpdateNetwork中消费输入这是游戏逻辑的核心。FixedUpdateNetwork会在每个Tick在所有需要模拟此对象的机器上服务器、拥有输入权限的客户端、其他旁观客户端被调用。public override void FixedUpdateNetwork() { // 1. 尝试获取当前Tick的输入 if (GetInputPlayerInput(out var input)) { // 成功获取到输入处理移动、旋转等逻辑 ProcessMovement(input.MoveDirection); ProcessRotation(input.Yaw, input.Pitch); ProcessButtons(input.Buttons); } else { // 没有获取到输入的情况对于此对象 // 对于拥有输入权限的客户端这通常意味着 // a) 这是服务器尚未裁决的预测Tick但输入数据可能因为缓冲尚未就绪罕见。 // b) 网络延迟或丢包。 // 常见的处理方式是“向前外推”例如继续应用上一Tick的速度。 ExtrapolateMovement(); } // 无论是否有输入一些每Tick都需要更新的逻辑放在这里比如重力应用、状态同步检查等。 ApplyGravity(); }GetInput方法的奥秘对于拥有输入权限的客户端GetInput会从本地采集并缓冲的输入数据中取出对应此Tick的输入。在预测阶段它总是能成功。对于服务器GetInput会尝试从对应客户端发送过来的输入数据包中取出对应Tick的输入。如果网络延迟导致输入数据包尚未到达GetInput会返回false。Fusion有输入缓冲机制允许服务器等待一定时间。对于其他客户端它们通常无法获取其他玩家的原始输入GetInput返回false。它们完全依赖于服务器同步过来的网络状态进行渲染。避坑提示2区分“HasInput”与“HasInputAuthority”一个常见的误解是在FixedUpdateNetwork里只有Object.HasInputAuthority为真时才处理输入。这是错误的服务器在模拟你的角色时Object.HasInputAuthority为假但它需要通过GetInput来获取你的输入数据以运行权威逻辑。正确的做法是依赖GetInput(out input)的返回值来判断当前Tick是否有可用的输入数据供逻辑消费。3.4 输入缓冲与延迟补偿网络延迟是客观存在的。Fusion通过输入缓冲来应对。客户端输入缓冲客户端会提前采集并发送未来几个Tick的输入。这给了服务器一定的“缓冲时间”来等待迟到的输入包。服务器重演如果服务器在运行某个Tick的逻辑时对应的客户端输入尚未到达它可以暂停模拟此对象等待输入。如果等待超时则可能使用旧的输入或默认输入进行模拟。在Shared Mode下由于服务器是权威它必须尽可能使用正确的输入。如何配置在Fusion的NetworkProjectConfig资产中可以找到Input相关配置Redundancy输入数据冗余发送的次数用于对抗丢包。Resend Rate输入重发间隔。Buffer Size输入缓冲区大小。增大缓冲区可以容忍更高的网络延迟波动但也会增加操作延迟从按下按键到服务器响应的最坏情况时间。需要在流畅性和响应性之间权衡。个人经验对于动作类游戏Buffer Size设置为3-5对应100-166ms 30Hz是一个不错的起点。对于回合制或延迟不敏感的游戏可以设得更高。4. 网络状态同步与权威性处理输入驱动了逻辑而逻辑的结果需要通过网络状态同步给所有玩家。在Shared Mode下状态同步是服务器权威的体现。4.1 使用Networked Properties将需要同步的变量标记为[Networked]。这是Fusion状态同步的基础。public class PlayerState : NetworkBehaviour { [Networked] public Vector3 NetworkPosition { get; set; } [Networked] public Quaternion NetworkRotation { get; set; } [Networked] public float CurrentHealth { get; set; } [Networked] public NetworkBool IsGrounded { get; set; } // Fusion提供的网络布尔类型 // 注意[Networked]属性不能是普通的getter/setterFusion需要生成代码。 }同步时机[Networked]属性的值在服务器每Tick计算后如果发生变化并且根据变化检测模式会被包含在发送给客户端的快照中。4.2 NetworkTransform便捷与陷阱NetworkTransform组件是Fusion提供的用于同步Transform位置、旋转、缩放的神器。但它也是Shared Mode下的一个主要“坑点”。问题NetworkTransform默认在拥有状态权限的端即Shared Mode下的服务器上计算权威的Transform然后同步给客户端。但是对于拥有输入权限的玩家角色客户端需要进行预测移动。如果直接使用NetworkTransform你会遇到客户端预测移动了自己的Transform。服务器稍后发来权威的Transform状态。NetworkTransform用服务器的状态覆盖了客户端本地的Transform。结果客户端看到自己的角色在预测移动和服务器纠正之间反复横跳产生剧烈的抖动。解决方案对于需要客户端预测的游戏对象通常是玩家角色避免使用NetworkTransform来同步其位置和旋转。你应该手动管理[Networked]的位置和旋转状态。在拥有输入权限的客户端上根据输入在FixedUpdateNetwork中计算并直接修改Transform预测。在服务器上运行同样的逻辑计算权威的[Networked]状态。在没有输入权限的客户端其他玩家视角根据同步过来的[Networked]状态在Render回调中通过插值平滑地更新Transform。public class PlayerMovement : NetworkBehaviour { [Networked] public Vector3 NetworkPosition { get; set; } [Networked] public Quaternion NetworkRotation { get; set; } private CharacterController _controller; private float _verticalVelocity 0f; public override void Spawned() { _controller GetComponentCharacterController(); if (Object.HasInputAuthority) { // 本地玩家初始化位置可能从Networked属性加载 transform.position NetworkPosition; } } public override void FixedUpdateNetwork() { if (GetInputPlayerInput(out var input)) { // 所有模拟端服务器和输入客户端都执行相同的确定性逻辑 Vector3 move CalculateMovement(input.MoveDirection, Runner.DeltaTime); if (Object.HasStateAuthority) // 仅在服务器上更新Networked属性 { NetworkPosition move; // 服务器也可以进行碰撞检测等权威逻辑 NetworkPosition AuthoritativeCollisionCheck(NetworkPosition); } // 拥有输入权限的客户端进行预测直接修改Transform if (Object.HasInputAuthority) { _controller.Move(move); // 注意这里预测的位置可能与服务器最终的NetworkPosition不同 // 调和发生时Fusion会通过[Networked]属性纠正 } } // 服务器同步Networked状态 // 其他客户端在Render中根据NetworkPosition插值 } public override void Render() { // 对于其他玩家的角色没有输入权限进行平滑插值渲染 if (!Object.HasInputAuthority !Object.HasStateAuthority) { transform.position Vector3.Lerp(transform.position, NetworkPosition, Runner.DeltaTime * interpolationSpeed); transform.rotation Quaternion.Slerp(transform.rotation, NetworkRotation, Runner.DeltaTime * interpolationSpeed); } // 对于本地玩家预测已经修改了Transform通常不需要在Render中再做处理 // 除非需要进行视觉上的平滑如摄像机跟随但那与网络同步无关。 } }避坑提示3区分逻辑位置与渲染位置永远要清楚你正在操作的是哪个“位置”。NetworkPosition是服务器权威的逻辑状态。transform.position是本地视觉表现。对于预测对象在FixedUpdateNetwork中修改transform.position是为了即时反馈在Render中对于非权威对象需要根据NetworkPosition插值来平滑网络延迟带来的跳跃。混淆二者是同步问题的常见根源。4.3 变化检测与带宽优化[Networked]属性默认使用OnChanged回调进行变化检测即值改变时才同步。这对于频繁变化的属性如位置是合适的。但对于一些离散事件如玩家开枪使用NetworkBehaviour的RPC远程过程调用可能更合适。[Networked(OnChanged nameof(OnHealthChanged))][Networked] public float CurrentHealth { get; set; } private void OnHealthChanged() { // 当Health在网络上变化时所有客户端都会调用此方法 UpdateHealthUI(CurrentHealth); }RPC用于离散事件[Rpc(RpcSources.InputAuthority, RpcTargets.StateAuthority)] public void RPC_Fire() { // 服务器收到RPC执行权威的射击逻辑生成子弹、计算伤害等 if (Runner.IsServer) { SpawnBullet(); } }在GetInput中收集开火按钮状态但在FixedUpdateNetwork中当检测到按钮刚按下时调用RPC_Fire。这样射击事件由拥有输入权限的客户端发起但逻辑在权威的服务器上执行完美契合Shared Mode的权限模型。5. 调试、监控与性能优化开发多人游戏没有强大的调试工具就像蒙着眼睛走钢丝。Fusion提供了一些内建工具但我们也需要自己搭建监控。5.1 使用Fusion的Stats GUI和Debug Runner在开发时务必在NetworkRunner的预制体或初始化代码中启用Stats GUI。var args new StartGameArgs() { // ... 其他参数 EnableClientSessionCreation true, RunnerPrefab myRunnerPrefab // 确保Prefab上有FusionStats组件 };FusionStats会显示至关重要的实时信息RTT (Round Trip Time)往返延迟。Tick当前本地Tick和服务器Tick。Offset本地Tick与服务器Tick的差值预测深度。In / Out网络流量。Simulation模拟时间。当出现同步问题时首先看Offset是否稳定。如果Offset持续增大说明客户端模拟速度跟不上服务器可能是性能问题或网络延迟过高。如果Offset剧烈波动通常意味着网络抖动或丢包严重。5.2 可视化预测与调和Fusion的NetworkObject有一个DrawPredictionHistory的调试选项在Inspector中勾选。启用后可以在Scene视图中看到绿色权威状态来自服务器。蓝色/红色客户端预测的历史状态。当你的角色移动时你会看到一条预测轨迹。如果网络顺畅绿色点和蓝色点会紧密重合。如果出现大的偏差或跳跃说明预测与服务器裁决不一致可能是逻辑非确定性或输入处理有问题。这是诊断预测错误最直观的方法。5.3 自定义网络状态监控对于关键状态可以添加自定义的调试绘制。void OnGUI() { if (Runner ! null Object ! null) { GUI.Label(new Rect(10, 100, 300, 20), $HasInputAuth: {Object.HasInputAuthority}); GUI.Label(new Rect(10, 120, 300, 20), $HasStateAuth: {Object.HasStateAuthority}); GUI.Label(new Rect(10, 140, 300, 20), $Tick: {Runner.Tick}); GUI.Label(new Rect(10, 160, 300, 20), $Pos: {NetworkPosition}); GUI.Label(new Rect(10, 180, 300, 20), $Predicted Pos: {transform.position}); } }比较NetworkPosition权威状态和transform.position本地预测/渲染位置可以清晰看到调和是否发生以及幅度。5.4 性能考量Shared Mode下服务器需要模拟所有游戏逻辑负担较重。兴趣管理 (Interest Management)如果游戏场景很大、玩家很多确保启用并正确配置兴趣管理。Fusion支持基于距离、区域等的兴趣管理可以大幅减少不必要的状态同步降低带宽和服务器CPU开销。在NetworkObject上配置Interest属性和在NetworkRunner中设置兴趣管理范围。Networked Property数量最小化[Networked]属性的数量和大小。只同步必要的数据。对于复杂的结构考虑使用压缩或自定义序列化。Tick Rate选择更高的Tick Rate如60Hz意味着更精细的模拟和更低的操作延迟但也会加倍网络流量和服务器计算量。对于大多数游戏30Hz是一个很好的平衡点。格斗或高速射击游戏可能需要60Hz。客户端预测开销预测只发生在拥有输入权限的对象上。确保预测逻辑你的FixedUpdateNetwork是高效的。避免在预测循环中进行昂贵的物理查询或复杂的AI计算。6. 常见问题排查与实战技巧这里汇总了我在多个项目中遇到的最典型问题及其解决方案。6.1 角色抖动或“橡皮筋”效应症状自己的角色或他人角色在移动时不稳定偶尔回退。排查步骤检查网络延迟和抖动观察Fusion Stats的RTT和Offset。高延迟或剧烈抖动是首要原因。确认预测与调和打开DrawPredictionHistory。如果绿色权威和蓝色预测轨迹分离严重说明预测逻辑与服务器裁决不一致。检查确定性确保FixedUpdateNetwork中的逻辑完全确定。排查所有UnityEngine.Random、Time.deltaTime应使用Runner.DeltaTime、物理查询非确定性的用法。检查输入处理确保GetInput采集的是原始输入且在所有客户端上对于同一Tick的输入数据是完全相同的例如按钮按下状态是二元的不是模拟轴经过平滑的值。检查插值对于其他玩家的角色非输入权限抖动可能是渲染插值问题。确保在Render中使用了正确的插值算法如Vector3.Lerp和适当的插值速度。NetworkTransform的插值设置也可能需要调整。检查NetworkTransform误用如果是自己的角色抖动几乎可以断定错误地使用了NetworkTransform在预测对象上。按4.2节方案重构。6.2 输入响应延迟高症状按下按键后角色反应有明显的延迟感。排查步骤区分本地延迟和网络延迟在单人模式或主机模式下测试。如果延迟依旧问题在本地逻辑如动画状态机过渡时间过长。检查输入缓冲大小在NetworkProjectConfig中减少Input Buffer Size。但这会降低对抗网络波动的能力需要权衡。优化网络路径使用Photon的云服务并让玩家选择最近的地域Region。确保没有不必要的网络中间层。检查客户端预测是否生效在GetInput中打印日志确认输入被采集。在FixedUpdateNetwork中确认GetInput(out input)在预测Tick返回true并且你的移动逻辑立即应用了该输入。预测的核心就是让输入立刻生效如果这里还有延迟就是代码逻辑问题。6.3 射击或其他事件不同步症状A玩家看到自己击中了B但B没有掉血或者服务器没有记录这次击中。排查步骤权威性在谁那里牢记Shared Mode下所有重要的游戏事件伤害计算、物品生成、胜负判定都必须在服务器State Authority上执行。检查RPC调用客户端发起射击事件是否通过RPC标记为RpcSources.InputAuthority, RpcTargets.StateAuthority发送到了服务器确保RPC在FixedUpdateNetwork中调用而不是在Update中。检查时机服务器在收到RPC_Fire的Tick执行逻辑。客户端在调用RPC_Fire的Tick进行预测表现如播放射击动画、生成本地特效。要处理“命中判定”的延迟补偿Fusion提供了LagCompensation组件它可以帮助服务器在过去的Tick考虑子弹飞行时间和网络延迟进行射线检测这是实现公平射击游戏的关键。使用Hitbox对于复杂的角色命中区域使用Fusion的Hitbox组件代替简单的射线检测或碰撞体它能与延迟补偿系统完美配合。6.4 实体生成/销毁不同步症状子弹、特效等动态生成的物体有的客户端能看到有的看不到。排查步骤生成权限确保动态NetworkObject的生成是通过Runner.Spawn并且只在拥有状态权限的端服务器调用。客户端通过RPC请求服务器生成。预制体注册检查要生成的预制体是否已在NetworkProjectConfig的Registered Networked Prefabs列表中注册。资源加载如果使用Addressables或异步加载确保在生成前预制体已加载完成。最好在游戏初始化阶段就预加载所有需要的网络预制体。6.5 一个实战技巧平滑处理调和导致的瞬移即使预测逻辑完全正确在网络条件极差时服务器发来的权威状态可能与客户端预测状态差距很大导致角色位置被“硬纠正”玩家会看到瞬移。缓解方案对于自己的角色拥有输入权限可以在调和发生时不立即将Transform硬切到NetworkPosition而是用一个极短的时间如0.1秒平滑地过渡过去。这需要在Render中实现private Vector3 _renderPosition; private bool _isReconciling false; private float _reconcileTimer 0f; public override void Render() { if (Object.HasInputAuthority) { // 本地玩家 if (_isReconciling) { _reconcileTimer Runner.DeltaTime; float t Mathf.Clamp01(_reconcileTimer / 0.1f); // 用0.1秒完成平滑 transform.position Vector3.Lerp(_renderPosition, NetworkPosition, t); if (t 1f) { _isReconciling false; } } else { // 正常情况预测位置就是渲染位置 _renderPosition transform.position; } } else { // 其他玩家标准插值 transform.position Vector3.Lerp(transform.position, NetworkPosition, Runner.DeltaTime * interpolationSpeed); } } // 在Networked属性的OnChanged回调中触发平滑 private void OnNetworkPositionChanged() { if (Object.HasInputAuthority) { // 当NetworkPosition被服务器更新调和发生时开始平滑过渡 _isReconciling true; _reconcileTimer 0f; _renderPosition transform.position; // 从当前预测位置开始 // 注意不要立即修改transform.position } }这个技巧牺牲了极小程度的“绝对正确性”换取了视觉上的平滑对于大多数游戏来说体验提升巨大。但它增加了代码复杂度需要谨慎测试。开发Photon Fusion 2的Shared Mode多人游戏是一个不断与网络不确定性斗争的过程。理解权限模型、Tick机制、预测与调和是基础而细致的输入处理、正确的状态同步策略和强大的调试手段则是成功的保障。最大的心得是永远假设网络会延迟、会丢包、会乱序你的代码必须在任何网络条件下都能优雅降级保持逻辑的一致性和视觉的可接受性。从一个小原型开始频繁进行多客户端测试充分利用调试工具每解决一个同步问题你对网络游戏开发的理解就会更深一层。