Unity Mirror联网游戏开发(12) 自定义数据类型的网络读写

759 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第20天,点击查看活动详情

对自定义类型添加网络数据读写支持

如果自定义的数据类型Mirror不支持网络发送同步,那么可以通过给NetworkWriterNetworkReader添加扩展方法来支持该类型的网络数据读写。例如,我们有一个游戏对象类型:

public class Weapon
{
    int WeaponId;
    GameObject prefab;
    WeaponConfig config;   
    Texture2D icon;
    
    public Weapon(int id)
    {
        WeaponId = id;
        var weaponData = DataTables.GetWeaponTable().Get(id);
        prefab = PrefabMgr.Instance.GetPrefab(weaponData.prafabPath);
        config = weaponData.config;
        icon = IconMgr.Instance.Get(config.iconId);
    }
}

这个对象包含了一些Mirror不支持的数据类型,比如Texture2D。那么想在网络上传递他就要将必要的数据进行读写。对于这个类型,他有一个WeaponId属性,通过这个id可以从游戏的数据表里面查询到Weapon的相关数据,然后通过数据构造出Weapon。因此只需要在网络上传递这个id就可以:

public static class WeaponReaderWriter
{
    public static void WriteWeapon(this NetworkWriter writer, Weapon weapon)
    {
        write.WriteInt64(weapon.WeaponId);
    }
    
    public static Weapon ReadWeapon(this NetworkReader reader)
    {
        return new Weapon(reader.ReadInt64());
    }
}

添加以上扩展方法之后,Weapon类型就可以在远程调用和网络同步中使用,即可以在[command]SyncVar等处使用。

这个例子的情况,是自定义类型中出现了Mirror不支持的数据类型,因此不能直接被Mirrro序列化。但如果自定义类型中的所有成员的数据类型都是Mirror支持的,但这些成员其实没必要全部通过网络传输,例如这儿的WeaponConfig,这个是一个配置数据,是游戏载入时从配置文件载入的,对于所有客户端都是一样的数据,传输这个数据其实是浪费带宽和CPU。因此这种情况,也可以对自定义类型使用自定义的网络读写。

Scriptable Object

SO往往会带有一些不能序列化的数据,且SO本身就是一种资源,对于所有客户端都一样,因此在网络上传输他们的内容也没必要。所以对于SO,也需要自定义网络读写,一般可以只传递他们的文件名,对面收到后将其载入。

网络数据对象 VS 继承和多态

看这个例子:

class Item
{
    public string name;
}

class Weapon: Item
{
    public int attackPoint;
}

class Armor : Item
{
    public int defencePoint;
}

这是一个继承体系,比如对于Player有一个CmdEquip(Item item)方法,如果这是一个普通的C#方法,那么item可以传入一个Weapon对象或者Armor对象,使用is判断具体的对象类型,例如:

void CmdEquip(Item item)
{
    if(item is Weapon weapon)
    {
        //This is a weapon, equip it in the hand
    }
    else if(item is Armor armor)
    {
        //This is a armor, equip it in the body
    }
}

但如果CmdEquip是一个[command],Item就真的只是一个Item,如果不做任何处理,通过网络发送的就只有Item基类的数据name,并且CmdEquip中服务器获取到的对象也只是一个Item基类对象。因为Mirror在序列化时,看到Item就直接对Item序列化,并不会考虑这对象其实可能是Item的子类。并且Mirror自动生成的序列化代码也没法考虑所有可能的子类,只能就事论事,看到啥序列化啥。 另外,如果在Cmd中直接传递子类可以吗?比如这样:

[command]
void CmdEquipArmor(Armor armor)
{
    //注意:这样也不行,原因下面说明
}

很可惜,即便在客户端调用CmdEquipArmor时传递的是一个真实的Armro对象,服务器获得的Armro也是不对的,他缺少来自于Item的name属性。为什么呢?因为Mirror的自动序列化只能处理他看到的类型,并不会再去查找所有可能的基类,并逐个序列化基类的成员。 那么,我们应该怎么做呢?很显然,使用自定义网络读写就可以解决,比如这样:

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.attackPoint);
        }
        else if(item is Armor armor)
        {
            writer.WriteByte(ARMOR);
            writer.WriteString(armor.name);
            writer.WritePackedInt32(armor.defencePoint);
        }
    }
    
    public static void ReadItem(this NetworkReader reader)
    {
        byte type = reader.ReadByte();
        switch(type)
        {
            case WEAPON:
                return new Weapon
                {
                    name = reader.ReadString();
                    attackPoint = reader.ReadPackedInt32();
                };
                break;
            case ARMOR:
                return new Armor
                {
                    name = reader.ReadString();
                    defencePoint = reader.ReadPackedInt32();                    
                };
                break;
            default:
                throw new Exception($"Invalid weapon type {type}");
        }
    }
}