
1. 项目概述为什么选择Mirror来啃VR联机这块硬骨头如果你正在Unity里折腾VR项目并且动了让多个玩家在同一个虚拟空间里互动的心思那你大概率已经站在了“网络同步”这座大山面前。VR联机开发尤其是基于Unity的从来都不是一件轻松的事。它不像传统的PC或手游联机你不仅要处理角色位置、动画、状态这些基础同步还得额外照顾到头显HMD和手柄控制器Controller的六自由度6DoF姿态数据——这意味着每帧都要传输大量的旋转和位置信息延迟和抖动会直接破坏沉浸感让用户头晕目眩。几年前大家可能会首选UNetUnity官方的高层API网络系统但它早已被标记为弃用。社区里涌现过不少方案而Mirror凭借其开源、高性能、以及对UNet HLAPI的高度兼容性成为了许多独立开发者和中小团队的实际选择。它本质上是一个社区维护、功能增强版的UNet你之前为UNet写的很多代码几乎可以无缝迁移过来这大大降低了学习和迁移成本。对于VR项目来说Mirror提供了足够的底层控制力让我们能精细地处理每一份网络数据包同时又保留了相对友好的高层抽象比如NetworkManager、NetworkTransform和NetworkAnimator。所以这个实战系列的第一篇我们不谈空洞的理论直接从零开始搭建一个最基础的、可运行的VR多人联机原型。目标很明确让两个或更多的VR玩家进入同一个场景能看到彼此由头显和手柄驱动的虚拟化身Avatar并能进行基础的交互比如挥手、抓取物体。我们会使用Unity的XR Interaction Toolkit目前Unity官方主推的VR开发框架和Mirror一步步拆解其中的关键技术和避坑要点。2. 环境准备与核心工具链搭建工欲善其事必先利其器。在动手写代码之前确保你的开发环境是正确且完整的这能避免后续无数诡异的问题。2.1 Unity版本与XR插件管理首先Unity版本的选择至关重要。对于现代VR开发我强烈建议使用Unity 2021.3 LTS或Unity 2022.3 LTS这些长期支持版本。它们稳定性好对XR插件的支持也最成熟。本教程将以Unity 2021.3 LTS为例。安装Unity Hub并创建项目通过Unity Hub安装指定版本的Unity Editor。创建一个新的3D项目URP或Built-in渲染管线均可URP在移动端VR上更有优势但Built-in更稳定通用初学者可从Built-in开始。启用XR Plugin Management打开Window - Package Manager在Unity Registry中搜索并安装“XR Plugin Management”。安装后Unity会提示你安装对应平台的XR插件。对于Meta QuestAndroid平台你需要安装“OpenXR Plugin”或“Oculus XR Plugin”。目前OpenXR是更开放、更面向未来的标准建议优先选择。对于PC VR如SteamVR同样安装“OpenXR Plugin”并配置相应的运行时。安装XR Interaction Toolkit在Package Manager中搜索并安装“XR Interaction Toolkit”。这是Unity官方提供的、用于构建VR交互的核心框架它包含了输入系统、射线交互、抓取、传送等一套完整的组件。安装后建议从Window - XR - Interaction Toolkit - Starter Assets导入官方提供的示例资产和预设这能极大加速我们的开发。2.2 Mirror网络插件的导入与配置接下来是网络部分的核心——Mirror。获取Mirror最推荐的方式是通过Unity的Package Manager使用Git URL添加。在Package Manager中点击左上角的“”号选择“Add package from git URL”然后输入https://github.com/vis2k/Mirror.git。这种方式可以方便地更新到最新版本。基础检查导入完成后你的项目里会出现Mirror的文件夹。为了测试Mirror是否正常工作你可以先创建一个空场景然后从Assets/Mirror/Examples/下随便拖一个示例场景比如“Basic”运行一下。如果能正常作为主机Host运行并看到网络对象说明Mirror基础环境OK。了解核心组件Mirror的核心组件包括NetworkManager网络管理的大脑负责启动服务器、客户端、管理玩家预制体生成、场景切换等。NetworkIdentity任何需要在网络上存在的GameObject都必须挂载此组件。它是网络对象的“身份证”。NetworkTransform用于同步GameObject的位置、旋转和缩放。但对于VR设备我们通常需要自定义同步逻辑因为默认的NetworkTransform可能无法满足高频率、低延迟的6DoF数据同步需求。NetworkBehaviour你的自定义网络脚本必须继承自这个类。它提供了诸如[Command]、[ClientRpc]、[SyncVar]等属性用于定义服务器和客户端之间的通信。2.3 项目初始结构规划在开始编码前规划好文件夹结构能让后续开发有条不紊。建议在Assets下创建如下目录Assets/ ├── _Project/ │ ├── 01_Scenes/ # 存放所有场景文件 │ ├── 02_Scripts/ # 存放所有C#脚本 │ │ ├── Network/ # 网络相关脚本 │ │ ├── VR/ # VR交互相关脚本 │ │ └── Utilities/ # 工具类脚本 │ ├── 03_Prefabs/ # 预制体 │ │ ├── VRPlayer/ # VR玩家预制体 │ │ └── NetworkedObjects/ # 网络同步物体预制体 │ ├── 04_Art/ # 模型、材质、纹理等 │ └── 05_Settings/ # 各种配置文件、ScriptableObjects3. 构建基础VR玩家本地化身在考虑网络之前我们首先要有一个能在本地正常运行的VR玩家。这一步我们利用XR Interaction Toolkit快速搭建。3.1 创建XR Origin并配置基础移动生成XR Origin在Hierarchy中右键 -XR-XR Origin (Action-based)。这会创建一个包含Camera Offset、Main Camera以及左右手Controller的游戏对象结构。配置输入动作Input ActionsXR Interaction Toolkit 2.0版本严重依赖新的Input System。你需要一个.inputactions资产文件来定义所有控制输入如手柄摇杆、按钮。最简单的方法是直接从导入的Starter Assets中找到XR Device Simulator使用的那个Input Action Asset通常叫XRI Default Input Actions并将其拖拽到XR Origin上ActionBasedController组件的Controller字段中。添加移动能力为XR Origin添加CharacterController组件和一个CapsuleCollider用于物理碰撞。添加Locomotion System、Teleportation Provider组件。添加Continuous Move Provider (Action-based)组件并将其Move Action关联到左手柄摇杆的Vector2输入。这样推动左手摇杆就能实现平滑移动。添加Snap Turn Provider (Action-based)组件并将其Turn Action关联到右手柄摇杆的X轴输入。这样向右推动右手摇杆可以瞬间右转45度或其他设定角度。测试本地运行此时运行场景你应该已经可以通过手柄摇杆移动和转向了。头显的视角旋转是自动由XR系统驱动的。3.2 创建可视化的手部模型为了让其他玩家能看到我们的手我们需要为手柄控制器附加一个3D手部模型。准备模型你可以使用简单的方块Cube作为占位符也可以导入更精细的手部模型。将模型做成预制体。关联模型与控制器在XR Origin下的LeftHand Controller和RightHand Controller子物体上找到XR Controller (Action-based)组件或Model Parent字段。通常你可以创建一个空物体作为手部模型的父级将其作为“视觉渲染”部分然后通过脚本使其跟随控制器运动。更简单的方式是直接将手部模型预制体拖拽为Controller的子物体并调整其位置旋转使其与手柄物理位置匹配。关键点模型与输入解耦记住这里的3D手部模型仅仅是为了视觉表现。真正的交互逻辑如抓取、射线是由XR Direct Interactor或XR Ray Interactor组件处理的。我们需要确保网络同步的是控制器的姿态数据然后用这个数据去驱动视觉模型而不是反过来。4. 将本地VR玩家升级为网络实体这是最核心的一步。我们需要让这个本地VR玩家预制体能够在网络上被生成、同步和控制。4.1 创建网络化的VR玩家预制体制作预制体将你在场景中配置好的XR Origin包含所有子物体拖入03_Prefabs/VRPlayer/文件夹创建一个预制体命名为NetworkVRPlayer。添加Mirror核心组件在预制体根节点即XR Origin上添加NetworkIdentity组件。勾选Local Player Authority。这意味着这个玩家对象将由对应的客户端控制。仍然在根节点添加一个自定义脚本例如NetworkVRPlayer.cs这个脚本必须继承自NetworkBehaviour。设计网络同步变量SyncVars我们需要同步哪些数据对于VR玩家最核心的是头显Main Camera的位置和旋转。左手控制器的位置和旋转。右手控制器的位置和旋转。一些基础状态如玩家ID、颜色等。 在NetworkVRPlayer脚本中我们可以定义[SyncVar]。但注意[SyncVar]更适合同步简单的值类型如int, float, bool或一些结构体对于每帧都在高速变化的Transform数据使用[SyncVar]效率极低。因此我们将采用自定义的网络消息Network Messages或更高效的状态同步在Update或FixedUpdate中发送。4.2 实现6DoF数据的自定义网络同步Mirror的NetworkTransform组件对于常规游戏角色可能够用但对于VR设备的高频6DoF数据其插值和压缩设置可能不理想容易导致“抖动”或“延迟感”。我们需要自己实现一个轻量级的同步方案。定义姿态数据结构首先创建一个可序列化的结构体来封装一个设备头或手的姿态。[System.Serializable] public struct VRDevicePose { public Vector3 position; public Quaternion rotation; // 你可以根据需要添加其他数据如手柄按钮状态 // public bool triggerPressed; // public float gripValue; }在NetworkVRPlayer中收集和发送数据using Mirror; using UnityEngine.XR; public class NetworkVRPlayer : NetworkBehaviour { // 引用本地VR设备的Transform [SerializeField] private Transform headTransform; [SerializeField] private Transform leftHandTransform; [SerializeField] private Transform rightHandTransform; // 上一次发送的姿态用于脏数据检查减少不必要的数据发送 private VRDevicePose lastHeadPose; private VRDevicePose lastLeftHandPose; private VRDevicePose lastRightHandPose; // 发送频率控制每秒发送次数 [SerializeField] private float sendRate 20f; // 20Hz private float lastSendTime; public override void OnStartLocalPlayer() { base.OnStartLocalPlayer(); // 只有本地玩家才需要获取和发送数据 if (isLocalPlayer) { // 找到场景中的XR Origin部件并赋值 // 这部分代码需要根据你的XR Origin结构来写 // 例如headTransform Camera.main.transform; } // 非本地玩家的视觉模型可能需要禁用本地输入组件只保留渲染 else { DisableLocalComponents(); } } void Update() { // 只有本地玩家才执行发送逻辑 if (!isLocalPlayer) return; // 控制发送频率 if (Time.time - lastSendTime 1f / sendRate) return; SendPoseUpdate(); lastSendTime Time.time; } void SendPoseUpdate() { VRDevicePose headPose new VRDevicePose { position headTransform.position, rotation headTransform.rotation }; // 简单脏数据检查如果姿态变化超过阈值才发送 if (PoseChangedSignificantly(headPose, lastHeadPose)) { CmdUpdateHeadPose(headPose); lastHeadPose headPose; } // 同理发送左手和右手数据... } bool PoseChangedSignificantly(VRDevicePose newPose, VRDevicePose oldPose) { // 根据位置和旋转的变化量设定一个阈值来判断 float posThreshold 0.001f; float rotThreshold 0.1f; // 角度差 return Vector3.Distance(newPose.position, oldPose.position) posThreshold || Quaternion.Angle(newPose.rotation, oldPose.rotation) rotThreshold; } [Command(channel Channels.Unreliable)] // 使用不可靠通道允许丢包追求低延迟 void CmdUpdateHeadPose(VRDevicePose pose) { // 服务器收到命令后广播给所有其他客户端 RpcApplyHeadPose(pose); } [ClientRpc(channel Channels.Unreliable, includeOwner false)] // 不包含所有者因为本地玩家已有最新数据 void RpcApplyHeadPose(VRDevicePose pose) { // 所有其他客户端非本地玩家在这里应用接收到的姿态 if (!isLocalPlayer) // 确保不应用到本地玩家自己身上 { // 这里直接设置位置旋转或者使用插值平滑处理 headTransform.position pose.position; headTransform.rotation pose.rotation; } } // ... 为左手和右手实现类似的Cmd和Rpc方法 }关键提示这里使用了[Command]和[ClientRpc]进行同步。[Command]是从客户端调用在服务器上执行[ClientRpc]是从服务器调用在所有客户端执行。我们让本地玩家收集自己的姿态通过Cmd发送给服务器服务器再通过Rpc广播给其他所有玩家。includeOwner false是为了避免数据回传给自己造成不必要的计算和干扰。使用Unreliable通道是为了速度牺牲一点可靠性偶尔丢帧对VR姿态影响可能不大但延迟降低感知明显。4.3 处理本地与非本地玩家的组件差异一个常见的坑是网络生成的玩家预制体对于本地玩家它需要处理输入和驱动相机对于远程玩家其他玩家看到的你它只需要显示同步过来的模型动画必须禁用本地输入控制否则会出现多个输入争夺相机控制权的混乱。在OnStartLocalPlayer方法中我们已经做了区分。DisableLocalComponents方法需要实现如下功能private void DisableLocalComponents() { // 1. 禁用或销毁本地玩家的Camera组件 Camera cam GetComponentInChildrenCamera(); if (cam ! null) cam.enabled false; // 或 Destroy(cam); // 2. 禁用XR相关的输入和交互组件防止它们响应本地输入 var inputProviders GetComponentsInChildrenLocomotionProvider(); foreach (var provider in inputProviders) provider.enabled false; var interactors GetComponentsInChildrenXRBaseInteractor(); foreach (var interactor in interactors) interactor.enabled false; // 3. 可能还需要禁用AudioListener AudioListener audioListener GetComponentInChildrenAudioListener(); if (audioListener ! null) audioListener.enabled false; // 4. 对于非本地玩家的手部模型可以启用一个“远程手”的Shader或材质以示区别 Renderer[] renderers GetComponentsInChildrenRenderer(); foreach (Renderer rend in renderers) { // 例如设置为半透明或不同颜色 // rend.material.color new Color(1,1,1,0.5f); } }5. 配置NetworkManager与首次联机测试现在我们已经有了一个网络化的VR玩家预制体接下来需要配置网络管理器来使用它。5.1 创建并配置NetworkManager创建NetworkManager在场景中创建一个空游戏对象命名为“NetworkManager”。为其添加Mirror的NetworkManager组件和KCP Transport或Telepathy Transport组件KCP在需要可靠UDP时性能更好Telepathy是简单的TCP传输对于原型开发够用。关联玩家预制体在NetworkManager组件的Player Prefab字段中拖入我们之前创建的NetworkVRPlayer预制体。设置离线场景与在线场景在NetworkManager的Offline Scene和Online Scene字段中填入你的场景名称。这允许网络管理器在断开连接或停止主机时自动切换场景。配置生成位置默认情况下玩家会在NetworkStartPosition组件标记的位置生成。你可以在场景中放置几个空物体挂上NetworkStartPosition组件网络管理器会随机选择其中一个作为玩家出生点。5.2 创建简单的UI控制界面为了方便测试我们创建一个简单的Canvas UI包含几个按钮Host (Server Client)作为主机启动既当服务器又当本地客户端。Client作为客户端连接到指定地址。Server Only仅作为服务器启动。Stop停止连接。为这些按钮编写脚本调用NetworkManager.singleton的相应方法using Mirror; using UnityEngine.UI; public class SimpleNetworkHUD : MonoBehaviour { public InputField addressInput; // 输入服务器地址如 localhost 或 192.168.1.100 public Button hostButton; public Button clientButton; public Button serverButton; public Button stopButton; void Start() { hostButton.onClick.AddListener(() NetworkManager.singleton.StartHost()); clientButton.onClick.AddListener(() NetworkManager.singleton.StartClient()); serverButton.onClick.AddListener(() NetworkManager.singleton.StartServer()); stopButton.onClick.AddListener(() NetworkManager.singleton.StopHost()); // 默认地址 addressInput.text localhost; addressInput.onEndEdit.AddListener((text) NetworkManager.singleton.networkAddress text); } }5.3 首次本地测试与问题排查构建测试在Unity编辑器中直接点击运行。先点击“Host”按钮。你应该能看到自己的VR玩家正常生成并且可以自由移动。启动第二个客户端Unity编辑器允许你通过File - Build and Run构建出一个独立的可执行文件比如一个.exe。构建时在Build Settings中取消勾选Development Build以加快构建速度。运行这个构建好的程序点击“Client”按钮连接到localhost。观察现象理想情况在两个窗口里你都能看到两个VR玩家模型。你自己控制一个另一个模型会随着你第二个客户端中头显和手柄的运动而运动。常见问题1看不到其他玩家。检查NetworkManager的玩家预制体是否正确赋值检查NetworkVRPlayer脚本的OnStartLocalPlayer中是否正确获取了Transform引用检查Cmd和Rpc方法是否被正确调用可以在方法内加Debug.Log。常见问题2模型位置错乱或抖动严重。这通常是坐标系或父子级关系问题。确保你同步的position和rotation是世界坐标World Space。对于子物体如手是头的子物体在非本地玩家身上你可能需要直接同步世界坐标或者确保本地玩家发送的是相对于某个公共根节点的局部坐标并在远程客户端上正确应用。抖动可能源于发送频率与网络更新频率不匹配或者没有在远程客户端做插值Lerp平滑处理。我们将在下一节优化。6. 网络同步优化与状态插值直接每帧设置远程玩家的位置旋转会导致视觉上的剧烈抖动因为网络数据包到达是不均匀的。我们必须进行插值平滑。6.1 实现网络姿态的插值修改NetworkVRPlayer脚本中接收姿态的部分不直接赋值而是设置一个目标姿态然后在Update中平滑地向目标移动。public class NetworkVRPlayer : NetworkBehaviour { // ... 之前的变量 ... // 用于插值的目标姿态 private VRDevicePose targetHeadPose; private VRDevicePose currentHeadPose; // 当前渲染使用的姿态 // 为左手和右手也定义类似的变量 [SerializeField] private float interpolationSpeed 15f; // 插值速度 void Update() { if (isLocalPlayer) { // ... 本地玩家的发送逻辑 ... } else { // 远程玩家在Update中平滑插值 InterpolatePoses(); } } void InterpolatePoses() { // 对头部姿态进行插值 currentHeadPose.position Vector3.Lerp(currentHeadPose.position, targetHeadPose.position, Time.deltaTime * interpolationSpeed); currentHeadPose.rotation Quaternion.Slerp(currentHeadPose.rotation, targetHeadPose.rotation, Time.deltaTime * interpolationSpeed); // 应用插值后的姿态到Transform headTransform.position currentHeadPose.position; headTransform.rotation currentHeadPose.rotation; // 对左手和右手进行同样的插值处理... } [ClientRpc(channel Channels.Unreliable, includeOwner false)] void RpcApplyHeadPose(VRDevicePose pose) { if (!isLocalPlayer) { // 不再直接赋值而是更新目标姿态 targetHeadPose pose; // 如果是第一次收到数据直接设置当前姿态避免从原点开始插值 if (currentHeadPose.position Vector3.zero) { currentHeadPose pose; } } } // ... }6.2 处理网络延迟与预测对于VR这种高实时性应用网络延迟Lag是致命的。除了插值我们还可以考虑一些进阶策略客户端预测Client-side Prediction对于本地玩家的移动我们立即应用而不是等待服务器确认。这能带来即时的反馈。但需要服务器做权威验证和位置纠正实现复杂。实体插值Entity Interpolation这是上面我们已经做的。让远程实体比世界时间“慢”一点点总是渲染其过去的状态从而有稳定的数据流进行平滑。滞后补偿Lag Compensation在射击等游戏中服务器在判断命中时会回滚到玩家开枪时的世界状态。在VR交互中对于抓取等动作也可能需要类似的技巧。对于我们的基础原型先做好稳健的插值已经能大幅改善观感。你可以调整sendRate如提升到30Hz和interpolationSpeed来在平滑度和响应性之间找到平衡。7. 基础VR交互的网络同步以抓取为例让玩家看到彼此只是第一步能一起交互才是联机的乐趣。我们以最典型的“抓取并移动物体”为例讲解如何同步VR交互。7.1 创建可被抓取的网络物体制作预制体创建一个简单的物体如Cube为其添加NetworkIdentity组件。取消勾选Local Player Authority因为物体通常由服务器权威控制。添加可交互组件添加XR Grab Interactable组件来自XR Interaction Toolkit使其可以被VR手柄抓取。创建网络脚本添加一个脚本例如NetworkedGrabbable.cs继承自NetworkBehaviour。7.2 同步抓取状态与所有权在Mirror中一个网络物体在某个时刻只能由一个客户端拥有“权限”Authority。抓取的本质就是所有权的转移。using Mirror; using UnityEngine.XR.Interaction.Toolkit; public class NetworkedGrabbable : NetworkBehaviour { private XRGrabInteractable grabInteractable; private Rigidbody rb; void Start() { grabInteractable GetComponentXRGrabInteractable(); rb GetComponentRigidbody(); // 监听抓取和释放事件 grabInteractable.selectEntered.AddListener(OnGrabbed); grabInteractable.selectExited.AddListener(OnReleased); } private void OnGrabbed(SelectEnterEventArgs args) { // 只有本地玩家抓取时才请求权限 if (isClient NetworkClient.localPlayer ! null) { CmdRequestAuthority(NetworkClient.localPlayer.connectionToClient); } } [Command(requiresAuthority false)] // 允许无权限者调用 void CmdRequestAuthority(NetworkConnectionToClient requester) { // 服务器检查如果物体当前没有被抓取或允许强制转移则转移权限 NetworkIdentity netIdentity GetComponentNetworkIdentity(); if (netIdentity.connectionToClient null) // 如果当前没有所有者 { netIdentity.AssignClientAuthority(requester); RpcOnGrabbed(); // 通知所有客户端物体被抓住了 } // 也可以在这里加入更复杂的逻辑比如判断谁先抓取 } [ClientRpc] void RpcOnGrabbed() { // 在所有客户端上可以播放抓取音效、改变物体外观等 Debug.Log(${gameObject.name} was grabbed on all clients.); } private void OnReleased(SelectExitEventArgs args) { if (isClient NetworkClient.localPlayer ! null hasAuthority) { CmdReleaseAuthority(); } } [Command] void CmdReleaseAuthority() { NetworkIdentity netIdentity GetComponentNetworkIdentity(); netIdentity.RemoveClientAuthority(); // 服务器可能需要同步物体被释放后的状态如速度 RpcOnReleased(rb.velocity, rb.angularVelocity); } [ClientRpc] void RpcOnReleased(Vector3 velocity, Vector3 angularVelocity) { // 在所有客户端上应用被释放时的物理状态 if (rb ! null) { rb.velocity velocity; rb.angularVelocity angularVelocity; } } void Update() { // 如果这个客户端拥有该物体的权限则将其Transform同步给服务器和其他客户端 if (hasAuthority isClient) { // 我们可以像同步玩家姿态一样定期通过Command发送物体的位置旋转 // 或者更简单但负载高的方式使用Mirror的NetworkTransform组件。 // 对于抓取物NetworkTransform通常可以接受因为抓取时物体数量有限。 } } // 当权限发生变化时 public override void OnStartAuthority() { base.OnStartAuthority(); // 获得权限后可以启用更精细的本地控制比如禁用物理预测等 if (rb ! null) { rb.isKinematic false; // 获得权限后由本地物理驱动 } } public override void OnStopAuthority() { base.OnStopAuthority(); // 失去权限后物体应由网络同步数据驱动本地物理应设为Kinematic避免干扰 if (rb ! null) { rb.isKinematic true; } } }核心逻辑当本地玩家抓取物体时客户端向服务器发送一个CmdRequestAuthority请求。服务器批准后通过AssignClientAuthority将物体的控制权转移给该客户端。拥有权限的客户端可以自由移动该物体通过其本地的XR交互系统并需要负责将物体的新位置同步给服务器和其他客户端可以通过NetworkTransform或自定义同步。当释放物体时客户端释放权限服务器可能将物体设为“无主”状态并同步其最终的物理速度。7.3 使用NetworkTransform简化同步对于抓取物使用Mirror自带的NetworkTransform组件来同步位置旋转是一个快速可行的方案。只需在物体预制体上添加NetworkTransform组件并适当调整其同步间隔和插值设置。但要注意当客户端拥有物体权限时NetworkTransform的同步方向是“客户端-服务器-其他客户端”。你需要确保在OnStartAuthority和OnStopAuthority中对NetworkTransform或Rigidbody进行正确的配置避免冲突。8. 实测部署与性能考量完成以上步骤后你应该已经拥有了一个基础但功能完整的VR多人联机原型。接下来是更实际的测试和优化。8.1 局域网与远程连接测试局域网测试让两台PC连接在同一个路由器下。在一台机器上运行构建的程序作为主机Host另一台作为客户端Client在客户端输入主机的本地IP地址如192.168.1.xxx进行连接。这是测试网络功能最直接的方式。端口转发与云服务器如果想通过互联网连接主机需要拥有公网IP或在路由器上设置端口转发默认Mirror使用7777 TCP/UDP端口。更简单的方法是租用一台云服务器如阿里云、腾讯云的轻量应用服务器在服务器上以“Server Only”模式运行构建的程序然后所有客户端都连接到服务器的公网IP。8.2 VR联机性能监控与瓶颈分析VR应用本身对性能要求极高加上网络同步性能优化是永恒的主题。带宽使用Unity的NetworkManager中的Network Statistics组件或Mirror的NetworkDiagnostics监控发送和接收的数据量。一个VR玩家的6DoF数据3个Transform每个含Position和Rotation如果以20Hz频率发送未经优化可能达到每秒数十KB。使用压缩如将Quaternion压缩为较小的格式、降低发送频率、优化脏数据检查可以显著降低带宽。CPU开销大量的GameObject、Update中的网络发送逻辑、复杂的插值计算都会消耗CPU。使用对象池管理网络物体优化Update中的逻辑如分帧处理对于不重要的远程实体降低其同步频率。渲染开销多个玩家意味着更多的模型和骨骼需要渲染。使用LOD多层次细节、对视锥体外的玩家进行剔除、使用高效的Avatar系统如使用GPU Skinning来减轻压力。8.3 常见问题速查与解决方案问题现象可能原因排查步骤与解决方案客户端连接失败防火墙/杀毒软件阻止、端口未开放、地址错误、服务器未启动1. 检查服务器程序是否已运行。2. 确认客户端输入的IP和端口正确。3. 关闭防火墙或添加规则允许程序通过。4. 服务器端确保端口已在安全组/防火墙中放行。能看到玩家但模型不动或抖动网络同步脚本未生效、数据未发送/接收、插值参数不当、坐标系错误1. 在CmdUpdatePose和RpcApplyPose中添加Debug.Log检查是否被调用及数据值。2. 检查isLocalPlayer判断是否正确。3. 调整sendRate和interpolationSpeed。4. 确认同步的是世界坐标。抓取物体不同步权限未成功转移、NetworkTransform冲突、事件未正确触发1. 在CmdRequestAuthority和OnStartAuthority中加日志检查权限流。2. 检查抓取物是否有NetworkIdentity和NetworkTransform。3. 确保XRGrabInteractable的事件监听已绑定。非本地玩家模型位置偏移预制体中的Transform引用错误、父子层级关系问题1. 确保NetworkVRPlayer脚本中headTransform等字段在预制体模式下已正确赋值。2. 检查非本地玩家是否错误地继承了某些本地坐标系的偏移。尝试在远程玩家初始化时将其视觉模型根节点与世界坐标系对齐。高延迟下交互体验差网络延迟高、无预测或补偿机制1. 选择物理距离近的服务器。2. 对于抓取等操作尝试实现简单的外观预测客户端先显示抓取效果等待服务器确认。3. 降低对实时性要求极高的交互的精度预期。这个实战篇的第一部分我们从零搭建了VR联机的骨架。核心在于理解Mirror的权威架构服务器-客户端以及如何将XR Interaction Toolkit的本地输入与Mirror的网络通信结合起来。我们实现了玩家化身的6DoF同步并探讨了交互物体同步的基本思路。这只是一个起点后续可以在此基础上添加语音聊天、Avatar动画同步、更复杂的交互如双手攀爬、房间管理、匹配系统等。记住VR联机开发是性能和体验的平衡艺术每一步优化都需要仔细测量和权衡。