文章目录
- 1 Protobuf
- 1.1 导入 Protobuf
- 1.2 导入编译器
- 2 配置规则
- 2.1 注释
- 2.2 版本号
- 2.3 命名空间
- 2.4 消息类
- 2.4.1 float / double
- 2.4.2 int/sint/uint
- 2.4.3 fixed/sfixed
- 2.4.4 bool/string/bytes
- 2.5 特殊标识
- 2.5.1 required
- 2.5.2 optional
- 2.5.3 repeated
- 2.5.4 map
- 2.6 枚举
- 2.7 默认值
- 2.8 允许嵌套
- 2.9 保留字段
- 2.10 导入定义
- 附:完整 .proto 代码
- 3 生成 C# 代码
- 4 使用 Protobuf
- 4.1 序列化
- 4.2 反序列化
- 5 Protobuf-Net
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# - floatdouble 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; // 第一个常量必须映射到 0BOSS = 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; // 第一个常量必须映射到 0BOSS = 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 8int64 testInt64 = 4; // C# - long 它不太适用于来表示负数 请使用 sint64// 更实用与表示负数类型的整数sint32 testSInt32 = 5; // C# - int 适用于来表示负数的整数sint64 testSInt64 = 6; // C# - long 适用于来表示负数的整数// 无符号 变长编码// 1 2 4uint32 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# - stringbytes testBytes = 15; // C# - BytesString 字节字符串// 数组Listrepeated int32 listInt = 16; // C# - 类似List<int>的使用// 字典 Dictionarymap<int32, string> testMap = 17; // C# - 类似Dictionary<int, string> 的使用// 枚举成员变量的声明 需要唯一编码TestEnum testEnum = 18;// 声明自定义类对象 需要唯一编码// 默认值是nullTestMsg2 testMsg2 = 19;// 规则 8:允许嵌套// 嵌套一个类在另一个类当中 相当于是内部类message TestMsg3 {int32 testInt32 = 1;}TestMsg3 testMsg3 = 20;// 规则 9:允许嵌套enum TestEnum2 {NORMAL = 0; // 第一个常量必须映射到 0BOSS = 1;}TestEnum2 testEnum2 = 21;// int32 testInt3233333 = 22;bool testBool2123123 = 23;GamePlayerTest.HeartMsg testHeart = 24;// 告诉编译器 22 被占用 不准用户使用// 之所以有这个功能 是为了在版本不匹配时 // 反序列化不会出现结构不统一 解析错误的问题reserved 22;
// reserved testInt3233333;
}// 枚举的声明
enum TestEnum {NORMAL = 0; // 第一个常量必须映射到 0BOSS = 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 文件的方式不同。