Unity C# 编译集成 Google Protobuf

踩了很多坑,因为网上几年前基本上都是用第三方 protobuf-net 库来作为 C# 的 Protobuf 工具使用的,尤其是 Unity 。所以关于 Google 官方的 C# 集成到 Unity 基本没人使用,C# 非 Unity 的开发(比如 .net )可以通过VS的 Nuget 快速集成 Protobuf,在 Unity 上这样做只能够在 VS 编辑器下检测到这个库,而 Unity 编辑器是不知道的,所以不能用这种方式。

而直接接入源码也会因为缺少依赖报错无法解决,最终还是采用自己编译出DLL集成的方式来接入。

另外前提是 Unity 切换成 .net4.x,因为 Google Protobuf 需要 4.5+。

编译DLL

源码:https://github.com/protocolbuffers/protobuf/releases

下载 protobuf-csharp-3.14.0.zip

使用VS打开目录 protobuf-3.14.0\csharp\src 中的 Google.Protobuf.sln

如图选择:

image-20201117164601808

然后生成

image-20201117164624330

生成结束后 protobuf-3.14.0\csharp\src\Google.Protobuf\bin\Release\net45 目录会生成以下文件:

image-20201117164722068

把这些文件复制到 Unity 项目的 Assets/Plugins 目录,这样就算集成好了序列化和反序列化的库。

导出Proto

集成之后只是有了序列化和反序列化的基础库,但是没有数据结构,所以还需要通过 .proto 文件编译生成C#可以使用的数据结构。

用 protoc.exe:

1
protoc --csharp_out=. *.proto

把多个proto分别生成 cs 文件,放到项目里合适的目录即可。

序列化与反序列化

在需要使用的地方引入 Protobuf:

1
using Google.Protobuf;

构造Message数据:

1
2
3
4
5
6
Person person = new Person
{
Age = 22,
Address = "111111",
Name = "John"
};

序列化:

1
byte[] result = person.ToByteArray();

反序列化:

1
Person p = Person.Parser.ParseFrom(result);

修改网络层接口

最后是要把所有以前调用 protobuf-net 的地方改掉,另外 Google Protobuf 的接口类是 IMessage。

泛型方法反序列化

在知道类型的情况下是很容易反序列化 Proto 数据的,尤其是可以把对应的 Type 作为函数泛型约束的情况。

即使是只知道 Type 的时候,在 protobuf-net 中也提供了反射反序列化的接口。不过 Google 官方没有提供反射解析的方式,因此只能使用泛型方法反序列化。

这里我提供两种泛型方法用于反序列化,这两种都是可以用的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//一种泛型反序列化方法
public T Deserialize<T>(Stream s) where T : IMessage<T>, new()
{
T message = new T();
message.MergeFrom(s);
return message;
}

//另一种泛型反序列化方法
public T Deserialize2<T>(Stream s) where T : IMessage<T>, new()
{
MessageParser<T> parser = new MessageParser<T>(() => new T());
return parser.ParseFrom(s);
}

工厂模式函数反序列化

在不知道类型的情况下怎么反序列化 Proto 数据?服务器发给我们的数据只会有消息ID和二进制数据,这种情况怎么反序列化?

一般来说,需要在游戏启动时提前实例化好需要用到的消息的解析器,并保存与消息ID的对应关系,用于之后的序列化和反序列化。

不推荐使用反射的方式创建,而且 Google 官方也没提供反射解析的方式,因此想保存消息Type与消息ID的映射是没用的。所以我们写一个静态类,里面提供一个工厂方法,用于提供指定类型消息的解析器对象:

1
2
3
4
5
6
7
8
9
//Proto 消息解析器创建类
static class Parser
{
public static MessageParser Get<T>() where T : IMessage<T>, new()
{
MessageParser<T> parser = new MessageParser<T>(() => new T());
return parser;
}
}

同时,在游戏启动时也要创建对应的字典,来保存消息ID和解析器的对应关系,这里就不写了。

注意事项

最后要注意的是 Google 官方的 C# Protobuf 相比 protobuf-net 还有一些区别:

  1. 所有消息的字段都会强制转为驼峰命名并去掉下划线,并且没有办法避免;
  2. 消息字段不能再使用byte数组,必须使用 Google 封装的 ByteString,并且它是只读的;
  3. 不提供通过反射进行反序列化的接口,也就是不能通过把消息的 Type 作为参数传递来反序列化。

前两点还是挺坑的,命名导致从 protobuf-net 换为官方 Protobuf 成本很高,要修改的太多,然后不能使用 byte 数组导致需要加密的时候必须对 ByteString 进行复制转为 byte 数组然后解密。

不过还是更换为官方的 Protobuf 了,因为最新的解决了 GC 的问题,然后避免了反射的调用(其实是强制的)。