在 Unity 开发中,当涉及网络通信或本地文件存储时,相信大家对 Protobuf(Protocol Buffers) 这个库可能并不陌生。接下来,我将简要介绍这个库的基本使用方法,并探讨它适用的场景。
1.库资源与官方教程
protobuf的github地址
protobuf的官方教程
1.1 获取所需组件:
1.访问 GitHub 仓库: 打开上述 GitHub 链接。
2.下载运行时库:
在仓库的 Releases/Tags 页面中,选择一个你认为稳定的版本(例如,protobuf csharp-31.0 或类似的 C# 特定包)。这个库(通常以 NuGet 包或源代码形式提供)是你项目需要引用的核心组件。
1.1.3.下载命令行工具 (protoc):
在同一个 Releases/Tags 页面下,找到并下载适用于你操作系统的 protoc 编译器(例如 protoc-31.0-win64.zip)。这个工具用于将 .proto 文件编译成 C# 代码。
1.2 关键组件说明:
运行时库 (Runtime Library): 集成到你的 Unity 项目中,用于在运行时序列化和反序列化 Protobuf 数据。
编译器 (protoc): 独立的命令行工具,用于编译 .proto 接口定义文件,生成目标语言(如 C#)的数据访问类。这两个东西截图如下:
一个是源码,一个是protoc的工具。
2.使用vs编译源码,
我这边使用的是vs2022,如果提示你netcode版本低根据提示下载netcode6重新打开即可,
2.1 解压protobuf-31.0这个zip
2.2 然后打开 你的文件路径\Downloads\protobuf-31.0\csharp\src这个文件夹
2.3 双击Google.Protobuf.sln这个文件
2.4 打开项目后选择Google.Protobuf这个文件点击鼠标右键选择生成按钮
2.5 等生成结束后我们就可以在 你的工程目录\protobuf-31.0\csharp\src\Google.Protobuf\bin\Debug这里面看到生成的脚本了。
我们这里选择net45,然后将里面的文件Google.Protobuf.dll System.Buffers.dll System.Memory.dll System.Runtime.CompilerServices.Unsafe.dll这些放到我们新建的unity工程下面。
最终我将文件放到的目录如下图:
至于这个protoc工具我们后面再介绍。
3.有了protobuf的动态库我们就有了开发protobuf的能力了接下来我们要介绍的是编写protobuf脚本。
我们在工程script文件目录结构下创建一个文件夹用于写protobuf的文件(注意文件的结尾都要用.proto结尾而不是(text)
这里我们创建了两个文件test1以及test2,
3.1 首先看test2的脚本这个脚本比较简单代码如下
syntax = "proto3";//文档版本号
package GameSysTest;//命名控件
message HeartMsg
{
int64 time = 1;
}
其中必填项为syntax = “proto3”; package可以不带,也可以带,下面的message HeartMsg这个代表的是一个类这个类名字叫做HeartMsg,int64 time = 1;是里面定义的字段代表生成一个long类型的字段名字叫做time =1是必须的这个代表是第几个参数。
3.2 接下来是复杂的脚本test1代码如下
syntax = "proto3";//文档版本号
import "test2.proto";
package GamePlayerTest;//命名控件
message TestMessage {
float testF = 1;
double testD = 2;
int32 testInt32 = 3;//int 会根据数字大小来使用字节数存储
int64 testInt64 = 4;//lomng 会根据数字大小来使用字节数存储
sint32 testsInt32 = 5;//int 会根据数字大小来使用字节数存储
sint64 testsInt64 = 6;//lomng 会根据数字大小来使用字节数存储
bool testBool = 7;
string testString = 8;
bytes testBytes = 9;//C#的byteString
repeated int32 listInt = 10;//list数据
map<int32, string> testMap = 11;//字典
TestEnum testEnum = 12;//枚举
TestMsg2 testMsg2 = 13;//自定义类
TestMsg3 testMsg3 = 14;//自定义内部类
int32 testInt322222 = 15;//
GameSysTest.HeartMsg heartMsg = 16;
//内部类
message TestMsg3 {
int32 testInt32 = 1;
}
}
enum TestEnum {
Normal = 0;//第一个必须为0
BOSS = 2;
}
message TestMsg2 {
int32 testInt32 = 1;
}
里面的语法都比较简单,其中包含枚举定义,内部类定义,外部类定义,以及引用文件test2的类处理。语法层面的大家可以看下官方文档介绍。
4. 编译写好的脚本
有了proto文件test1以及test2以及运行时需要的库,接下来就要用到前面下载的protoc的工具了。
4.1解压并获取bin文件
解压已经下载的protoc-31.0-win64.zip,我们从里面找到bin目录下面的protoc.exe,然后放到我们的unity工程目录下面,我是放到了如下的路径中
接下来是编译的过程了我们打开命令函工具cmd,我的是windows电脑mac电脑的可能会有所差异。
4.1命令行编译proto文件
最终我的命令行如下
G:\UnityProjectsTest\TestUI\ProtoBuf\protoc.exe -I=G:\UnityProjectsTest\TestUI\Assets\Scripts\TestProto\text --csharp_out=G:\UnityProjectsTest\TestUI\Assets\Scripts\TestProto\output test1.proto
其实主要内容就是 找到exe路径 然后执行 I=proto文件路径 --csharp_out 输出文件路径 test1.proto代表我要编译test1这个proto文件,同时同样的命令 后面把test1.proto这个文件名替换为test2.proto即可编译test2.proto文件。
编译没问题后最后生成文件如下:
会看到生成这两个脚本。
5. 生成脚本的使用
我们的proto脚本生成后如何使用下面的代码是使用的处理
public class TestProto : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
TestMessage test = new TestMessage();
test.TestInt32 = 1;
test.TestString = "你好啊";
test.HeartMsg = new GameSysTest.HeartMsg();
test.HeartMsg.Time = 100000;
print(Application.persistentDataPath + "/Testmsg.txt");
/*using (FileStream fs= File.Create(Application.persistentDataPath+"/Testmsg.txt"))
{
test.WriteTo(fs);
}*/
TestMessage msg;
using (FileStream fs = File.OpenRead(Application.persistentDataPath + "/Testmsg.txt"))
{
msg = TestMessage.Parser.ParseFrom(fs);
}
print(msg.TestString);
print(msg.HeartMsg.Time);
byte[] data;
using (MemoryStream ms = new MemoryStream())
{//将二进制转换为网络传输数据
test.WriteTo(ms);
data = ms.ToArray();
}
TestMessage msg3;
using (MemoryStream ms = new MemoryStream(data))
{//将网络传输数据转换为二进制数据
msg3 = TestMessage.Parser.ParseFrom(ms);
}
}
}
这里我本着最简单最易懂的原则说下用法。
下面这一段意思是我们可以通过调用api将创建的TestMessage对象数据以二进制的形式存储到文件中去
using (FileStream fs= File.Create(Application.persistentDataPath+"/Testmsg.txt"))
{
test.WriteTo(fs);
}
下面这一段意思我们可以通过文件流将对象直接从本地文件读出来。
TestMessage msg;
using (FileStream fs = File.OpenRead(Application.persistentDataPath + "/Testmsg.txt"))
{
msg = TestMessage.Parser.ParseFrom(fs);
}
print(msg.TestString);
print(msg.HeartMsg.Time);
有人会问我通过json存储对应的数据直接转换不也一样能达到目的吗,这样想没问题并且存到本地的数据也能读取出来,但是使用二进制存储读取数据的好处是我们可以快速的读取数据到对象并且这个过程GC很小,所以这么写主要为了提高效率同时省去了不少解析json的过程处理起来也简单。
我们再来看下面:
byte[] data;
using (MemoryStream ms = new MemoryStream())
{//将二进制转换为网络传输数据
test.WriteTo(ms);
data = ms.ToArray();
}
TestMessage msg3;
using (MemoryStream ms = new MemoryStream(data))
{//将网络传输数据转换为二进制数据
msg3 = TestMessage.Parser.ParseFrom(ms);
}
这块代码主要用处是网络传输比如tcp udp 或者kcp的消息通信,我们可以将对象直接转化为二进对象传递给对方,同时对方收到二进制数据通过这种方式转换为对象大大减少了自定义对象的定义及解析的工作量。
6. 制作脚本编辑生成工具
使用命令行模式假如proto文件比较多的情况下一行一行的执行命令比较麻烦,这里我引用了唐老师的自动解析proto文件的类下面是这个类的处理,感谢唐老师的课程
/// <summary>
/// Protobuf 自动编译工具类
/// 当存在大量 .proto 文件时,逐行执行命令行操作较为繁琐
/// 本类实现批量编译功能,特别感谢唐老师提供的解决方案
/// </summary>
public class ProtobufTool
{
// .proto 文件存储目录
private static string PROTO_PATH = "C:\\Users\\MECHREVO\\Desktop\\TeachNet\\Protobuf\\proto";
// protoc 编译器可执行文件路径
private static string PROTOC_PATH = "C:\\Users\\MECHREVO\\Desktop\\TeachNet\\Protobuf\\protoc.exe";
// C# 代码生成目录
private static string CSHARP_PATH = "C:\\Users\\MECHREVO\\Desktop\\TeachNet\\Protobuf\\csharp";
// C++ 代码生成目录
private static string CPP_PATH = "C:\\Users\\MECHREVO\\Desktop\\TeachNet\\Protobuf\\cpp";
// Java 代码生成目录
private static string JAVA_PATH = "C:\\Users\\MECHREVO\\Desktop\\TeachNet\\Protobuf\\java";
/// <summary>
/// 生成 C# 协议代码
/// </summary>
[MenuItem("ProtobufTool/生成C#代码")]
private static void GenerateCSharp()
{
Generate("csharp_out", CSHARP_PATH);
}
/// <summary>
/// 生成 C++ 协议代码
/// </summary>
[MenuItem("ProtobufTool/生成C++代码")]
private static void GenerateCPP()
{
Generate("cpp_out", CPP_PATH);
}
/// <summary>
/// 生成 Java 协议代码
/// </summary>
[MenuItem("ProtobufTool/生成Java代码")]
private static void GenerateJava()
{
Generate("java_out", JAVA_PATH);
}
/// <summary>
/// 执行协议文件批量编译
/// </summary>
/// <param name="outCmd">输出类型命令参数</param>
/// <param name="outPath">代码输出目录</param>
private static void Generate(string outCmd, string outPath)
{
// 获取 .proto 文件目录信息
DirectoryInfo directoryInfo = Directory.CreateDirectory(PROTO_PATH);
// 获取目录下所有文件
FileInfo[] files = directoryInfo.GetFiles();
// 遍历处理所有 .proto 文件
for (int i = 0; i < files.Length; i++)
{
// 仅处理 .proto 扩展名的文件
if (files[i].Extension == ".proto")
{
// 创建编译进程
Process cmd = new Process();
// 设置 protoc 编译器路径
cmd.StartInfo.FileName = PROTOC_PATH;
// 构建编译命令参数
cmd.StartInfo.Arguments = $"-I={PROTO_PATH} --{outCmd}={outPath} {files[i]}";
// 启动编译进程
cmd.Start();
// 输出单文件编译完成日志
UnityEngine.Debug.Log($"{files[i].Name} 编译完成");
}
}
// 输出批量编译完成日志
UnityEngine.Debug.Log("所有协议文件编译完成");
}
}
感谢大家的阅读有什么疑问或者问题可以随时评论。