You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Unity Netcode库存同步异常:客户端物品功能失效求助

Unity Netcode多人游戏物品系统同步问题排查与修复

问题现象

  • 拾取的物品已成功加入客户端库存,但无法在手中显示;
  • 客户端无法执行物品丢弃操作;
  • 客户端无法看到主机丢弃的物品。

问题成因

1. 物品拾取后客户端无法显示手中物品

  • collectedItems和currentItemIndex是本地变量,未通过Netcode同步:服务器修改这两个变量后,客户端副本的状态完全脱节,导致ActivateItemClientRpc执行时,客户端的collectedItems为空或索引不匹配,无法激活物品。
  • SyncItemInHandClientRpc依赖客户端的collectedItems列表,但该列表仅在服务器端更新,客户端没有同步数据,因此找不到对应物品对象。

2. 客户端无法执行丢弃操作

  • 客户端切换物品时仅在本地修改currentItemIndex,未同步到服务器:服务器端的currentItemIndex与客户端不一致,调用DropCurrentItemServerRpc时,服务器判断索引无效,无法执行丢弃逻辑。

3. 客户端看不到主机丢弃的物品

  • 服务器生成丢弃物品后已调用networkObject.Spawn(true)自动同步,但额外发送SpawnDroppedItemClientRpc让客户端重复生成并Spawn,造成网络对象冲突,同时客户端生成的物品未与服务器端实例绑定,导致同步失败。

修复方案

核心原则:使用NetworkVariable同步所有需要跨端共享的状态,确保服务器是唯一的状态权威,客户端仅通过同步数据更新显示。

1. 同步库存与当前物品索引

自定义可序列化的物品名称列表(NetworkVariable不支持直接序列化List<GameObject>),用NetworkVariable存储库存和当前索引:

[System.Serializable]
public class SerializableItemList
{
    public List<string> itemNames = new List<string>();
}

2. 修正物品切换逻辑

客户端切换物品时调用ServerRpc同步索引到服务器,再由服务器通过NetworkVariable自动同步所有客户端的显示状态。

3. 修正物品丢弃逻辑

服务器生成丢弃物品后仅调用networkObject.Spawn(),Netcode会自动同步该物品到所有客户端,移除客户端手动生成的逻辑。

4. 修正物品拾取同步逻辑

服务器更新NetworkVariable后,客户端通过监听变量变化自动更新本地显示,无需额外ClientRpc重复通知。


修正后的完整代码

PlayerInventory 脚本

using System.Collections.Generic;
using UnityEngine;
using Unity.Netcode;

public class PlayerInventory : NetworkBehaviour
{
    public List<GameObject> allItemsPrefabs; // 存储物品预制体(需在NetworkManager中注册为可Spawn)
    public Transform itemHandTransform; // 物品手持父物体
    private GameObject currentHeldItem;

    private NetworkVariable<SerializableItemList> collectedItemNames = 
        new NetworkVariable<SerializableItemList>(new SerializableItemList(), NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server);
    private NetworkVariable<int> currentItemIndex = 
        new NetworkVariable<int>(-1, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server);
    private const int maxInventorySlots = 3;
    public Transform dropPosition;

    public override void OnNetworkSpawn()
    {
        // 监听变量变化,自动更新本地显示
        collectedItemNames.OnValueChanged += OnCollectedItemsUpdated;
        currentItemIndex.OnValueChanged += OnCurrentItemIndexUpdated;

        // 初始化隐藏所有预制体实例
        foreach (var prefab in allItemsPrefabs)
        {
            var instance = Instantiate(prefab, itemHandTransform);
            instance.SetActive(false);
        }
    }

    private void OnCollectedItemsUpdated(SerializableItemList oldValue, SerializableItemList newValue)
    {
        UpdateHeldItem();
    }

    private void OnCurrentItemIndexUpdated(int oldIndex, int newIndex)
    {
        UpdateHeldItem();
    }

    private void UpdateHeldItem()
    {
        // 隐藏当前手持物品
        if (currentHeldItem != null)
        {
            currentHeldItem.SetActive(false);
        }

        // 激活新物品
        if (currentItemIndex.Value >= 0 && currentItemIndex.Value < collectedItemNames.Value.itemNames.Count)
        {
            var itemName = collectedItemNames.Value.itemNames[currentItemIndex.Value];
            currentHeldItem = allItemsPrefabs.Find(p => p.name == itemName);
            if (currentHeldItem != null)
            {
                currentHeldItem.SetActive(true);
            }
        }
        else
        {
            currentHeldItem = null;
        }
    }

    [ServerRpc(RequireOwnership = false)]
    public void AddItemToInventoryServerRpc(string itemName, ulong networkObjectId)
    {
        if (collectedItemNames.Value.itemNames.Count >= maxInventorySlots)
        {
            Debug.Log("库存已满");
            return;
        }

        if (!collectedItemNames.Value.itemNames.Contains(itemName))
        {
            collectedItemNames.Value.itemNames.Add(itemName);
            collectedItemNames.SetDirty(); // 标记变量需要同步

            // 第一个物品自动选中
            if (currentItemIndex.Value == -1)
            {
                currentItemIndex.Value = 0;
            }

            // 服务器端移除世界中的物品
            if (NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(networkObjectId, out var networkObj))
            {
                networkObj.Despawn();
                Destroy(networkObj.gameObject);
            }
        }
    }

    [ServerRpc(RequireOwnership = false)]
    public void DropCurrentItemServerRpc()
    {
        if (currentItemIndex.Value < 0 || currentItemIndex.Value >= collectedItemNames.Value.itemNames.Count)
        {
            Debug.LogWarning("无物品可丢弃");
            return;
        }

        var itemName = collectedItemNames.Value.itemNames[currentItemIndex.Value];
        var prefab = allItemsPrefabs.Find(p => p.name == itemName);
        if (prefab == null) return;

        // 服务器生成物品并同步到所有客户端
        var droppedItem = Instantiate(prefab, dropPosition.position, dropPosition.rotation);
        var networkObj = droppedItem.GetComponent<NetworkObject>();
        if (networkObj != null)
        {
            networkObj.Spawn();
        }

        // 添加刚体逻辑
        var rb = droppedItem.GetComponent<Rigidbody>() ?? droppedItem.AddComponent<Rigidbody>();
        rb.collisionDetectionMode = CollisionDetectionMode.Continuous;
        rb.mass = 1f;
        rb.AddForce(dropPosition.forward * 1.5f, ForceMode.Impulse);

        // 更新库存
        collectedItemNames.Value.itemNames.RemoveAt(currentItemIndex.Value);
        collectedItemNames.SetDirty();

        // 更新当前索引
        currentItemIndex.Value = collectedItemNames.Value.itemNames.Count > 0 
            ? Mathf.Clamp(currentItemIndex.Value, 0, collectedItemNames.Value.itemNames.Count - 1) 
            : -1;
    }

    [ServerRpc(RequireOwnership = false)]
    public void SwitchItemServerRpc(int direction)
    {
        if (collectedItemNames.Value.itemNames.Count == 0) return;

        var newIndex = currentItemIndex.Value + direction;
        newIndex = newIndex >= collectedItemNames.Value.itemNames.Count ? 0 : newIndex;
        newIndex = newIndex < 0 ? collectedItemNames.Value.itemNames.Count - 1 : newIndex;

        currentItemIndex.Value = newIndex;
    }

    void Update()
    {
        if (!IsOwner || collectedItemNames.Value.itemNames.Count == 0) return;

        if (Input.GetAxis("Mouse ScrollWheel") != 0f)
        {
            var direction = Input.GetAxis("Mouse ScrollWheel") > 0f ? 1 : -1;
            SwitchItemServerRpc(direction);
        }

        if (Input.GetKeyDown(KeyCode.G))
        {
            DropCurrentItemServerRpc();
        }
    }
}

PlayerInteract 脚本

using UnityEngine;
using Unity.Netcode;
using TMPro;

public class PlayerInteract : NetworkBehaviour
{
    public float interactionRange = 3f;
    public Camera playerCamera;
    public GameObject interactUI;
    private TMP_Text interactText;
    private PlayerInventory playerInventory;

    void Start()
    {
        interactText = interactUI?.GetComponent<TMP_Text>();
        playerInventory = GetComponent<PlayerInventory>();
    }

    void Update()
    {
        if (!IsOwner) return;

        var ray = playerCamera.ScreenPointToRay(new Vector3(Screen.width / 2, Screen.height / 2));
        if (Physics.Raycast(ray, out var hit, interactionRange))
        {
            var item = hit.collider.GetComponent<Item>();
            if (item != null)
            {
                ShowInteractUI(item);
                if (Input.GetKeyDown(KeyCode.E))
                {
                    PickupItemServerRpc(item.NetworkObjectId);
                }
            }
            else
            {
                HideInteractUI();
            }
        }
        else
        {
            HideInteractUI();
        }
    }

    void ShowInteractUI(Item item)
    {
        if (interactText != null)
        {
            interactUI.SetActive(true);
            interactText.text = $"按下E拾取 {item.itemName}";
        }
    }

    void HideInteractUI()
    {
        interactUI?.SetActive(false);
    }

    [ServerRpc]
    void PickupItemServerRpc(ulong itemId)
    {
        if (NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(itemId, out var networkObj))
        {
            var item = networkObj.GetComponent<Item>();
            if (item != null)
            {
                playerInventory.AddItemToInventoryServerRpc(item.itemName, itemId);
            }
        }
    }
}

Item 脚本

using UnityEngine;
using Unity.Netcode;

public class Item : NetworkBehaviour
{
    public string itemName;

    // 注意:物品预制体必须添加NetworkObject组件,并在NetworkManager的Spawnable Prefabs中注册
}

额外注意事项

  1. 所有物品预制体必须添加NetworkObject组件,并在NetworkManager的Spawnable Prefabs列表中注册,否则服务器无法同步物品到客户端。
  2. allItemsPrefabs必须存储物品预制体,而非场景中的实例,避免客户端加载时出现引用错误。
  3. 所有状态修改必须在服务器端执行,客户端仅通过NetworkVariable读取状态并更新显示,确保网络同步的权威性。

内容的提问来源于stack exchange,提问作者Domi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 07:35:54