Protobuf学习

protobuf原理及游戏中应用

Posted by Luffy on February 1, 2017

前言

在之前的博客中提到了关于序列化的相关知识。考虑一下这种情况:策划配置好相应的游戏数据表格,想要将excel表格转化为程序中可以使用的数据,那么你会采用什么样的策略呢?xml或是二进制?或是本文介绍的Protobuf?

Protobuf

protobuf提供一种十分高效、灵活的序列化结构化数据的方式,可以更加容易生成源码,来更加高效的读写结构化数据。具体可查看Protobuf的开发者文档

与其他格式的性能比对

那么,相对于其他可以序列化结构数据的方式,如xml,protobuf具有哪些优势呢? 相较于xml,protobuf的优势可以总结为:

  • 更容易code
  • 小3~10倍
  • 快20~100倍
  • 更少的歧义
  • 生成的数据更易于在程序中使用

而关于不同的策略的详细性能数据对比可以参考此开源项目,通过对比采用不同方式序列化/反序列化所占用的空间和时间,来比对每种方式的性能。 结论__protobuf__在U3D实际游戏开发中有较好的性能。

Protobuf格式

在下载Protobuf之后,文件夹example为测试用例,其中观察addressbook.proto可以了解protobuf格式。

message Person {
  required string name = 1;
  required int32 id = 2;        // Unique ID number for this person.
  optional string email = 3;

  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }

  message PhoneNumber {
    required string number = 1;
    optional PhoneType type = 2 [default = HOME];
  }

  repeated PhoneNumber phone = 4;
}

Person中包含有name,id,emailPhoneNumber(其中phoneNumber为嵌套消息)。 required表示字段为必要,optional可选。

Protobuf 编码格式

官网Encoding说明。Protobuf的高效部分源于起编码格式。 考虑以下信息:

message Test1{
	required int32 a = 1;
	}

在应用当中,传递一个Test1结构,其中a设为150,那么其对应的编码信息为08 96 01。是不是空间占用率很小?接下来解释一下原理。 字节的首位代表是否有后续字节,即08首字节为0,则无后续字节,96首字节为1,则与后续字节作为一个整体,即96 01

首先,Protobuf的类型可以用下表来说明:

类型 含义 应用
0 Varint int32, int64, uint32…
1 64-bit fixed64, sfixed64, double
2 Length-delimited string, bytes, embedded messages…

等等…,

类型有字节后三位表示。考虑08字节,首字节去掉后为:000 1000,后三位为0,表示为Variant类型,而将字节右移三位表示tag,即为1(所以前16标示位表示常用字段,可以只用1个字节表示)。而接下来的字节96 01,去除最高为后,可以表示为 000 0001 + 001 0110(反转后),即10010110 = 150。

Protobuf的使用

可以查看官方教程,并结合Protobuf中的相应例子来学习Protobuf的具体使用。首先,可以参考ReadMe,按步骤安装Protobuf。在example中含有相应例子。前文中已经描述了proto文件如何定义及编码规则,这里就不细讲。

可以通过指令:protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/addressbook.proto生成protobuf所需要的相应源文件,其中-cpp_out表示为c++类。例如其中的id字段,生成为:

inline bool has_id() const;
inline void clear_id();
inline int32_t id() const;
inline void set_id(int32_t value);

完整代码可查看例子中生成的c++文件。

例子中包含有add_person.cclist_people.cc两个程序,分别为添加列出两种protobuf文件操作的测试用例。利用所生成的c++类中如set_email()等函数,进行赋值,并将其序列化转存到指定文件中;listpeople则是读取并显示相应文件的protobuf信息。

游戏中的具体应用

在U3D中由于使用的是c#语言,所以可以使用protobuf-net,github地址点这里。可以查看其中的ReadMe来查看具体使用。

 [global::System.Serializable, global::ProtoBuf.ProtoContract(Name=@"EXPLEVEL")]
  public partial class EXPLEVEL : global::ProtoBuf.IExtensible
  {
    public EXPLEVEL() {}
  
    private uint _Difficulty = (uint)0;
    [global::ProtoBuf.ProtoMember(1, IsRequired = false, Name=@"Difficulty", DataFormat = global::ProtoBuf.DataFormat.TwosComplement)]
    [global::System.ComponentModel.DefaultValue((uint)0)]
    public uint Difficulty
    {
      get { return _Difficulty; }
      set { _Difficulty = value; }
    }
    
    private string _Level = "";
    [global::ProtoBuf.ProtoMember(2, IsRequired = false, Name=@"Level", DataFormat = global::ProtoBuf.DataFormat.Default)]
    [global::System.ComponentModel.DefaultValue("")]
    public string Level
    {
      get { return _Level; }
      set { _Level = value; }
    }
  }

可观察到有两个字段DifficultyLevel。可以通过函数SerializeDeserialize来序列化和反序列化,并最终实现数据的传递与应用。

总结

通常在实际应用中:

  • 首先定义proto文件,通过protobuf-net生成程序中可用的代码;
  • 然后通过转表工具(利用转表工具Excel Reader等将策划表格转为protobuf格式)。
  • 可以将proto序列化的二进制文件,通过id-proto键值对形式存储在sqlite中(待续)。
  • 通过assetbundle(待续)或其他形式更新/传递数据。