Mirror是一个简单高效的开源的Unity多人游戏网络框架。

必要性

首先解释一下标题的含义,这里复现一下所谓网络ID引用数据不同步的问题场景。

假设玩家A有一个跟随物,他不是玩家A这个对象的儿子,而是同级的一个对象。
它拥有一个独立的网络ID(NetworkIdentity),
我们希望一开始这个跟随物不存在,在玩家A召唤后才出现。
并且玩家A引用到跟随物的网络ID,从而控制其跟踪自己。
若此时又进来一个玩家B,由于玩家A和跟随物都有独立的网络ID,因此玩家B可以直接看到玩家A和跟随物。
但是在玩家B眼里,跟随物将无法跟随玩家A。
这是因为玩家A的客户端中的跟随物,玩家A对它的网络ID的`引用`是正确设置的,
但是玩家B的客户端中的跟随物,这个引用是空指针

下面用代码实现一下这个问题:

脚本挂载在玩家预制体上,
用来在按下空格键后,召唤出跟随自己的武器(我这里实际上就是一个胶囊)。
注意下面代码和流程中,让服务器创建一个prefab是需要先注册预制体、并且创建玩实例后调用NetworkServer.Spawn(tmp);开启网络数据同步的
而且由于服务器和客户端内存引用地址的差异,需要进行同步的GameObject必须改成NetworkIdentity

//PlayerController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Mirror;
using Cinemachine;
public class PlayerController : NetworkBehaviour
{
    Rigidbody rb;
    CinemachineVirtualCamera cv;
    
    public GameObject prefabWeapon;

    [SyncVar]
    bool isWeaponHolded = false;
    [SyncVar]
    private NetworkIdentity weaponSpawned;

    [Command]
    void GetWeaponHold(){
        if(isWeaponHolded)return;
        isWeaponHolded = true;
        GameObject tmp = GameObject.Instantiate(prefabWeapon);
        NetworkServer.Spawn(tmp);
        weaponSpawned = tmp.GetComponent<NetworkIdentity>();
    }

    private void Start() {
        rb = GetComponent<Rigidbody>();
        if(isLocalPlayer){
            cv = GameObject.FindGameObjectWithTag("VCAM").GetComponent<CinemachineVirtualCamera>();
            cv.Follow = this.transform;
            cv.LookAt = this.transform;
        }
    }

    private void Update() {
        if(Input.GetKeyDown(KeyCode.Space)){
            GetWeaponHold();
        }
        float moveDirX = Input.GetAxis("Horizontal");
        float moveDirY = Input.GetAxis("Vertical");
        rb.velocity = new Vector3(moveDirX * 3f, moveDirY * 3f, 0);
        
        if(isWeaponHolded){
            weaponSpawned.transform.position = new Vector3(transform.position.x, transform.position.y+2, transform.position.z);
        }
    }
}

最后尤其注意一点,需要对预制体进行注册,否则网络管理器不会去识别预制体并将之与各个客户端同步

Unity局域网多设备同步播放 unity 网络数据同步_学习

玩家预制体正常的引用武器预制体:

Unity局域网多设备同步播放 unity 网络数据同步_unity_02

运行之:

本机可以正常看到跟随的武器。

Unity局域网多设备同步播放 unity 网络数据同步_客户端_03

但是新加入的玩家只能看到武器和老玩家,无法跟随。

而且在疯狂报错空指针,原因刚才解释过了,就是引用未成功设置——尽管这个引用是一个SyncVar

(未来版本的Mirror可能会修复这个Issue,2022/2/28未修复)

Unity局域网多设备同步播放 unity 网络数据同步_unity_04

当然让武器的transform直接进行服务器与客户端之间的同步可以容易解决这个问题,但是效率低。

问题解决

这里直接转一下FirstGearGames这位大佬的解决方案,
其核心思想就是通过客户端已有的网络对象列表,与对应的netID,重新设定对象引用。

更多这位大佬的解决方案可以订阅他的patreon频道获取。

代码
using Mirror;

#pragma warning disable CS0618, CS0672

    [System.Serializable]
    public class NewNetworkIdentityReference
    {
        /// <summary>
        /// NetworkId for the NetworkIdentity this was initialized for.
        /// </summary>
        public uint NetworkId { get; private set; }

        /// <summary>
        /// NetworkIdentity this referencer is holding a value for.
        /// </summary>
        public NetworkIdentity Value
        {
            get
            {
                //No networkId, therefor is null.
                if (NetworkId == 0)
                    return null;
                //If cache isn't set then try to set it now.
                if (_networkIdentityCached == null)
                    _networkIdentityCached = NetworkIdentity.spawned[NetworkId];

                return _networkIdentityCached;
            }
        }
        /// <summary>
        /// Cached NetworkIdentity value. Used to prevent excessive dictionary iterations.
        /// </summary>
        private NetworkIdentity _networkIdentityCached = null;

        /// <summary>
        /// 
        /// </summary>
        public NewNetworkIdentityReference() { }

        /// <summary>
        /// Initializes with a NetworkIdentity.
        /// </summary>
        /// <param name="networkIdentity"></param>
        public NewNetworkIdentityReference(NetworkIdentity networkIdentity)
        {
            if (networkIdentity == null)
                return;

            NetworkId = networkIdentity.netId;
            _networkIdentityCached = networkIdentity;
        }
        /// <summary>
        /// Initializes with a NetworkId.
        /// </summary>
        /// <param name="networkId"></param>
        public NewNetworkIdentityReference(uint networkId)
        {
            NetworkId = networkId;
        }
    }


    public static class NetworkIdentityReferenceReaderWriter
    {
        public static void WriteNetworkIdentityReference(this NetworkWriter writer, NewNetworkIdentityReference nir)
        {
            //Null NetworkIdentityReference or no NetworkIdentity value.
            if (nir == null || nir.Value == null)
                writer.WriteUInt(0);
            //Value exist, write netId.
            else
                writer.WriteUInt(nir.Value.netId);
        }

        public static NewNetworkIdentityReference ReadNetworkIdentityReference(this NetworkReader reader)
        {
            return new NewNetworkIdentityReference(reader.ReadUInt());
        }

        public static void WriteUInt(this NetworkWriter writer, uint value)
        {
            writer.WriteByte((byte)value);
            writer.WriteByte((byte)(value >> 8));
            writer.WriteByte((byte)(value >> 16));
            writer.WriteByte((byte)(value >> 24));
        }
        public static uint ReadUInt(this NetworkReader reader)
        {
            uint value = 0;
            value |= reader.ReadByte();
            value |= (uint)(reader.ReadByte() << 8);
            value |= (uint)(reader.ReadByte() << 16);
            value |= (uint)(reader.ReadByte() << 24);
            return value;
        }
    }
使用方式

将刚刚失败的代码中,做出如下修改:

//引用类型的修改
[SyncVar]
private NetworkIdentity weaponSpawned;
=>
[SyncVar]
private NewNetworkIdentityReference weaponSpawned;
//引用对象设定的修改
weaponSpawned = tmp.GetComponent<NetworkIdentity>();
=>
weaponSpawned = new NewNetworkIdentityReference(tmp.GetComponent<NetworkIdentity>());
//解包使用 多一个Value获取到正确的NetworkIdentity
weaponSpawned.transform.position = new Vector3(transform.position.x, transform.position.y+2, transform.position.z);
=>
weaponSpawned.Value.transform.position = new Vector3(transform.position.x, transform.position.y+2, transform.position.z);
测试

可以发现能够成功跟随了。

Unity局域网多设备同步播放 unity 网络数据同步_客户端_05


Unity局域网多设备同步播放 unity 网络数据同步_unity_06