英文原文:
https://mirror-networking.gitbook.io/docs/guides/data-types
客户端和服务器可以通过远程操作、状态同步或网络消息相互传递数据
Mirror 支持您可以使用的多种数据类型,包括:
SyncVars、SyncLists 和 SyncDictionaries 中的游戏对象在某些情况下很脆弱,应谨慎使用。
当同步数据到达客户端时,该客户端上可能还不存在引用的游戏对象,从而导致同步数据中的值为空。这是因为内部 Mirror 从 NetworkIdentity 传递 netId 并尝试在客户端的 NetworkIdentity.spawned 字典中查找它。
如果该对象还没有在客户端上生成,将找不到匹配的对象。它可能在同一个有效载荷中,特别是对于加入的客户端,但在另一个对象的同步数据之后。
它也可能是空的,因为游戏对象由于网络可见性而被排除在客户端之外,例如NetworkProximityChecker。
您可能会发现同步 NetworkIdentity.netID (uint) 更为稳健,并在 NetworkIdentity.spawned 中进行自己的查找以获取对象,可能在协程中:
public GameObject target;
[SyncVar(hook = nameof(OnTargetChanged))]
public uint targetID;
void OnTargetChanged(uint _, uint newValue)
{
if (NetworkIdentity.spawned.TryGetValue(targetID, out NetworkIdentity identity))
target = identity.gameObject;
else
StartCoroutine(SetTarget());
}
IEnumerator SetTarget()
{
while (target == null)
{
yield return null;
if (NetworkIdentity.spawned.TryGetValue(targetID, out NetworkIdentity identity))
target = identity.gameObject;
}
}
有时您不希望Mirror为您自己的类型生成序列化。例如,您可能希望仅序列化 quest id,而不是序列化 quest 数据,接收方可以通过 id 在预定义列表中查找 quest。
有时您可能想要序列化使用 Mirror 不支持的不同类型的数据,例如 DateTime 或 System.Uri
您可以通过向 NetworkWriter 和 NetworkReader 添加扩展方法来添加对任何类型的支持。例如,要添加对 DateTime 的支持,请在项目中的某处添加:
public static class DateTimeReaderWriter
{
public static void WriteDateTime(this NetworkWriter writer, DateTime dateTime)
{
writer.WriteInt64(dateTime.Ticks);
}
public static DateTime ReadDateTime(this NetworkReader reader)
{
return new DateTime(reader.ReadInt64());
}
}
…然后您可以在 [Command] 或 SyncList 中使用 DateTime
有时您可能希望将多态数据类型发送到您的命令。 Mirror 不会序列化类型名称以保持消息较小并且出于安全原因,因此 Mirror 无法通过查看消息来确定它接收到的对象的类型。
此代码不能开箱即用。
class Item
{
public string name;
}
class Weapon : Item
{
public int hitPoints;
}
class Armor : Item
{
public int hitPoints;
public int level;
}
class Player : NetworkBehaviour
{
[Command]
void CmdEquip(Item item)
{
// 重要提示:这不起作用。 Mirror 会给你一个 item 类型的对象
// 即使您传入的是武器或盔甲。
if (item is Weapon weapon)
{
// 该物品是武器,
// 也许你需要把它装备在手中
}
else if (item is Armor armor)
{
// 你可能想在身上装备盔甲
}
}
[Command]
void CmdEquipArmor(Armor armor)
{
// 重要提示:这也不起作用,你会收到一件盔甲,但是
// 即使您传递了带有名称的盔甲,盔甲也不会有有效的 Item.name
}
}
如果您为 Item 类型提供自定义序列化程序,CmdEquip 将起作用。例如:
public static class ItemSerializer
{
const byte WEAPON = 1;
const byte ARMOR = 2;
public static void WriteItem(this NetworkWriter writer, Item item)
{
if (item is Weapon weapon)
{
writer.WriteByte(WEAPON);
writer.WriteString(weapon.name);
writer.WritePackedInt32(weapon.hitPoints);
}
else if (item is Armor armor)
{
writer.WriteByte(ARMOR);
writer.WriteString(armor.name);
writer.WritePackedInt32(armor.hitPoints);
writer.WritePackedInt32(armor.level);
}
}
public static Item ReadItem(this NetworkReader reader)
{
byte type = reader.ReadByte();
switch(type)
{
case WEAPON:
return new Weapon
{
name = reader.ReadString(),
hitPoints = reader.ReadPackedInt32()
};
case ARMOR:
return new Armor
{
name = reader.ReadString(),
hitPoints = reader.ReadPackedInt32(),
level = reader.ReadPackedInt32()
};
default:
throw new Exception($"Invalid weapon type {type}");
}
}
}
人们经常希望从客户端或服务器发送可编写脚本的对象。例如,您可能将一堆剑创建为可编写脚本的对象,并且您希望将装备好的剑放在同步变量中。这将正常工作,Mirror 将通过调用 ScriptableObject.CreateInstance 为可编写脚本的对象生成读取器和写入器并复制所有数据。
然而,生成的 reader 和 writer 并不适合所有场合。可编写脚本的对象通常会引用其他资源,例如纹理、预制件或其他无法序列化的类型。可编写脚本的对象通常保存在 Resources 文件夹中。可编写脚本的对象有时会在其中包含大量数据。生成的读取器和写入器可能无法正常工作,或者可能无法满足这些情况。
您可以传递名称,而不是传递可编写脚本的对象数据,而另一方可以按名称查找相同的对象。这样,您可以在可编写脚本的对象中拥有任何类型的数据。您可以通过提供自定义读取器和写入器来做到这一点。这是一个例子:
[CreateAssetMenu(fileName = "New Armor", menuName = "Armor Data")]
class Armor : ScriptableObject
{
public int Hitpoints;
public int Weight;
public string Description;
public Texture2D Icon;
// ...
}
public static class ArmorSerializer
{
public static void WriteArmor(this NetworkWriter writer, Armor armor)
{
// 不需要序列化数据,只需要盔甲的名字
writer.WriteString(armor.name);
}
public static Armor ReadArmor(this NetworkReader reader)
{
// 按名称加载相同的盔甲。数据将来自资源文件夹中的资产
return Resources.Load(reader.ReadString());
}
}