Mirror是一个简单高效的开源的Unity多人游戏网络框架。
官方文档链接:
https://mirror-networking.gitbook.io/docs
必要性
首先解释一下标题的含义,这里复现一下所谓网络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);
}
}
}
最后尤其注意一点,需要对预制体进行注册,否则网络管理器不会去识别预制体并将之与各个客户端同步
玩家预制体正常的引用武器预制体:
运行之:
本机可以正常看到跟随的武器。
但是新加入的玩家只能看到武器和老玩家,无法跟随。
而且在疯狂报错空指针,原因刚才解释过了,就是引用未成功设置——尽管这个引用是一个SyncVar:
(未来版本的Mirror可能会修复这个Issue,2022/2/28未修复)
当然让武器的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);
测试
可以发现能够成功跟随了。