【Unity】消息协议的设计

本文介绍了在Unity游戏中构建网络同步框架SGNet的编解码设计,涉及Json和protobuf两种数据格式,以及消息协议的设计。作者探讨了两种不同的消息协议设计方法,包括使用泛型减少重复代码,同时分析了各自的优缺点。文章总结了新手在构建框架时可能遇到的问题,并提供了参考资料。
摘要由CSDN通过智能技术生成


前言

在疫情的背景下,网游很有市场,这是我初出茅庐尝试搭建网络同步框架(SGNet),本文描述该框架下的编解码设计,编解码目标是兼容大部分数据格式。

开发环境

  • Uinty 2020.3.18f1c1
  • VS2022
  • protobuf-net、System.Text.Json dll
  • 编码格式使用长度信息法

一、了解编数据格式

1.Json

Json是目前web最流行的数据格式,我最喜欢它的优点是方便阅读。
值得注意的是:
System.Text.Json库的版本支持
Unity虽然说可支持.net 4.x但是经测试,没有json库,需要导入,方法不多做介绍(参考)。
根据msdn文档一般用到下面两个api:

// 形参省略了库名和参数名
// 编码api
JsonSerializer.Serialize(object, Type, JsonSerializerContext);
// 解码api
JsonSerializer.Deserialize(ReadOnlySpan<Byte>, Type, JsonSerializerContext);

注意到这里,需要获取类类型。

2.protobuf

protobuf是谷歌的一种数据格式,Unity里我使用了protobuf-net,因为没找到文档,下面的api是我摸索出来的,可能有更好的方法。

// 形参省略了库名和参数名
// 编码api
Serialize(stream, T);
// 解码api
Deserialize(Type, stream);

同样需要知道类类型。

3.待了解其他数据格式

二、消息协议的设计

1.第一版本

首先,每个消息都应该继承于MessageProtocol,便于归类管理,可存放在同一类连表里,然后有个问题,protobuf是自动生成代码的,每次修改proto文件后生成代码,会把上一次添加的继承给覆盖,因此需要套一层数据结构去包含它,添加必要的信息,此处名为消息协议,另外虽然继承与一个基类global::ProtoBuf.IExtensible,但是在运行时的父类时object,所以还是套一层数据结构较佳。“所有问题都可以通过增加一层来解决”
消息封装

public abstract class MessageProtocol
{
    public string protoName = "";
    // 有两个减少反射使用的方法
    // 1.可以添加属性给子类重写,获取对象类型,方便编码时获取类型。
    // 2.可以添加字典,每新增一个子类就在子类的静态构造函数里注册子类类型,方便解码时获取类型。
    // 网络传输只有消息是有效信息,因此还可以把在基类设置消息属性,给子类重写,在编解码的时候获取消息(应该叫拆包吧)即可。
}

public class SystemRoomCancelReady : MessageProtocol
{
    public RoomCancelReadyMsg roomCancelReady;
    public SystemRoomCancelReady()
    {
        protoName = "SystemRoomCancelReady";
        roomCancelReady = new RoomCancelReadyMsg();
    }
}
//... ...

解码器

    public interface ICoder
    {
        byte[] Encode(MessageProtocol message);
        MessageProtocol Decode(byte[] messageBuffer, string protocolName, UInt16 start, UInt16 length);
    }
public class JsonCoder : ICoder
{
    static JsonSerializerOptions options = new JsonSerializerOptions
    {
        IncludeFields = true,   // 序列化所有公共字段,或者字段加{get;set;}
    };

    public byte[] Encode(MessageProtocol message)
    {
        return System.Text.Encoding.UTF8.GetBytes(JsonSerializer.Serialize(message, message.GetType(), options));
    }

    public MessageProtocol Decode(byte[] messageBuffer, string protocolName, UInt16 start, UInt16 length)
    {
    	ArraySegment<byte> buffer = new ArraySegment<byte>(messageBuffer, start, length);
        return (MessageProtocol)JsonSerializer.Deserialize(buffer , Type.GetType(protocolName), options);
    }
}

public class ProtobufCoder: ICoder
{
    public byte[] Encode(MessageProtocol message)
    {
        MemoryStream ms;
        using (ms = new MemoryStream())
        {
            ProtoBuf.Serializer.Serialize(ms, message.Message);
        }
        return ms.ToArray();
    }

    public MessageProtocol Decode(byte[] messageBuffer, string protocolName, UInt16 start, UInt16 length)
    {
		using (var ms = new MemoryStream(messageBuffer, start, length))
        {
            obj = ProtoBuf.Serializer.Deserialize(msg.MsgType, ms);
        }
        return obj;
    }
}

总结: 这种方法有三个缺点,一是频繁的调用反射Gettype(解决方法见消息协议基类),二是消息协议类会随消息类一起增长,编写过程机械,可以用代码生成器解决(但是我不会)。可是这种方法相对第二种方法,方便测试,可以直接遍历基类的字典,便可全部消息收发一遍,第三是应该传输消息而非消息协议,节省流量(解决方法见消息协议基类)。

2.第二种方法

基于第一种方法的总结,再观察消息协议类的构成,不难发现,每个类仅仅只是包含的消息类不同,这个时候就应该想到泛型,减少没有意义的重复代码。并结合总结中的改进方法修改。
消息协议基类

    public abstract class BaseMsg
    {
        public abstract Type MsgType { get; }
        public abstract string MsgName { get; }
        public abstract object Message { get; set; }

        public static Dictionary<string, BaseMsg> MessageDict = new Dictionary<string, BaseMsg>();
        public abstract BaseMsg Clone();
    }

因为仅传输消息,解码后使用字典找到对应的消息协议,把消息赋值给消息协议的一份克隆,然后返回。

public class NetworkMsg<T> : BaseMsg where T : new()
    {
        static Type msgType = typeof(T);
        public T message;

        public override Type MsgType => msgType;
        public override string MsgName => msgType.Name;
        public override object Message
        {
            get { return message; }
            set { message = (T)value; }
        }

        static NetworkMsg()
        {
            MessageDict.Add(msgType.Name, new NetworkMsg<T>());
        }


        public NetworkMsg()
        {
            message = new T();
        }

        public NetworkMsg(T msg)
        {
            message = msg;
        }

        public override BaseMsg Clone()
        {
            return new NetworkMsg<T>(default);
        }
    }

可惜该方法并非十全十美,有两个缺点,一是上面提到的无法靠反射生成所有消息协议类测试,二是没有有效的手段约束传进泛型里的消息,可能传错类型,导致编解码异常。


总结

新手确实不应该一上来就做框架性的东西,浪费了我很多时间设计,而且不怎么符合心意,但是为了实现游戏同步又不得不搭建一个C/S框架,或许我应该从单机做起吧。

参考资料:
罗培羽大佬的《unity游戏网络实战》《百万在线》
TSRPC + Cocos 多人实时对战 Demo

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值