Photon Fusion共享模式:输入处理与网络同步实战解析

发布时间:2026/7/15 3:55:28
Photon Fusion共享模式:输入处理与网络同步实战解析 1. 项目概述为什么Fusion的共享模式是多人游戏开发的“深水区”如果你正在用Unity做多人游戏并且已经跳过了Mirror、Netcode for GameObjects这些基础方案开始研究Photon Fusion 2那你大概率已经踩过不少坑或者至少听说过它的“威力”。Fusion以其强大的状态同步和客户端预测能力著称但它的“共享模式”绝对是让开发者又爱又恨的一个特性。爱它是因为它提供了介于专用服务器和客户端托管之间的一种灵活、成本效益高的架构恨它是因为它的输入处理和网络同步逻辑和传统的主从式或权威服务器模式有着本质区别稍有不慎就会引入难以调试的同步问题。简单来说Fusion的共享模式意味着游戏房间运行在Photon Cloud上拥有状态的最终权威但特定网络对象Network Object的输入和状态控制权可以动态地分配给连接的客户端。这听起来很美既享受了云端服务器的稳定性和防作弊优势又允许玩家对自己的角色进行低延迟的客户端预测操作。但魔鬼藏在细节里——“谁在什么时候、以什么方式、同步什么数据”这个问题的答案在共享模式下变得异常复杂。输入处理不再是简单的Input.GetKey然后发送RPC网络同步也不再是服务器说了算。你需要精确地理解State Authority状态权威和Input Authority输入权威的分离以及Fusion的Tick逻辑帧系统如何贯穿其中。我最近刚用一个共享模式的项目从坑里爬出来过程中经历了角色抽搐、输入延迟、状态回滚诡异等一系列经典问题。这篇文章我就结合实战把Fusion共享模式下输入处理与网络同步的核心逻辑、常见陷阱以及我的解决方案掰开揉碎讲清楚。无论你是在做一款快节奏的竞技游戏还是一个需要精细交互的协作体验这些经验都能帮你避开那些让我熬了好几个通宵的“坑”。2. 核心概念拆解权威、Tick与预测在深入代码之前我们必须统一对几个核心概念的理解。这些概念是Fusion架构的基石理解偏差会导致整个设计方向错误。2.1 状态权威 vs. 输入权威这是共享模式最核心也最易混淆的一对概念。状态权威指对某个网络对象如一个玩家角色、一个可拾取物品的NetworkTransform、NetworkRigidbody以及你自定义的Networked Properties的最终解释权。拥有状态权威的一方其对该对象状态的修改会被Fusion视为“真相”并同步给其他所有客户端。输入权威指被允许向某个网络对象提交NetworkInput数据的一方。通常一个玩家控制自己的角色时该玩家客户端就是这个角色的输入权威。在共享模式下游戏房间服务器默认持有所有网络对象的状态权威。但是你可以通过代码将特定对象的输入权威有时连同部分状态的控制权分配给某个客户端。关键点在于输入权威客户端提交的输入需要经过房间拥有状态权威的验证和转发才能最终影响对象状态。这引入了“延迟”和“预测”的概念。2.2 Tick系统与预测Fusion是一个基于Tick逻辑帧的确定性模拟框架。每个Tick都是一个离散的时间步长游戏逻辑在其中运行。渲染帧 vs. 逻辑Tick你的游戏可能以60FPS渲染但Fusion的逻辑Tick可能是30Hz。所有网络同步和核心游戏逻辑如移动、伤害计算都在Tick边界发生。客户端预测当你是某个对象的输入权威时你不需要等待服务器确认你的输入就可以本地预测这个输入对对象状态产生的影响并立即在本地呈现结果。这创造了零延迟的操作手感。状态回滚与调和服务器在稍后的Tick接收到你的输入后会进行权威模拟。如果服务器的模拟结果与你本地的预测不一致由于网络延迟或逻辑错误Fusion会自动将你的本地对象状态“回滚”到服务器认可的那个Tick的状态然后重新应用从那个Tick之后的所有输入包括服务器的权威结果进行“调和”最终平滑地过渡到正确状态。这个过程通常是自动的但如果你处理不当就会看到角色“抽搐”或“闪现”。2.3 共享模式下的数据流理解数据流向是正确编码的关键。以一个玩家移动为例客户端输入权威在FixedUpdateNetwork中收集本地输入如WASD按键。通过GetInputT获取一个输入结构体填充数据。调用SetInput将输入提交给Fusion系统。注意这个输入不是直接生效而是被放入一个缓冲区等待发送给服务器。网络传输客户端的输入数据被打包通过网络发送到拥有状态权威的房间服务器。服务器状态权威在下一个Tick服务器收到客户端之前发送的输入。服务器在FixedUpdateNetwork中通过GetInputT读取所有客户端对这个Tick的输入。服务器基于这些输入运行权威的游戏逻辑例如应用移动、检测碰撞计算出所有网络对象在这个Tick的权威状态。状态同步服务器将计算出的权威状态或状态变化量打包成快照发送给所有客户端。客户端接收与调和客户端收到服务器的状态快照。Fusion内核将本地对象的状态回滚到与服务器快照对应的Tick。然后Fusion使用从那个Tick到当前Tick的所有已知输入包括本地预测的输入和服务器确认的输入重新模拟调和得到当前帧应该呈现的状态。渲染层根据这个调和后的状态进行渲染实现平滑的视觉表现。这个过程确保了所有客户端最终会收敛到服务器认定的状态同时在输入响应的瞬间提供了流畅的本地反馈。3. 输入处理实战从采集到提交理论说再多不如一行代码。我们来看如何在共享模式下正确地处理玩家输入。3.1 创建网络输入结构体首先你需要定义一个结构体来承载你的输入数据。这个结构体必须实现INetworkInput接口。using Fusion; using UnityEngine; // 定义输入结构体 public struct PlayerInput : INetworkInput { public Vector2 MoveDirection; // 移动方向 public NetworkButtons Buttons; // 按钮状态跳跃、攻击等 public Vector3 LookForward; // 视角朝向可选用于同步旋转 }注意NetworkButtons是Fusion提供的一个高效处理按钮按下/释放状态的类型。对于简单的布尔值按钮强烈推荐使用它而不是多个bool因为它能更好地处理网络压缩和变化检测。3.2 在NetworkBehaviour中处理输入接下来在你的玩家角色脚本继承自NetworkBehaviour中处理输入。using Fusion; using UnityEngine; public class PlayerCharacter : NetworkBehaviour { // 网络化属性用于同步由输入计算出的状态 [Networked] public Vector3 NetworkedVelocity { get; set; } [Networked] public Angle NetworkedYaw { get; set; } // 使用Fusion的Angle类型同步旋转 // 本地缓存的速度用于平滑和预测 private Vector3 _cachedVelocity; private CharacterController _controller; private Camera _viewCamera; public override void Spawned() { // 只在拥有输入权威的客户端上初始化本地组件 if (HasInputAuthority) { _controller GetComponentCharacterController(); _viewCamera Camera.main; // 或你自己的摄像机 // 可以在这里禁用非输入权威客户端的控制器避免误操作 } // 所有客户端都需要知道这个对象的存在 // 可能初始化视觉表现部分 } public override void FixedUpdateNetwork() { // 这是最重要的方法在每个逻辑Tick都会被调用 // 1. 检查是否拥有输入权威并且能获取到输入 if (GetInputPlayerInput(out var input)) { // 2. 处理移动输入 Vector3 move new Vector3(input.MoveDirection.x, 0, input.MoveDirection.y); // 将输入从本地空间转换到世界空间考虑摄像机旋转 move Quaternion.Euler(0, _viewCamera.transform.eulerAngles.y, 0) * move; // 3. 应用移动逻辑这里简单设置为速度实际可能有加速度、阻力等 _cachedVelocity move * 5.0f; // 5是移动速度 // 将计算出的速度同步到网络属性 NetworkedVelocity _cachedVelocity; // 4. 处理按钮输入 // 假设Buttons的第0位是“跳跃” var pressed input.Buttons.GetPressed(NetworkRunner.Instance.GetPreviousInputSnapshotPlayerInput(Object).Buttons); if (pressed.IsSet(0)) // 如果“跳跃”按钮在这一帧被按下 { // 触发跳跃逻辑例如修改速度的Y分量 _cachedVelocity.y 8.0f; NetworkedVelocity _cachedVelocity; // 记得更新网络属性 } // 5. 处理旋转如果需要同步 if (input.LookForward ! Vector3.zero) { var targetYaw Quaternion.LookRotation(input.LookForward).eulerAngles.y; NetworkedYaw Angle.FromDegrees(targetYaw); } } // 6. 无论是否有新输入都应用网络同步的速度这对于状态调和至关重要 // 注意这里直接使用NetworkedVelocity因为它已经包含了服务器调和后的权威值 if (_controller ! null Runner.DeltaTime 0) { // 使用CharacterController移动 _controller.Move(NetworkedVelocity * Runner.DeltaTime); // 应用旋转 transform.rotation Quaternion.Euler(0, (float)NetworkedYaw, 0); } } // 可选的在Render方法中处理视觉平滑如摄像机跟随、动画混合 // FixedUpdateNetwork处理逻辑状态Render处理表现平滑 public override void Render() { // 这里可以根据NetworkedVelocity和_cachedVelocity进行视觉插值使运动更平滑 // 例如对于非输入权威的对象可以使用Runner.InterpolationAlpha对位置进行插值 if (!HasInputAuthority) { // 对其他玩家的角色进行视觉平滑处理 // transform.position Vector3.Lerp(_previousPos, _currentPos, Runner.InterpolationAlpha); } } }关键点解析GetInputT(out var input)这个方法是输入处理的核心。只有在当前客户端拥有该对象的输入权威并且在该Tick成功从缓冲区获取到输入数据时它才返回true。对于非输入权威的客户端或服务器当它不模拟该对象时这个方法返回false。HasInputAuthority属性用于判断当前运行实例是否拥有该对象的输入权威。在Spawned中我们用它来初始化只有输入者才需要的组件如CharacterController、摄像机引用。千万不要在FixedUpdateNetwork里依赖它来决定是否处理逻辑因为服务器的FixedUpdateNetwork也会在拥有状态权威的对象上执行但它没有输入权威。逻辑应该基于GetInput的成功与否。Networked属性所有需要跨网络同步的状态都必须标记为[Networked]。Fusion会自动同步它们。在FixedUpdateNetwork中修改它们修改会在下一个Tick生效并被同步。状态计算与赋值我们在获取输入后计算速度并赋值给NetworkedVelocity。这个赋值操作既发生在本地预测的客户端也发生在进行权威模拟的服务器。服务器用同样的逻辑计算出的NetworkedVelocity就是权威值会覆盖客户端可能错误的预测值。3.3 输入采集与提交上面的代码处理了输入的消费那么输入是如何产生的呢通常你需要另一个脚本来采集原始输入并调用SetInput。一个常见的模式是使用一个独立的NetworkBehaviour比如叫PlayerInputHandler挂载在玩家对象或一个全局管理器上。using Fusion; using UnityEngine; public class PlayerInputHandler : NetworkBehaviour { private PlayerInput _framedInput; private NetworkRunner _runner; public override void Spawned() { _runner Runner; } public override void FixedUpdateNetwork() { // 只有拥有自己玩家对象输入权威的客户端才需要采集和提交输入 // 假设我们通过某种方式获取了当前客户端控制的PlayerCharacter对象 PlayerCharacter myCharacter GetMyCharacter(); // 你需要实现这个方法 if (myCharacter ! null myCharacter.HasInputAuthority) { // 1. 重置输入结构体重要避免上一帧的数据污染 _framedInput default; // 2. 采集移动输入 _framedInput.MoveDirection.x Input.GetAxisRaw(Horizontal); _framedInput.MoveDirection.y Input.GetAxisRaw(Vertical); // 归一化防止斜向移动更快 if (_framedInput.MoveDirection.sqrMagnitude 1f) _framedInput.MoveDirection.Normalize(); // 3. 采集按钮输入 _framedInput.Buttons.Set(0, Input.GetKey(KeyCode.Space)); // 跳跃 _framedInput.Buttons.Set(1, Input.GetMouseButton(0)); // 攻击 // 4. 采集视角朝向示例基于鼠标位置计算看向的方向 Ray ray Camera.main.ScreenPointToRay(Input.mousePosition); Plane groundPlane new Plane(Vector3.up, Vector3.zero); if (groundPlane.Raycast(ray, out float distance)) { Vector3 point ray.GetPoint(distance); _framedInput.LookForward (point - myCharacter.transform.position).normalized; } // 5. 将输入提交给Fusion // 注意SetInput的调用必须在FixedUpdateNetwork中且通常在每个Tick只调用一次 Runner.SetInput(_framedInput); } } private PlayerCharacter GetMyCharacter() { // 实现逻辑例如通过Object.HasInputAuthority查找或通过PlayerRef关联 // 这是一个简化示例 var players FindObjectsOfTypePlayerCharacter(); foreach (var p in players) { if (p.HasInputAuthority) return p; } return null; } }重要提示SetInput必须在FixedUpdateNetwork中调用并且最好只调用一次。它提交的是针对当前Tick的输入。Fusion会管理输入缓冲区处理预测和延迟。4. 网络同步实战状态、RPC与插值输入处理好了同步就是下一个大头。Fusion提供了多种同步机制。4.1 Networked Properties网络属性这是最基础、最常用的同步方式用于同步持续变化的状态如位置、血量、分数。[Networked] public float NetworkedHealth { get; set; } [Networked] public int NetworkedScore { get; set; } [Networked] public NetworkString_16 PlayerName { get; set; } // 使用NetworkString同步字符串特性与技巧OnChanged 回调当网络属性的值发生变化时可以触发一个回调。这对于更新UI、播放音效等表现层逻辑非常有用因为FixedUpdateNetwork是逻辑层不适合直接处理表现。[Networked(OnChanged nameof(OnHealthChanged))] public float NetworkedHealth { get; set; } public static void OnHealthChanged(ChangedPlayerCharacter changed) { // changed.Behaviour 是属性所属的NetworkBehaviour实例 float newHealth changed.Behaviour.NetworkedHealth; // 在这里更新血条UI播放受伤音效等 // 注意这个回调会在所有客户端上触发 changed.Behaviour.UpdateHealthUI(newHealth); }精度与压缩对于float和Vector3可以通过[Networked, Accuracy(...)]属性设置精度和压缩减少带宽。例如[Networked, Accuracy(0.01f)]表示精确到小数点后两位。Tick对齐网络属性的修改在FixedUpdateNetwork中生效并在下一个Tick开始同步。这意味着修改和同步之间有一个Tick的延迟。设计游戏逻辑时需要考虑到这一点。4.2 RPC远程过程调用用于触发一次性的事件如发射子弹、发送聊天消息、播放特定动画。// RPC必须在一个NetworkBehaviour类中定义 [Rpc(RpcSources.InputAuthority, RpcTargets.StateAuthority)] public void Rpc_FireWeapon(Vector3 direction, float power) { // 这个函数会在State Authority服务器/主机上执行 // 在这里进行权威的伤害计算、生成网络对象等 if (Runner.IsServer) // 双重检查 { // 生成子弹网络对象由服务器进行权威模拟 Runner.Spawn(bulletPrefab, firePoint.position, Quaternion.LookRotation(direction)); } } // 在客户端输入处理中调用 if (Input.GetMouseButtonDown(0)) { Rpc_FireWeapon(aimDirection, chargePower); }RPC参数详解RpcSources指定谁可以调用这个RPC。InputAuthority只有拥有该对象输入权威的客户端可以调用。这是最常用、最安全的方式确保只有控制该对象的玩家才能触发相关动作。StateAuthority只有拥有状态权威的一方服务器可以调用。All任何客户端都可以调用。慎用容易导致作弊或状态不一致。RpcTargets指定这个RPC在哪里执行。StateAuthority在状态权威方执行。这是处理游戏逻辑如伤害、生成的标准做法确保逻辑的权威性。All在所有客户端执行。适用于播放视觉效果、音效等不影响核心逻辑的表现事件。Proxies在所有非状态/输入权威的客户端即旁观者上执行。Channel默认为Reliable可靠保证送达但可能有延迟。对于非关键性的视觉效果可以设置为Unreliable不可靠以减少开销。4.3 插值与平滑即使网络同步完美直接渲染网络状态也会导致卡顿因为状态是按Tick如30Hz更新的而渲染是按帧如60Hz进行的。Fusion内置了插值机制。自动插值对于NetworkTransform组件Fusion会自动处理位置和旋转的插值。你只需要确保在NetworkObject上勾选了Interpolation Data相关的选项。手动插值对于自定义的网络属性如速度、颜色你可以在Render方法中手动进行插值。Runner.InterpolationAlpha提供了当前渲染帧介于哪两个Tick之间的插值因子。public override void Render() { // 假设我们有一个网络化的颜色属性 // [Networked] public Color NetworkedColor { get; set; } // 我们需要在之前记录下它的变化 // 通常在OnChanged回调里记录_previousColor和_currentColor float alpha Runner.InterpolationAlpha; Color renderColor Color.Lerp(_previousColor, _currentColor, alpha); GetComponentRenderer().material.color renderColor; }预测平滑对于拥有输入权威的对象其运动已经是本地预测的通常很平滑。但对于其他玩家对象插值就非常重要。确保非玩家对象的NetworkObject上启用了插值并注意NetworkTransform的插值模式InterpolatevsExtrapolate。5. 共享模式下的高级陷阱与调试技巧即使理解了所有概念实际开发中还是会遇到各种诡异的问题。下面是我踩过的一些坑和解决方法。5.1 陷阱一输入抖动与“橡皮筋”效应现象自己控制的角色移动时偶尔会抖动或向后回弹一小段距离。根源通常是客户端预测移动与服务器权威位置不一致且调和过程不顺畅。排查与解决检查移动逻辑的确定性确保客户端和服务器在相同的输入下计算出完全相同的速度和位移。任何微小的差异如使用Time.deltaTime而非Runner.DeltaTime浮点数精度问题物理引擎的非确定性都会导致预测错误。强制使用Runner.DeltaTime在FixedUpdateNetwork中所有与时间相关的计算必须使用Runner.DeltaTime而不是Time.deltaTime或Time.fixedDeltaTime。避免UnityEngine.Random在游戏逻辑中使用UnityEngine.Random会导致不同客户端结果不同。应使用Fusion提供的Runner.GetRandom()它是基于种子的确定性随机数生成器。简化物理尽量避免在核心移动逻辑中使用Rigidbody的AddForce等非确定性方法。使用CharacterController或自己计算速度位移通常更可控。调整网络平滑参数在Fusion的NetworkProjectConfig资产中调整Interpolation和Extrapolation设置。增加插值延迟稍微增加Interpolation Delay如从2调到3可以给Fusion更多缓冲区来处理延迟波动使运动更平滑但会引入少量输入延迟感。禁用外推将Extrapolation模式设为None。外推预测未来位置在网络条件差时很容易出错导致角色“飞”出去再拉回来。验证NetworkTransform设置如果你的移动由NetworkTransform同步检查其Interpolation模式。对于玩家角色Interpolate通常比Extrapolate更稳定。5.2 陷阱二其他玩家角色动作卡顿或瞬移现象你看其他玩家的移动不流畅像在“瞬移”。根源网络延迟、丢包或插值设置不当。排查与解决确保插值开启确认其他玩家角色的NetworkObject上Interpolation Data Source不是None。通常设为Auto或Snapshots。检查带宽和延迟在游戏运行时打开Fusion的统计面板通常通过NetworkDebugRunner或按某个快捷键。观察RTT往返时间和丢包率。高延迟150ms或高丢包率5%必然导致卡顿。这可能是网络环境问题也可能是你同步的数据量太大了。优化网络变量使用[Networked, Accuracy(...)]和[Networked, Precision(...)]减少浮点数同步的精度。将多个相关的bool合并为一个NetworkButtons或使用位掩码[Networked] public byte StatusFlags { get; set; }。对于不常变化的状态如玩家名称、队伍使用OnChanged回调而不是每帧同步。使用兴趣管理如果场景中对象很多使用Fusion的Interest兴趣管理系统只同步玩家附近的对象可以大幅减少带宽。5.3 陷阱三RPC调用无效或重复执行现象发射子弹的RPC没反应或者一个动作触发了多次。根源RPC的调用条件或执行目标理解有误。排查与解决仔细检查RpcSources和RpcTargets这是最常见的错误来源。一个典型的射击逻辑应该是[Rpc(RpcSources.InputAuthority, RpcTargets.StateAuthority)]客户端调用在服务器执行权威逻辑生成子弹、计算伤害。服务器生成子弹后子弹的移动由NetworkTransform或网络属性同步。命中效果、音效等表现类RPC可以由服务器调用RpcTargets.All在所有客户端播放。确保在正确的时机调用RPC应该在FixedUpdateNetwork中或由它触发的事件中调用。避免在Update或协程中直接调用除非你能确保网络状态正确。处理重复调用对于按钮按下事件使用GetPressed如前文所示来检测“按下”的瞬间而不是每帧都调用。或者在RPC内部加一个“冷却时间”或状态检查。5.4 调试神器Fusion Stats GUI 与 Network DebugFusion Stats GUI在NetworkRunner的预制体或场景对象上通常可以找到一个启用Enable Stats GUI的选项。运行时屏幕上会显示关键的实时数据RTT往返延迟。绿色良好黄色警告红色糟糕。Packet Loss丢包率。Objects当前活跃的网络对象数量。In/Out Bit/s网络带宽占用。可以快速判断是否数据同步过多。Network Debugger在Unity编辑器的Window - Fusion - Network Debugger中打开。这是一个强大的工具可以查看所有网络对象它们的ID、状态权威、输入权威。监视网络变量实时查看每个网络属性的值以及它们的变化历史。模拟网络条件可以手动增加延迟、丢包、乱序测试游戏在恶劣网络下的表现。逐Tick调试可以暂停游戏一帧一帧地前进观察每个Tick输入、状态和RPC的变化是定位同步问题的终极武器。6. 性能优化与最佳实践当你的游戏人数增多对象变多时性能问题就会浮现。以下是一些共享模式下的优化经验。6.1 带宽优化带宽是多人游戏的生命线。优先级状态同步 RPC尽量使用网络属性来同步持续状态而不是用RPC频繁发送事件。RPC更适合低频、离散的事件。压缩与精度如前所述充分利用Accuracy和Precision。一个Vector3的精度从0.0001降到0.01带宽节省非常可观。变化检测Fusion只会同步发生变化的网络属性。确保你的属性在值没变时不要去赋值。例如如果速度是零向量就不要每帧都NetworkedVelocity Vector3.zero。使用NetworkString同步字符串时务必使用NetworkString_长度它有内置的池化和压缩。6.2 计算性能优化减少FixedUpdateNetwork中的开销这是执行最频繁的网络逻辑层。避免在这里做复杂的计算、查找FindObjectOfType,GetComponent、或分配内存如new List()。将结果缓存起来。分帧处理如果有很多非玩家对象需要每Tick更新可以考虑将它们分组在不同的Tick更新不同的组分散CPU压力。使用NetworkAreaOfInterest这是Fusion内置的兴趣管理系统。为你的网络对象添加NetworkAreaOfInterestBehaviour并为玩家设置NetworkInterest。这样玩家只会收到其感兴趣区域AOI内的对象更新极大减少同步量和处理负载。6.3 架构设计建议明确的状态机对于复杂的角色状态闲置、移动、攻击、死亡使用一个明确的网络化状态机[Networked] public State CurrentState { get; set; }。状态转换在FixedUpdateNetwork中基于输入和条件进行并通过OnChanged回调触发动画和特效。这比用多个布尔值或RPC来管理要清晰和高效得多。分离逻辑与表现坚持FixedUpdateNetwork处理逻辑和网络状态Render处理视觉平滑和特效。不要在FixedUpdateNetwork里直接播放动画或修改Transform除非是NetworkTransform。使用OnChanged回调或从Render中查询网络状态来驱动表现层。预制体与网络对象池频繁生成/销毁网络对象如子弹、特效会产生开销。使用Fusion的NetworkObjectPool系统进行对象池化管理可以显著提升性能。共享模式是Fusion提供的强大工具它用一定的复杂性换来了灵活性、较低的服务器成本和良好的玩家体验。理解其“输入-权威-同步”的核心数据流谨慎处理预测与调和的边界善用调试工具你就能驾驭这股力量打造出稳定流畅的多人游戏体验。记住多人游戏开发永远是关于状态、时间和信任的管理而Fusion为你提供了一套强大的基础设施剩下的就是你对游戏逻辑的精确实现了。