1 Protobuf
Protobuf 全称为 protocol-buffers(协议缓冲区),是谷歌提供给开发者的开源的协议生成工具。
Protobuf 可以基于协议配置文件生成 C++、Java、C#、Objective-C、PHP、Python、Ruby、Go 等语言的代码文件,通用性强、稳定性高,可以节约出开发自定义协议工具的时间,是商业游戏开发中常选择的协议生成工具。
protocol-buffers 官网:https://developers.google.com/protocol-buffers。
1.1 导入 Protobuf
进入官网,左侧选择“Downloads”页面,进入“release page”。
网页最下方选择“protobuf-30.2.zip”下载。
下载并解压后,进入“\protobuf-30.2\csharp\src”目录下,找到“Google.Protobuf.sln”解决方案并打开。使用 Visual Studio 或者 Rider 都可。
以 Rider 为例,右键“Google.Protobuf”项目,点击“构建所选项目”。
进入“\protobuf-30.2\csharp\src\Google.Protobuf\bin\Debug”目录,可看到多个版本的构建文件。这里选择 net45,点击进入。
在 Unity 中创建“Plugins/Protobuf ”录,将 net45 目录下所有 dll 文件拷贝至其中。等待 Unity 编译完成后,导入 Protobuf 成功。
1.2 导入编译器
下载对应操作系统的“protoc.zip”文件。
解压后,找到“\protoc-30.2-win64\bin”目录下的“protoc.exe”文件。
在 Unity 项目路径下创建文件夹 Protobuf,将“protoc.exe”文件复制到该文件夹中。
2 配置规则
Protobuf 中配置文件的后缀统一使用 .proto,可以通过多个后缀为 .proto 的配置文件进行配置。
在 Unity 的 Assets 文件夹下创建文件夹 Protobuf,在该目录下创建 test.txt 文件,将其后缀名更改为 .proto 并打开。
在 Rider 中,可安装插件“Protobuf”对 .proto 文件提供高亮和智能提示功能。
注意,需要先禁用自带的“Protocol Buffers”插件。
![]()
2.1 注释
注释:支持//
和/* */
。
// 规则1:注释方式
// 注释方式一
/* 注释方式二 */
2.2 版本号
版本声明(必须位于首行)。
syntax = "proto3"; // 默认为 proto2
2.3 命名空间
package GamePlayerTest; // 这决定了命名空间
2.4 消息类
message TestMsg{
...
}
成员声明方式:数据类型 字段名 = 唯一编号;
2.4.1 float / double
// = 1 不代表默认值 而是代表唯一编号 方便我们进行序列化和反序列化的处理
float testF = 1; // C# - float
double testD = 2; // C# - double
2.4.2 int/sint/uint
- int:使用可变长度编码。对负数编码效率低下。
- sint:使用可变长度编码。Signed int 值。这些比常规 int 更有效地编码负数。
- uint:使用可变长度编码。
变长编码会根据数字的大小使用对应的字节数来存储,如 1、2、4 个字节。是 Protobuf 实现的优化部分,可以尽量少的使用字节数来存储内容。
// 变长编码
// 1 2 4 8
int32 testInt32 = 3; // C# - int 它不太适用于来表示负数 请使用 sint32
int64 testInt64 = 4; // C# - long 它不太适用于来表示负数 请使用 sint64
// 更实用与表示负数类型的整数
sint32 testSInt32 = 5; // C# - int 适用于来表示负数的整数
sint64 testSInt64 = 6; // C# - long 适用于来表示负数的整数
// 无符号 变长编码
// 1 2 4
uint32 testUInt = 7; // C# - uint 变长的编码
uint64 testULong = 8; // C# - ulong 变长的编码
2.4.3 fixed/sfixed
- fixed:始终为 4/8 个字节。如果值通常较大,则比 uint 更高效。
- sfixed:始终为 4/8 个字节。
// 固定字节数的类型
fixed32 testFixed32 = 9; // C# -uint 它通常用来表示大于2的28次方的数 ,比uint32更有效 始终是4个字节
fixed64 testFixed64 = 10; // C# -ulong 它通常用来表示大于2的56次方的数 ,比uint64更有效 始终是8个字节
sfixed32 testSFixed32 = 11; // C# - int 始终4个字节
sfixed64 testSFixed64 = 12; // C# - long 始终8个字节
2.4.4 bool/string/bytes
- bool:布尔值。
- string:字符串必须始终包含 UTF-8 编码或 7 位 ASCII 文本,并且长度不能超过 232。
- bytes:可以包含任何不超过 232 的任意字节序列。
// 其它类型
bool testBool = 13; // C# - bool
string testStr = 14; // C# - string
bytes testBytes = 15; // C# - BytesString 字节字符串
2.5 特殊标识
2.5.1 required
- 必须赋值(proto2 特有,proto3 已移除)。
// required 必须赋值的字段
required float testF = 1; //C# - float
2.5.2 optional
可以不赋值的字段(proto3 默认)。
字段处于以下两种可能的状态之一:
- 该字段已设置,并包含从连线显式设置或解析的值。它将被序列化。
- 该字段未设置,并将返回默认值。它不会被序列化。
// optional 可以不赋值的字段
optional double testD = 2; //C# - double
2.5.3 repeated
- 可重复字段(数组)。
// 数组List
repeated int32 listInt = 16; // C# - 类似List<int>的使用
2.5.4 map
- 成对的键/值字段类型。
// 字典 Dictionary
map<int32, string> testMap = 17; // C# - 类似Dictionary<int, string> 的使用
2.6 枚举
// 枚举的声明
enum TestEnum {
NORMAL = 0; // 第一个常量必须映射到 0
BOSS = 5;
}
2.7 默认值
类型 | 默认值 |
---|---|
string | 空字符串 |
bytes | 空字节 |
bool | false |
数值类型 | 0 |
message | 取决于语言,C# 为空 |
enum | 0 |
2.8 允许嵌套
嵌套一个类在另一个类当中,相当于是内部类。
message TestMsg{
message TestMsg3 {
int32 testInt32 = 1;
}
enum TestEnum2 {
NORMAL = 0; // 第一个常量必须映射到 0
BOSS = 1;
}
TestMsg3 testMsg3 = 20;
TestEnum2 testEnum2 = 21;
}
2.9 保留字段
如果修改了协议规则,删除了部分内容,为了避免更新时重新使用已经删除了的编号,可以利用 reserved 关键字来保留字段,这些内容就不能再被使用了。
// int32 testInt3233333 = 22;
// 告诉编译器 22 被占用 不准用户使用
// 之所以有这个功能 是为了在版本不匹配时
// 反序列化不会出现结构不统一 解析错误的问题
reserved 22;
reserved testInt3233333;
message Foo {
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
}
reserved 仅影响 protoc 编译器行为,运行时 JSON 解析不受保留名称的影响。
2.10 导入定义
在 Protobuf 文件夹下创建 test2.proto 文件,并写入如下内容:
syntax = "proto3";
// 规则 3:命名空间
package GamePlayerTest;
// 这决定了命名空间
message HeartMsg {
int64 time = 1;
}
在 test.proto 中即可导入 test2.proto:
syntax = "proto3";
// 规则 2:版本号
// 规则 1:注释方式
// 注释方式一
/* 注释方式二 */
// 规则 11:导入定义
import "test2.proto";
// 规则 3:命名空间
package GamePlayerTest;
// 这决定了命名空间
// 规则 4:消息类
message TestMsg {
...
}
附:完整 .proto 代码
test.proto
syntax = "proto3";
// 规则 2:版本号
// 规则 1:注释方式
// 注释方式一
/* 注释方式二 */
// 规则 11:导入定义
import "test2.proto";
// 规则 3:命名空间
package GamePlayerTest;
// 这决定了命名空间
// 规则 4:消息类
message TestMsg {
// 浮点数
// = 1 不代表默认值 而是代表唯一编号 方便我们进行序列化和反序列化的处理
// required 必须赋值的字段
float testF = 1; // C# - float
// optional 可以不赋值的字段
optional double testD = 2; // C# - double
// 变长编码
// 所谓变长 就是会根据 数字的大小 来使用对应的字节数来存储 1 2 4
// Protobuf 帮助我们优化的部分 可以尽量少的使用字节数 来存储内容
int32 testInt32 = 3; // C# - int 它不太适用于来表示负数 请使用 sint32
// 1 2 4 8
int64 testInt64 = 4; // C# - long 它不太适用于来表示负数 请使用 sint64
// 更实用与表示负数类型的整数
sint32 testSInt32 = 5; // C# - int 适用于来表示负数的整数
sint64 testSInt64 = 6; // C# - long 适用于来表示负数的整数
// 无符号 变长编码
// 1 2 4
uint32 testUInt = 7; // C# - uint 变长的编码
uint64 testULong = 8; // C# - ulong 变长的编码
// 固定字节数的类型
fixed32 testFixed32 = 9; // C# -uint 它通常用来表示大于2的28次方的数 ,比uint32更有效 始终是4个字节
fixed64 testFixed64 = 10; // C# -ulong 它通常用来表示大于2的56次方的数 ,比uint64更有效 始终是8个字节
sfixed32 testSFixed32 = 11; // C# - int 始终4个字节
sfixed64 testSFixed64 = 12; // C# - long 始终8个字节
// 其它类型
bool testBool = 13; // C# - bool
string testStr = 14; // C# - string
bytes testBytes = 15; // C# - BytesString 字节字符串
// 数组List
repeated int32 listInt = 16; // C# - 类似List<int>的使用
// 字典 Dictionary
map<int32, string> testMap = 17; // C# - 类似Dictionary<int, string> 的使用
// 枚举成员变量的声明 需要唯一编码
TestEnum testEnum = 18;
// 声明自定义类对象 需要唯一编码
// 默认值是null
TestMsg2 testMsg2 = 19;
// 规则 8:允许嵌套
// 嵌套一个类在另一个类当中 相当于是内部类
message TestMsg3 {
int32 testInt32 = 1;
}
TestMsg3 testMsg3 = 20;
// 规则 9:允许嵌套
enum TestEnum2 {
NORMAL = 0; // 第一个常量必须映射到 0
BOSS = 1;
}
TestEnum2 testEnum2 = 21;
// int32 testInt3233333 = 22;
bool testBool2123123 = 23;
GamePlayerTest.HeartMsg testHeart = 24;
// 告诉编译器 22 被占用 不准用户使用
// 之所以有这个功能 是为了在版本不匹配时
// 反序列化不会出现结构不统一 解析错误的问题
reserved 22;
// reserved testInt3233333;
}
// 枚举的声明
enum TestEnum {
NORMAL = 0; // 第一个常量必须映射到 0
BOSS = 5;
}
message TestMsg2 {
int32 testInt32 = 1;
}
test2.proto
syntax = "proto3";
// 规则 3:命名空间
package GamePlayerTest;
// 这决定了命名空间
message HeartMsg {
int64 time = 1;
}
3 生成 C# 代码
在 Protobuf 目录下创建文件夹 CodeGen,用于存放生成的 C# 代码。
打开 Powershell 窗口,进入 protoc.exe 所在文件夹,输入转换指令生成 C# 代码。
protoc -I="配置路径" --csharp_out="输出路径" 配置文件名
protoc -I="你的项目路径\Assets\Protobuf" --csharp_out="你的项目路径\Assets\Protobuf\CodeGen" test.proto
注意
路径不要有中文和特殊符号,避免生成失败。
在 Unity 中引入生成的 .cs 文件,通过
Google.Protobuf
命名空间使用。using UnityEngine; namespace Lesson { using GamePlayerTest; public class Lesson40 : MonoBehaviour { private void Start() { var msg = new TestMsg(); msg.TestBool = true; // 对应的和List以及Dictionary使用方式一样的 数组和字典对象 msg.ListInt.Add(1); print(msg.ListInt[0]); msg.TestMap.Add(1, "xxx"); print(msg.TestMap[1]); // 枚举 msg.TestEnum = TestEnum.Boss; // 内部枚举 msg.TestEnum2 = TestMsg.Types.TestEnum2.Boss; // 其它类对象 msg.TestMsg2 = new TestMsg2(); msg.TestMsg2.TestInt32 = 99; // 其它内部类对象 msg.TestMsg3 = new TestMsg.Types.TestMsg3(); msg.TestMsg3.TestInt32 = 55; // 在另一个生成的脚本当中的类 如果命名空间不同 需要命名空间点出来使用 msg.TestHeart = new GamePlayerTest.HeartMsg(); } } }
4 使用 Protobuf
4.1 序列化
先向 TestMsg 中写入一些内容,然后序列化为二进制文件。
- 使用方法
msg.WriteTo(fs)
msg.ToByteArray()
using UnityEngine;
namespace Lesson
{
using System.IO;
using GamePlayerTest;
using Google.Protobuf;
public class Lesson41 : MonoBehaviour
{
private void Start()
{
// 主要使用
// 1.生成的类中的 WriteTo方法
// 2.文件流FileStream对象
var msg = new TestMsg();
msg.ListInt.Add(1);
msg.TestBool = false;
msg.TestD = 5.5;
msg.TestInt32 = 99;
msg.TestMap.Add(1, "xxx");
msg.TestMsg2 = new TestMsg2();
msg.TestMsg2.TestInt32 = 88;
msg.TestMsg3 = new TestMsg.Types.TestMsg3();
msg.TestMsg3.TestInt32 = 66;
msg.TestHeart = new GamePlayerTest.HeartMsg();
msg.TestHeart.Time = 7777;
print(Application.persistentDataPath);
using (FileStream fs = File.Create(Application.persistentDataPath + "/TestMsg.bytes"))
{
// 需要 using Google.Protobuf; 才不会报错
// Google.Protobuf 中会调用扩展方法进行隐式转换
msg.WriteTo(fs);
}
}
}
}
将上述代码挂载到 Unity 场景中并运行后,在 Application.persistentDataPath 目录下可看到序列化文件。

在网络传输中,使用内存流进行传输。
using UnityEngine;
namespace Lesson
{
using System.IO;
using GamePlayerTest;
using Google.Protobuf;
public class Lesson41 : MonoBehaviour
{
private void Start()
{
// 主要使用
// 1.生成的类中的 WriteTo方法
// 2.内存流MemoryStream对象
var msg = new TestMsg();
...
using (var ms = new MemoryStream())
{
msg.WriteTo(ms);
// var bytes = msg.ToByteArray();
var bytes = ms.ToArray();
print("字节数组长度:" + bytes.Length);
}
}
}
}
4.2 反序列化
将本地二进制文件反序列化为 TestMsg 对象。
- 使用方法:
TestMsg.Parser.ParseFrom(fs)
using UnityEngine;
namespace Lesson
{
using System.IO;
using GamePlayerTest;
using Google.Protobuf;
public class Lesson41 : MonoBehaviour
{
private void Start()
{
// 主要使用
// 1.生成的类中的 Parser.ParseFrom方法
// 2.文件流FileStream对象
using (FileStream fs = File.OpenRead(Application.persistentDataPath + "/TestMsg.bytes"))
{
TestMsg msg2 = null;
msg2 = TestMsg.Parser.ParseFrom(fs);
print(msg2.TestMap[1]);
print(msg2.ListInt[0]);
print(msg2.TestD);
print(msg2.TestMsg2.TestInt32);
print(msg2.TestMsg3.TestInt32);
print(msg2.TestHeart.Time);
}
}
}
}
将上述代码挂载到 Unity 场景中并运行后,结果如下。

在网络传输中,使用内存流进行传输。
using UnityEngine;
namespace Lesson
{
using System.IO;
using GamePlayerTest;
using Google.Protobuf;
public class Lesson41 : MonoBehaviour
{
private void Start()
{
byte[] bytes;
...
// 主要使用
// 1.生成的类中的 Parser.ParseFrom方法
// 2.内存流MemoryStream对象
using (var ms = new MemoryStream(bytes))
{
print("内存流当中反序列化的内容");
TestMsg msg2 = TestMsg.Parser.ParseFrom(ms);
print(msg2.TestMap[1]);
print(msg2.ListInt[0]);
print(msg2.TestD);
print(msg2.TestMsg2.TestInt32);
print(msg2.TestMsg3.TestInt32);
print(msg2.TestHeart.Time);
}
}
}
}
5 Protobuf-Net
早期的 Protobuf 并不支持 C#,国外大佬 Marc Gravell 在 Protobuf 基础上进行 .Net 环境下的移植,并发布到 GitHub。
- Github 地址:https://github.com/protobuf-net/protobuf-net。
与官方 Protobuf 的区别
特性 | Google.Protobuf | Protobuf-net |
---|---|---|
开发语言 | 多语言支持 | 专为 .NET 优化 |
API 设计 | 基于代码生成 | 支持属性标记+动态序列化 |
Unity 兼容性 | 需新版本 Unity | 支持老版本 Unity |
性能优势 | 通用性强 | GC 优化更好,IL 动态生成代码 |
注意
Protobuf 不支持 .Net3.5 及以下版本。
如果想在 Unity 老版本中使用 Protobuf,只能使用 Protobuf-Net。
在较新版本的 Unity 中不存在这个问题。
如何判断是否支持?
把 Protobuf 相关 dll 包导入后能够正常使用不报错,则证明支持。
Protobuf-Net 是较老的生产方式,用于解决老版本 Unity 使用 Protobuf 的问题,使用方式和 Protobuf 类似,只是获取 DLL 文件、protoc.exe 文件的方式不同。