前几天尝试了下在unity中使用protobuf-net库,踩到了一些坑,本篇文章用于总结一下如何在unity中使用protobuf-net库。
测试环境
unity版本为2020.3.3f1c1,使用il2cpp打包,目标环境为Android,protobuf-net的版本选用当前最新的Release版本3.0.62。
首先从protobuf-net/protobuf-net/releases下载3.0.62版本的zip包到本地,待会我们需要修改它的一些代码。
proto版本使用proto3。
C# IDE使用visual studio 2019社区版。
编译protobuf-net
解压我们之前下载的zip包,如下图所示,使用visual studio 2019打开protobuf-net-3.0.62/src/protobuf-net.sln解决方案文件。
打开后,可以看到解决方案资源管理器的src文件夹下有若干工程文件,我们只需要编译protobuf-net和protobuf-net.Core两个工程即可。由于protobuf-net依赖于protobuf-net.Core,所以,实际上我们只需要启动protobuf-net工程的编译就行了。
先切换到Release模式,然后右键点击protobuf-net工程文件,点击重新生成。
编译完成后,将在如下图所示的目录中生成面向不同.Net版本的库文件。由于unity2020版本已经支持.NetFramework4.x,所以我们可以直接在unity中使用net461下的库。
unity中使用protobuf-net库
新建一个unity工程,在根目录下新建一个文件夹protobuf-net/Plugins,拷贝上一步编译出来的net461文件夹中的所有文件到该文件夹下。
unity切换到il2cpp模式,API版本选用.Net4x。为了保证这些dll可以被il2cpp识别,我们需要使用link.xml来配置需要打进包中的dll。
从proto文件生成cs代码
protobuf-net官方提供了一个从proto文件生成代码的工具NuGet Gallery | protobuf-net.Protogen 3.0.101。
点击Download package按钮即可下载该工具的nuget包到本地,下载下来的是一个.nupkg文件,使用7z解压该文件。
解压后如下图所示:
打开tools文件夹,有net5.0版本和netcoreapp3.1版本两个版本的工具供我们选择,我们选用netcoreapp3.1工具作为我们的proto适配代码生成工具。
拷贝工具至工程目录下,如下图所示:
假定proto文件夹位于工程的protos文件夹下,生成得到的cs文件放在Assets/Scripts/protoscripts文件夹下。
编写一个unity工具脚本来调用这个protogen.dll以生成cs文件:
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;
using UnityEngine;
using UnityEditor;
public class ProtogenTool
{
[MenuItem("Tools/生成所有proto文件的CSharp代码")]
public static void GenerateProtos()
{
string protoFolder = "./protos";
string protogenTool = "./protogen/protogen.dll";
string protoScriptFolder = "./Assets/Scripts/protoscripts";
//清空代码文件夹下所有cs文件
if (Directory.Exists(protoScriptFolder))
{
Directory.Delete(protoScriptFolder, true);
}
//获取proto文件夹下所有proto文件
if (!Directory.Exists(protoFolder))
{
return;
}
var protoFiles = Directory.GetFiles(protoFolder, "*.proto", SearchOption.AllDirectories);
if (protoFiles == null || protoFiles.Length == 0)
{
return;
}
for (int i = 0; i < protoFiles.Length; i++)
{
protoFiles[i] = protoFiles[i].Replace('\\', '/');
}
//创建输出目录
Directory.CreateDirectory(protoScriptFolder);
//调用protogen生成cs文件
StringBuilder stringBuilder = new StringBuilder();
//指定需要调用protogen
stringBuilder.Append(protogenTool);
//+names=original表示命名方式使用proto中的字段名
stringBuilder.Append(" +names=original");
//指定proto文件夹路径
stringBuilder.Append(" --proto_path=");
stringBuilder.Append(protoFolder.Replace('/', '\\').TrimEnd('\\'));
//指定生成C#代码
stringBuilder.Append(" --csharp_out=");
stringBuilder.Append(protoScriptFolder.Replace('/', '\\').TrimEnd('\\'));
for (int i = 0; i < protoFiles.Length; i++)
{
stringBuilder.Append(" ");
stringBuilder.Append(Path.GetFileName(protoFiles[i]));
}
//调用dotnet执行proto=>cs文件的转换
var process = Process.Start("dotnet", stringBuilder.ToString());
process.WaitForExit();
//触发unity工程的刷新
AssetDatabase.Refresh();
}
}
Android真机上的序列化与反序列化
protobuf对象的序列化与反序列化可以通过调用ProtoBuf.Serializer提供Serialize和Deserialize接口来实现。
在unity中创建一个名为ProtobufTest的monobehaviour类:
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using config;
using UnityEngine;
using ProtoBuf;
public class ProtobufTest : MonoBehaviour
{
private List<string> errorLogList = new List<string>();
private void Awake()
{
Application.logMessageReceived += (condition, trace, type) =>
{
if (type == LogType.Assert || type == LogType.Error || type == LogType.Exception)
{
errorLogList.Add(condition);
if (errorLogList.Count > 20)
{
errorLogList.RemoveAt(0);
}
}
};
}
private void Start()
{
Test test = CreateTestInstance();
byte[] binaryData;
using (MemoryStream memoryStream = new MemoryStream())
{
Serializer.Serialize(memoryStream, test);
binaryData = memoryStream.ToArray();
}
using (MemoryStream memoryStream2 = new MemoryStream(binaryData))
{
Test test2 = Serializer.Deserialize<Test>(memoryStream2);
Debug.LogError(TestToString(test2));
}
}
private Test CreateTestInstance()
{
Test test = new Test();
test.intValue = 1;
test.floatValue = 2.2f;
test.longValue = 10;
test.doubleValue = 1.23f;
test.stringValue = "122333.fffsdf";
test.intArrayValue = new int[] {1, 2, 3};
test.floatArrayValue = new float[] {1, 2, 3};
test.longArrayValue = new long[] {12000, 12312412124, 1123412412L};
test.stringArrayValue.Add("str1");
test.stringArrayValue.Add("str2");
test.stringArrayValue.Add("str3");
test.doubleArrayValue = new double[] {1.1, 2.32, 4.5};
test.intStringPairValue = new IntStringPair() {key = 1, value = "str1"};
return test;
}
private string TestToString(Test test)
{
string output = "";
output += "test.intValue:" + test.intValue + "\r\n";
output += "test.floatValue:" + test.floatValue + "\r\n";
output += "test.doubleValue:" + test.doubleValue + "\r\n";
output += "test.stringValue:" + test.stringValue + "\r\n";
output += "test.longValue:" + test.longValue + "\r\n";
for (int i = 0; i < test.intArrayValue.Length; i++)
{
output += "test.intArrayValue[" + i + "]" + test.intArrayValue[i] + "\r\n";
}
for (int i = 0; i < test.floatArrayValue.Length; i++)
{
output += "test.floatArrayValue[" + i + "]" + test.floatArrayValue[i] + "\r\n";
}
for (int i = 0; i < test.doubleArrayValue.Length; i++)
{
output += "test.doubleArrayValue[" + i + "]" + test.doubleArrayValue[i] + "\r\n";
}
for (int i = 0; i < test.stringArrayValue.Count; i++)
{
output += "test.stringArrayValue[" + i + "]" + test.stringArrayValue[i] + "\r\n";
}
for (int i = 0; i < test.longArrayValue.Length; i++)
{
output += "test.longArrayValue[" + i + "]" + test.longArrayValue[i] + "\r\n";
}
output += "test.intStringPairValue.key:" + test.intStringPairValue.key + "\r\n";
output += "test.intStringPairValue.value:" + test.intStringPairValue.value + "\r\n";
return output;
}
private void OnGUI()
{
GUI.contentColor = Color.red;
for (int i = errorLogList.Count - 1; i >= 0; i--)
{
GUILayout.Label(errorLogList[i]);
}
}
}
在场景中挂上该脚本,编辑器中运行可以得到如下结果:
说明我们的序列化和反序列化都成功了。
将unity工程通过il2cpp方式打包出一个Android包,放到真机上实验一下效果,会发现在真机上会报错,如下图所示:
这是由于il2cpp模式下反射功能受到了限制,不能通过System.Reflection.Emit动态创建类型,所以为了使用protobuf-net,我们需要预先生成protomodel类型来进行序列化和反序列化。(参考自c# - Protobuf-net & IL2CPP - System.Reflection.Emit is not supported - Stack Overflow)
在Assets/protobuf-net/Editor下创建一个工具类ProtobufNetHelperEditor.cs,内容如下所示:
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;
using ProtoBuf;
using ProtoBuf.Meta;
using UnityEngine;
using UnityEditor;
using UnityEditor.Compilation;
using Assembly = System.Reflection.Assembly;
public class ProtobufNetHelperEditor
{
public const string protobufNetDirRoot = "Assets/protobuf-net";
public const string protobuf_net_dll_path = "Assets/protobuf-net/Plugins/protobuf-net.dll";
/// <summary>
/// 示例类型,用于从该类型所在的Assembly中获取到所有protobuf能够使用的类型,每个相关dll中只需要填一个即可
/// </summary>
public static List<Type> exampleTypes = new List<Type>()
{
typeof(config.Test),
};
[MenuItem("Tools/重建protobuf-model.dll")]
private static void RebuildProtobufModelForProject()
{
RuntimeTypeModel typeModel = GetModel(out string typeNames);
if (typeModel == null)
{
return;
}
typeModel.Compile("ProjectModel", "protobuf-model.dll");
if (!Directory.Exists(protobufNetDirRoot + "/Plugins"))
{
Directory.CreateDirectory(protobufNetDirRoot + "/Plugins");
}
File.Copy("protobuf-model.dll", protobufNetDirRoot + "/Plugins/protobuf-model.dll", true);
File.Delete("protobuf-model.dll");
UnityEngine.Debug.Log("为以下类型重建protobuf-model.dll\r\n" + typeNames);
AssetDatabase.Refresh();
}
private static RuntimeTypeModel GetModel(out string typeNames)
{
List<Type> types = GetAllRelatedTypeList();
RuntimeTypeModel typeModel = RuntimeTypeModel.Create();
StringBuilder stringBuilder = new StringBuilder();
List<Type> list = new List<Type>();
foreach (var t in types)
{
var contract = t.GetCustomAttributes(typeof(ProtoContractAttribute), false);
if (contract.Length > 0 && !list.Contains(t))
{
typeModel.Add(t, true);
stringBuilder.Append(t.ToString());
stringBuilder.Append("\r\n");
list.Add(t);
}
}
typeNames = stringBuilder.ToString();
return typeModel;
}
private static List<Type> GetAllRelatedTypeList()
{
List<Type> list = new List<Type>();
List<string> assemblyNames = new List<string>();
for (int i = 0; i < exampleTypes.Count; i++)
{
var assembly = Assembly.GetAssembly(exampleTypes[i]);
if (assemblyNames.Contains(assembly.FullName))
{
continue;
}
assemblyNames.Add(assembly.FullName);
list.AddRange(assembly.GetTypes());
}
return list;
}
}
运行"Tools/重建protobuf-model.dll"工具,即可生成一个protobuf-model.dll,代码中调用序列化和反序列化时,都应当使用我们刚才生成的protobuf-model.dll中的ProjectModel来进行。
在link.xml中加上我们刚才生成的protobuf-model.dll。
修改ProtobufTest.cs如下图所示:
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using config;
using UnityEngine;
using ProtoBuf;
using ProtoBuf.Meta;
public class ProtobufTest : MonoBehaviour
{
private List<string> errorLogList = new List<string>();
private ProjectModel typeModel = new ProjectModel();
private void Awake()
{
Application.logMessageReceived += (condition, trace, type) =>
{
if (type == LogType.Assert || type == LogType.Error || type == LogType.Exception)
{
errorLogList.Add(condition);
if (errorLogList.Count > 20)
{
errorLogList.RemoveAt(0);
}
}
};
}
private void Start()
{
Test test = CreateTestInstance();
byte[] binaryData;
using (MemoryStream memoryStream = new MemoryStream())
{
typeModel.Serialize(memoryStream, test);
binaryData = memoryStream.ToArray();
}
using (MemoryStream memoryStream2 = new MemoryStream(binaryData))
{
Test test2 = typeModel.Deserialize<Test>(memoryStream2);
Debug.LogError(TestToString(test2));
}
}
private Test CreateTestInstance()
{
Test test = new Test();
test.intValue = 1;
test.floatValue = 2.2f;
test.longValue = 10;
test.doubleValue = 1.23f;
test.stringValue = "122333.fffsdf";
test.intArrayValue = new int[] {1, 2, 3};
test.floatArrayValue = new float[] {1, 2, 3};
test.longArrayValue = new long[] {12000, 12312412124, 1123412412L};
test.stringArrayValue.Add("str1");
test.stringArrayValue.Add("str2");
test.stringArrayValue.Add("str3");
test.doubleArrayValue = new double[] {1.1, 2.32, 4.5};
test.intStringPairValue = new IntStringPair() {key = 1, value = "str1"};
return test;
}
private string TestToString(Test test)
{
string output = "";
output += "test.intValue:" + test.intValue + "\r\n";
output += "test.floatValue:" + test.floatValue + "\r\n";
output += "test.doubleValue:" + test.doubleValue + "\r\n";
output += "test.stringValue:" + test.stringValue + "\r\n";
output += "test.longValue:" + test.longValue + "\r\n";
for (int i = 0; i < test.intArrayValue.Length; i++)
{
output += "test.intArrayValue[" + i + "]" + test.intArrayValue[i] + "\r\n";
}
for (int i = 0; i < test.floatArrayValue.Length; i++)
{
output += "test.floatArrayValue[" + i + "]" + test.floatArrayValue[i] + "\r\n";
}
for (int i = 0; i < test.doubleArrayValue.Length; i++)
{
output += "test.doubleArrayValue[" + i + "]" + test.doubleArrayValue[i] + "\r\n";
}
for (int i = 0; i < test.stringArrayValue.Count; i++)
{
output += "test.stringArrayValue[" + i + "]" + test.stringArrayValue[i] + "\r\n";
}
for (int i = 0; i < test.longArrayValue.Length; i++)
{
output += "test.longArrayValue[" + i + "]" + test.longArrayValue[i] + "\r\n";
}
output += "test.intStringPairValue.key:" + test.intStringPairValue.key + "\r\n";
output += "test.intStringPairValue.value:" + test.intStringPairValue.value + "\r\n";
return output;
}
private void OnGUI()
{
GUI.contentColor = Color.red;
for (int i = errorLogList.Count - 1; i >= 0; i--)
{
GUILayout.Label(errorLogList[i]);
}
}
}
编辑器中尝试运行,结果与之前一致,再次打一个Android在真机上测试一下。
在真机上测试的结果如下图所示:
可以看到,原来的il2cpp无法使用反射调用的错误消失了,取而代之的是一个ProtoBuf.Serializers.VectorSerializer<int>
泛型未生成的报错,通过il2cpp打包时,一般情况下都要在代码中显式定义泛型的类型,以保证unity为其生成代码
查阅protobuf-net源码,可以看到VectorSerializer是一个sealed类。
为了能够在unity中显式定义VectorSerializer<int>
等类型,我们需要在它的类定义前方加上public。
按照第2步的方法重新编译一遍protobuf-net.dll和protobuf-net.Core.dll,然后放回unity中。
在unity中新建一个TypeReverse类,引用VectorSerializer<T>
的常见值类型和string的泛型类型实例。
using System;
using System.Collections.Generic;
public class TypeReverse
{
public List<Type> typeList = new List<Type>()
{
typeof(ProtoBuf.Serializers.VectorSerializer<int>),
typeof(ProtoBuf.Serializers.VectorSerializer<uint>),
typeof(ProtoBuf.Serializers.VectorSerializer<short>),
typeof(ProtoBuf.Serializers.VectorSerializer<ushort>),
typeof(ProtoBuf.Serializers.VectorSerializer<long>),
typeof(ProtoBuf.Serializers.VectorSerializer<ulong>),
typeof(ProtoBuf.Serializers.VectorSerializer<byte>),
typeof(ProtoBuf.Serializers.VectorSerializer<decimal>),
typeof(ProtoBuf.Serializers.VectorSerializer<bool>),
typeof(ProtoBuf.Serializers.VectorSerializer<float>),
typeof(ProtoBuf.Serializers.VectorSerializer<double>),
typeof(ProtoBuf.Serializers.VectorSerializer<string>),
};
}
重新打包然后放在真机上进行测试。
真机上打印出如上图所示的结果,说明真机上的序列化和反序列化成功了。
unity工程的github地址为:searock96/protobuf-net-demo-for-unity: use protobuf-net library in unity. (github.com)