[Unity Mirror] 数据类型

英文原文:

https://mirror-networking.gitbook.io/docs/guides/data-types

客户端和服务器可以通过远程操作、状态同步或网络消息相互传递数据

Mirror 支持您可以使用的多种数据类型,包括:

  • 基本 C# 类型(byte、int、char、uint、UInt64、float、string 等)
  • 内置 Unity 数学类型(Vector3、Quaternion、Rect、Plane、Vector3Int 等)
  • URI
  • NetworkIdentity
  • 附加了 NetworkIdentity 组件的游戏对象
    • 请参阅 GameObjects 部分中的重要细节。
  • 具有上述任何一种的结构
    • 建议实现 IEquatable 以避免装箱,并使结构只读,因为修改字段之一不会导致重新同步
  • 类, 只要每个字段都具有受支持的数据类型
    • 这些将分配垃圾,并且每次发送时都会在接收器上实例化新的。
  • ScriptableObject,只要每个字段都具有受支持的数据类型
    • 这些将分配垃圾,并且每次发送时都会在接收器上实例化新的。
  • 上述任何一个的数组
    • SyncVars 或 SyncLists 不支持
  • 上述任何一个的 ArraySegments
    • SyncVars 或 SyncLists 不支持

Game Objects

  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}");
        }
    }
}

ScriptableObjects

  人们经常希望从客户端或服务器发送可编写脚本的对象。例如,您可能将一堆剑创建为可编写脚本的对象,并且您希望将装备好的剑放在同步变量中。这将正常工作,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());
    }
}

你可能感兴趣的:(Unity,Mirror,网络,Mirror,unity)