在unity中使用protobuf-net库

前几天尝试了下在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包到本地,待会我们需要修改它的一些代码。

在unity中使用protobuf-net库

proto版本使用proto3。

C# IDE使用visual studio 2019社区版。

编译protobuf-net

解压我们之前下载的zip包,如下图所示,使用visual studio 2019打开protobuf-net-3.0.62/src/protobuf-net.sln解决方案文件。

在unity中使用protobuf-net库

打开后,可以看到解决方案资源管理器的src文件夹下有若干工程文件,我们只需要编译protobuf-net和protobuf-net.Core两个工程即可。由于protobuf-net依赖于protobuf-net.Core,所以,实际上我们只需要启动protobuf-net工程的编译就行了。

在unity中使用protobuf-net库

先切换到Release模式,然后右键点击protobuf-net工程文件,点击重新生成。

在unity中使用protobuf-net库

编译完成后,将在如下图所示的目录中生成面向不同.Net版本的库文件。由于unity2020版本已经支持.NetFramework4.x,所以我们可以直接在unity中使用net461下的库。

在unity中使用protobuf-net库

unity中使用protobuf-net库

新建一个unity工程,在根目录下新建一个文件夹protobuf-net/Plugins,拷贝上一步编译出来的net461文件夹中的所有文件到该文件夹下。

在unity中使用protobuf-net库

unity切换到il2cpp模式,API版本选用.Net4x。为了保证这些dll可以被il2cpp识别,我们需要使用link.xml来配置需要打进包中的dll。

从proto文件生成cs代码

protobuf-net官方提供了一个从proto文件生成代码的工具NuGet Gallery | protobuf-net.Protogen 3.0.101

在unity中使用protobuf-net库

点击Download package按钮即可下载该工具的nuget包到本地,下载下来的是一个.nupkg文件,使用7z解压该文件。

解压后如下图所示:

在unity中使用protobuf-net库

打开tools文件夹,有net5.0版本和netcoreapp3.1版本两个版本的工具供我们选择,我们选用netcoreapp3.1工具作为我们的proto适配代码生成工具。

在unity中使用protobuf-net库

拷贝工具至工程目录下,如下图所示:

在unity中使用protobuf-net库

假定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中使用protobuf-net库

说明我们的序列化和反序列化都成功了。

将unity工程通过il2cpp方式打包出一个Android包,放到真机上实验一下效果,会发现在真机上会报错,如下图所示:

在unity中使用protobuf-net库

这是由于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。

在unity中使用protobuf-net库

修改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在真机上测试一下。

在真机上测试的结果如下图所示:

在unity中使用protobuf-net库

可以看到,原来的il2cpp无法使用反射调用的错误消失了,取而代之的是一个ProtoBuf.Serializers.VectorSerializer<int>泛型未生成的报错,通过il2cpp打包时,一般情况下都要在代码中显式定义泛型的类型,以保证unity为其生成代码

查阅protobuf-net源码,可以看到VectorSerializer是一个sealed类。

在unity中使用protobuf-net库

为了能够在unity中显式定义VectorSerializer<int>等类型,我们需要在它的类定义前方加上public。

在unity中使用protobuf-net库

按照第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中使用protobuf-net库

真机上打印出如上图所示的结果,说明真机上的序列化和反序列化成功了。

unity工程的github地址为:searock96/protobuf-net-demo-for-unity: use protobuf-net library in unity. (github.com)

上一篇:c#网络模块


下一篇:grpc的.net core使用