Unity NGO 系列教程(四):多人抓取的权限争夺

前言

在多人协作(如电力巡检仿真、XR 实验室)中,"抓取"不仅是把东西拿起来,它涉及到网络同步中最核心的矛盾:本地操作的即时性 vs 服务器数据的权威性。如果单纯依赖服务器仲裁,高延迟下的抓取会感觉异常粘滞;如果完全信任客户端,又会出现物体在多人争抢时"瞬间移动"甚至逻辑死锁。本篇将结合 Unity 官方 XR 示例的精髓,手把手教你构建一套高性能的多人抓取系统。

一、 网络抓取的"三大难点"

  • 1.抢占冲突(Race Condition): 两个玩家同时伸手,谁该赢?
  • 2.物理动量丢失(Physics Momentum): 扔东西的瞬间,所有权切换导致速度归零,物体垂直掉落。
  • 3.视觉回弹(Jitter): 获得所有权的瞬间,网络坐标补间导致的视觉闪烁。

二、 抢占式抓取流程

为了实现"零延迟"体验,我们采用**"抢占式请求 + 本地先行渲染"**的策略。

2.1 核心组件设计

我们需要一个 NetworkPhysicsInteractable 组件,它需要具备以下能力:

  • 1.权限自管理: 允许非所有者请求权限。
  • 2.状态锁定: 防止在重置或特定状态下被非法抓取。
  • 3.速度重建: 记录物体位移轨迹,在权限切换时恢复动量。

2.2 核心代码实现

csharp 复制代码
using System.Collections;
using Unity.Netcode;
using UnityEngine;

[RequireComponent(typeof(Rigidbody), typeof(NetworkObject))]
public class NetworkPhysicsInteractable : NetworkBehaviour
{
    // 状态记录:记录最近几帧的位移,用于在所有权切换时重建物理速度
    private Vector3[] m_VelocityBuffer = new Vector3[3];
    private int m_BufferIndex;
    private Vector3 m_PrevPos;

    // 状态锁:用于同步该物体是否处于被锁定的特殊状态(如 spawn 保护)
    public NetworkVariable<bool> IsLocked = new(false);

    private Rigidbody m_Rigidbody;
    private NetworkObject m_NetObj;

    void Awake()
    {
        m_Rigidbody = GetComponent<Rigidbody>();
        m_NetObj = GetComponent<NetworkObject>();
    }

    void Update()
    {
        // 每一帧都在本地计算"感知速度",不依赖 Rigidbody.velocity
        // 因为在网络同步模式下,Rigidbody 往往是 Kinematic 的
        CalculateInstantaneousVelocity();
    }

    private void CalculateInstantaneousVelocity()
    {
        Vector3 currentVel = (transform.position - m_PrevPos) / Time.deltaTime;
        m_VelocityBuffer[m_BufferIndex] = currentVel;
        m_BufferIndex = (m_BufferIndex + 1) % m_VelocityBuffer.Length;
        m_PrevPos = transform.position;
    }

    /// <summary>
    /// 核心逻辑:发起抓取请求
    /// </summary>
    public void RequestOwnershipAndGrab()
    {
        if (IsOwner || IsLocked.Value) return;

        // 1. 本地先行:立即关闭网络位移同步,防止服务器旧数据覆盖本地手部动作
        var netTransform = GetComponent<NetworkTransform>();
        if (netTransform != null) netTransform.enabled = false;

        // 2. 物理预处理:为了手感流畅,本地先设为 Kinematic
        m_Rigidbody.isKinematic = true;

        // 3. 提交权限变更请求(NGO 会在底层处理先到先得的仲裁)
        m_NetObj.ChangeOwnership(NetworkManager.Singleton.LocalClientId);
        
        // 4. 开启超时检测协程(防止请求丢失导致的物体"假死")
        StartCoroutine(OwnershipTimeoutRoutine());
    }

    public override void OnGainedOwnership()
    {
        // 当服务器确认权限归我后
        var netTransform = GetComponent<NetworkTransform>();
        if (netTransform != null) netTransform.enabled = true; // 恢复同步
        Debug.Log("成功获得物体控制权");
    }
}

三、 为什么这么写更稳定?

3.1 权限模式的抉择

在 NetworkObject 的 Inspector 面板中,记得将 Ownership Permission 设置为 Everyone。为什么: 这样客户端可以直接调用 ChangeOwnership。虽然这看起来放开了权限,但 NGO 会自动按"时间戳顺序"处理并发请求。如果 A 和 B 同时点,服务器会先执行 A 的请求,再执行 B 的,B 的请求会覆盖 A,保证了最终一致性。

3.2 速度重建(Velocity Reconstruction)

这是解决"扔不出去"的关键。当你在 XR 中松开物体时,由于之前它是 Kinematic 的,Rigidbody.velocity 为 0。技巧: 在 OnSelectExited(松开)时,读取我们在 Update 中手动维护的 m_VelocityBuffer 平均值,并手动赋值给 Rigidbody.linearVelocity。

3.3 冲突仲裁:谁快谁赢?

在工业仿真中,如果 A 正在抓着物体,B 过来抢,我们通常有两种逻辑:保护优先: 如果 IsInteracting 为真,拒绝所有权转移。强制接力: 允许 B 抢走,但 A 会触发 OnLostOwnership。组长建议: 建议使用官方示例中的 m_RequestingOwnership 标志位。在发起请求后、获得确认前,不允许再次发起请求。

四、 不同框架下的"抓取"差异

维度 Unity NGO Mirror Photon Fusion
抓取权限 动态 ChangeOwnership 通过 Command 转发 强状态权威(State Auth)
延迟手感 需手动禁用同步组件实现本地先行 较重,通常有延迟 极佳(内置回滚预测)
物理表现 需手动重建速度缓冲区 容易丢动量 自动处理动量继承

五、 总结

  • RTT 考虑: 请求权限后,不要死等。如果 RTT * 2 时间还没拿到权限,强制恢复物体的同步组件,防止玩家看到一个"拿不动也点不了"的死物。
  • 断线收回: 在 OnClientDisconnect 里,服务器务必遍历所有物体,把属于该掉线玩家的物体所有权收回(RemoveOwnership),否则该物体会永久锁定在掉线点。
  • XR 适配: 如果结合 XRI(XR Interaction Toolkit),务必在 OnSelectEntered 时执行权限请求,在获得权限后再允许交互器(Interactor)进行真正的 Parent 绑定。

参考链接:

NGO 官方示例源码: GitHub - XR Multiplayer

网络物理同步进阶: Unity Multiplayer Docs

相关推荐
张老师带你学2 小时前
Unity 低多边形 赛博朋克城市 拼装 模型 道路 建筑 buildin
科技·游戏·unity·游戏引擎·模型
ฅ^•ﻌ•^ฅ12 小时前
Unity mcp并使用claude code制作游戏
游戏·unity·游戏引擎
程序员正茂2 小时前
Unity3d使用SRDebugger屏幕输出调试信息
unity·srdebugger
张老师带你学2 小时前
unity资源 buildin 低多边形 小镇村
科技·游戏·unity·游戏引擎·模型
PassionY2 小时前
Unity NGO 系列教程(五):如何构建多人联机区域触发系统
unity·rpc·ngo·网络触发器·serverrpc·networkvariable·authority
RReality3 小时前
【Unity UGUI】InputField 输入框全解
unity·游戏引擎
南無忘码至尊3 小时前
Unity学习90天-第3天-认识触屏输入(手游基础)并完成手机点击屏幕,物体向点击位置移动
学习·unity·c#·游戏引擎·游戏开发
南無忘码至尊3 小时前
Unity学习90天-第3天-认识C# 集合与常用类并实现生成随机位置的 10 个立方体
学习·unity·c#
mxwin13 小时前
Unity URP 下抓取当前屏幕内容实现扭曲、镜子与全局模糊效果
unity·游戏引擎·shader