ProtoBuf的使用有两种方式,一种是以.proto文件中转的方式,另一种是使用“protobuf-net”的方式,这里介绍第二种

获取Protobuf-net

GitHub - protobuf-net/protobuf-net: Protocol Buffers library for idiomatic .NET

首先在Visual Studio里面点击“工具——NuGet包管理器——管理解决方案的NuGet程序包”。

在打开的窗口中选择“浏览”标签页。输入“protobuf-net”点击搜索。点击protobuf-net,在右侧点击项目,选择最新稳定版并安装。

此时又会弹出一个窗口,要求添加新的引用,点击确定。

注意:
在Unity主工程中,可能会打不开“管理解决方案的NuGet程序包”窗口,我们可以在HotFix工程中打开此窗口并安装protobuf-net,但是要注意,安装后,我们在右侧的“解决方案资源管理器——引用”中,删除protobuf-net.dllprotobuf-net.Core.dllSystem.Buffers.dllSystem.Collections.Immutable.dllSystem.Memory.dllSystem.Numerics.Vectors.dllSystem.Runtime.CompilerServices.Unsafe.dll

进入“C:\Users\zerwa\.nuget\packages”文件夹,我们需要将一些dll库复制到Unity的Assets——Plugins——protobuf文件夹里面

  1. C:\Users\zerwa\.nuget\packages\protobuf-net\3.2.26\lib\net462\protobuf-net.dll
  2. C:\Users\zerwa\.nuget\packages\protobuf-net.core\3.2.26\lib\net462\protobuf-net.Core.dll
  3. C:\Users\zerwa\.nuget\packages\system.collections.immutable\7.0.0\lib\net462\System.Collections.Immutable.dll
  4. C:\Users\zerwa\.nuget\packages\system.memory\4.5.5\lib\net461\System.Memory.dll
  5. C:\Users\zerwa\.nuget\packages\system.numerics.vectors\4.5.0\lib\net46\System.Numerics.Vectors.dll
  6. C:\Users\zerwa\.nuget\packages\system.runtime.compilerservices.unsafe\6.0.0\lib\net461\System.Runtime.CompilerServices.Unsafe.dll

上面只是示例的dll库位置,不同的版本用作参考即可

Unity准备

在GameData——Data文件夹内新建“ProtobufData”文件夹

选中Assets——ResFrame——Editor——Resource中的“ABConfig”,添加一个All File Dir AB

1
2
AB Name:protobuffdata
Path:Assets/GameData/Data/ProtobufData

编辑器转换工具

ProtoBuff作为二进制文件的替代,我们也需要完善好“XmlToProto”的方法

修改ResFrameConfig脚本,使用这个脚本配置生成protobuf二进制文件时保存的文件夹路径。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
using UnityEngine;
using UnityEditor;
[CreateAssetMenu(fileName ="ResFrameConfig",menuName ="CreateResFrameConfig",order = 2)]
public class ResFrameConfig : ScriptableObject
{
//...
[Tooltip("存放protobuf二进制文件的文件夹路径")]
public string m_ProtobufPath = "Assets/GameData/Data/ProtobufData/";
}
[CustomEditor(typeof(ResFrameConfig))]
public class ResFrameConfigInspector : Editor
{
//...
SerializedProperty m_ProtobufPath;

private void OnEnable()
{
//...
m_ProtobufPath = serializedObject.FindProperty("m_ProtobufPath");
}
public override void OnInspectorGUI()
{
//...
EditorGUILayout.PropertyField(m_ProtobufPath, new GUIContent("Protobuf二进制文件路径"));
EditorGUILayout.Space(5);
serializedObject.ApplyModifiedProperties();
}
}

如果想要修改具体的路径,在Assets——ResFrame——Editor文件夹内点击“ResFrameConfig”并修改即可

修改BinarySerializeOpt,添加Protobuf序列化和反序列化的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
public class BinarySerializeOpt
{
/// <summary>
/// 类序列化成protobuf二进制
/// </summary>
/// <param name="path">proto二进制生成位置</param>
/// <param name="obj">需要序列化的类</param>
/// <returns>是否成功</returns>
public static bool ProtoSerialize(string path, System.Object obj)
{
try
{
using (Stream file = File.Create(path))
{
ProtoBuf.Serializer.Serialize(file, obj);
return true;
}
}
catch (Exception e)
{
Debug.LogError("类转protobuf失败:" + obj.GetType() + "," + e.Message);
}
return false;
}
/// <summary>
/// proto二进制反序列化
/// </summary>
/// <param name="path">proto二进制文件路径</param>
public static T ProtoDeserialize<T>(string path) where T : class
{
T t = default(T);
try
{
using (Stream file = File.OpenRead(path))
{
t = ProtoBuf.Serializer.Deserialize<T>(file);
}
}
catch (Exception e)
{
Debug.LogError("proto反序列化失败:"+ path + "," + e.Message);
throw;
}
return t;
}
/// <summary>
/// 类序列化成protobuf(bytes[]),用于网络
/// </summary>
/// <param name="obj">需要序列化的类</param>
/// <returns>是否成功</returns>
public static byte[] ProtoSerialize(System.Object obj)
{
try
{
using(MemoryStream ms = new MemoryStream())
{
ProtoBuf.Serializer.Serialize(ms, obj);
byte[] result = new byte[ms.Length];
ms.Position = 0;
ms.Read(result, 0, result.Length);
return result;
}
}
catch (Exception e)
{
Debug.LogError("类转protobuf(bytes[])失败:" + obj.GetType() + "," + e.Message);
}
return null;
}
/// <summary>
/// proto(byte[])反序列化,用于网络
/// </summary>
/// <param name="path">proto二进制文件路径</param>
public static T ProtoDeserialize<T>(byte[] msg) where T : class
{
T t = default(T);
try
{
using (MemoryStream ms = new MemoryStream())
{
ms.Write(msg, 0, msg.Length);
ms.Position = 0;
t = ProtoBuf.Serializer.Deserialize<T>(ms);
}
}
catch (Exception e)
{
Debug.LogError("proto(byte[])反序列化失败:" + e.Message);
throw;
}
return t;
}
//...
}

修改DataEditor脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
public class DataEditor
{
//...
public static string ProtobufPath = ResConfig.GetResFrameConfig().m_ProtobufPath;
//...
[MenuItem("Assets/Xml转Protobuf")]
public static void AssetsXmlToProtobuf()
{
UnityEngine.Object[] objs = Selection.objects;
for (int i = 0; i < objs.Length; i++)
{
EditorUtility.DisplayProgressBar("已经选中的xml转成protobuf", "正在扫描" + objs[i].name + "……", 1.0f / objs.Length * i);
XmlToProtobuf(objs[i].name);
}
AssetDatabase.Refresh();
EditorUtility.ClearProgressBar();
}
//...
[MenuItem("Tools/Xml/所有Xml转Protobuf")]
public static void AllXmlToProtobuf()
{
string xmlFullPath = Path.GetFullPath(XmlPath);
string[] filesPath = Directory.GetFiles(xmlFullPath, "*.*", SearchOption.AllDirectories);
for (int i = 0; i < filesPath.Length; i++)
{
EditorUtility.DisplayProgressBar("文件夹下所有的xml转二进制", filesPath[i], 1.0f / filesPath.Length * i);
if (filesPath[i].EndsWith(".xml"))//过滤掉.meta文件
{
string binaryName = Path.GetFileNameWithoutExtension(filesPath[i]);
XmlToProtobuf(binaryName);
}
}
AssetDatabase.Refresh();
EditorUtility.ClearProgressBar();
}
//...
private static void XmlToProtobuf(string name)
{
if (string.IsNullOrEmpty(name))
return;
try
{
Type type = null;
foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies())
{
Type curType = assembly.GetType(name);
if (curType != null)
{
type = curType;
break;
}
}
if (type != null)
{
string xmlPath = XmlPath + name + ".xml";
string protobufPath = ProtobufPath + name + ".bytes";
object obj = BinarySerializeOpt.XmlDeserialize(xmlPath, type);
BinarySerializeOpt.ProtoSerialize(protobufPath, obj);
Debug.Log(name + "xml转protobuf成功,protobuf路径为:" + protobufPath);
}
}
catch (Exception)
{

Debug.LogError(name + "xml转protobuf失败");
}
}
//...
}

ProtoBuf序列化准备

我们以MonsterData为例,打开MonsterData脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
using System.Collections.Generic;
using System.Xml.Serialization;

[ProtoBuf.ProtoContract]//++++++++++++++++++++
[System.Serializable]
public class MonsterData : ExcelBase
{
#if UNITY_EDITOR
/// <summary>
/// 编辑器下初始类转xml
/// </summary>
public override void Construction()
{
//...
}
#endif
/// <summary>
/// 数据初始化
/// </summary>
public override void Init()
{
m_AllMonsterDic.Clear();
foreach (MonsterBase monster in AllMonster)
{
if (m_AllMonsterDic.ContainsKey(monster.Id))
{
UnityEngine.Debug.LogError(monster.Name + " 有重复ID");
}
else
{
m_AllMonsterDic.Add(monster.Id, monster);
}
}
}
/// <summary>
/// 根据ID查找Monster数据
/// </summary>
public MonsterBase FindMonsterById(int id)
{
MonsterBase monster;
if (m_AllMonsterDic.TryGetValue(id, out monster))
{
return monster;
}
return null;
}
[XmlIgnore]
public Dictionary<int, MonsterBase> m_AllMonsterDic = new Dictionary<int, MonsterBase>();
[ProtoBuf.ProtoMember(1)]//++++++++++++++++++++
[XmlElement("AllMonster")]
public List<MonsterBase> AllMonster { get; set; }
}
[ProtoBuf.ProtoContract]//++++++++++++++++++++
[System.Serializable]
public class MonsterBase
{
[ProtoBuf.ProtoMember(1)]//++++++++++++++++++++
[XmlAttribute("Id")]
public int Id { get; set; }
[ProtoBuf.ProtoMember(2)]//++++++++++++++++++++
[XmlAttribute("Name")]
public string Name { get; set; }
//预制体路径,存储预制体全路径,因为在运行时拼接字符串会产生很多GC
[ProtoBuf.ProtoMember(3)]//++++++++++++++++++++
[XmlAttribute("OutLook")]
public string OutLook { get; set; }
[ProtoBuf.ProtoMember(4)]//++++++++++++++++++++
[XmlAttribute("Level")]
public int Level { get; set; }
[ProtoBuf.ProtoMember(5)]//++++++++++++++++++++
[XmlAttribute("Rarity")]
public int Rarity { get;set; }
[ProtoBuf.ProtoMember(6)]//++++++++++++++++++++
[XmlAttribute("Height")]
public float Height { get; set; }
[ProtoBuf.ProtoMember(7)]//++++++++++++++++++++
[XmlElement("AllString")]
public List<string> AllString { get; set; }
[ProtoBuf.ProtoMember(8)]//++++++++++++++++++++
[XmlElement("AllBuff")]
public List<BuffBase> AllBuff { get; set; }
}
[ProtoBuf.ProtoContract]//++++++++++++++++++++
[System.Serializable]
public class BuffBase
{
[ProtoBuf.ProtoMember(1)]//++++++++++++++++++++
[XmlAttribute("Id")]
public int Id { get; set; }
[ProtoBuf.ProtoMember(2)]//++++++++++++++++++++
[XmlAttribute("Name")]
public string Name { get; set; }
}

Protobuf在使用时,给需要序列化的类添加ProtoBuf.ProtoContract特性,给内部的变量添加ProtoBuf.ProtoMember(index)特性

关于index数字的要求:

  1. 必须是正整数,(为了更好的可移植性,这个数必须<=536870911,并且不在19000-19999范围内)
  2. 它们在单个类型中必须是唯一的,但如果启用继承,则相同的数字可以在子类型中重复使用
  3. 数字不得与任何继承Attribute中的数字冲突,(下面的Protobuf继承会讲到)
  4. 数字越小占用空间越少,尽量从1开始
  5. index数字是数据的唯一标记,我们可以随便修改变量属性的名字等。

测试

选中Assets——GameData——Data——Xml中的MonsterData.xml,然后“右键——Xml转Protobuf”

Protobuf继承

如果Protobuf需要序列化一个子类,需要在对应的基类设置ProtoInclude特性

基类在使用这个特性时,ProtoInclude(int,Type)数字参数不能与基类ProtoMember(int)里面的数字重复

举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[ProtoBuf.ProtoContract]
[ProtoBuf.ProtoInclude(5,typeof(FireBuff))]//+++++
[System.Serializable]
public class BuffBase
{
[ProtoBuf.ProtoMember(1)]
[XmlAttribute("Id")]
public int Id { get; set; }
[ProtoBuf.ProtoMember(2)]
[XmlAttribute("Name")]
public string Name { get; set; }
}
[ProtoBuf.ProtoContract]
[System.Serializable]
public class FireBuff : BuffBase
{
[ProtoBuf.ProtoMember(1)]
[XmlAttribute("Hit")]
public int Hit { get; set; }
}