ProtoEditor - 如何在Unity中实现一个Protobuf通信协议类编辑器
Posted CoderZ1010
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ProtoEditor - 如何在Unity中实现一个Protobuf通信协议类编辑器相关的知识,希望对你有一定的参考价值。
文章目录
简介
在Socket
网络编程中,假如使用Protobuf
作为网络通信协议,需要了解Protobuf
语法规则、编写.proto
文件并通过编译指令将.proto
文件转化为.cs
脚本文件,本文介绍如何在Unity中实现一个编辑器工具来使开发人员不再需要关注这些语法规则、编译指令,以及更便捷的编辑和修改.proto
文件内容。工具已上传至SKFramework
框架Package Manager
中:
Protobuf 语法规则
在介绍工具之前先简单介绍protobuf的语法规则,以便更好的理解工具的作用,下面是一个proto文件的示例:
message AvatarProperty
required string userId = 1;
required float posX = 2;
required float posY = 3;
required float posZ = 4;
required float rotX = 5;
required float rotY = 6;
required float rotZ = 7;
required float speed = 8;
- 类通过
message
来声明,后面是类的命名 - 字段修饰符包含三种类型:
- required : 不可增加或删除的字段,必须初始化
- optional : 可选字段,可删除,可以不初始化
- repeated : 可重复字段(对应C#里面的List)
- 与C#的字段类型对应关系如下,查阅自官网
.proto Type | C# Type | Notes |
---|---|---|
double | double | |
float | float | |
int32 | int | Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint32 instead. |
int64 | long | Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint64 instead. |
uint32 | uint | Uses variable-length encoding. |
uint64 | ulong | Uses variable-length encoding. |
sint32 | int | Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int32s. |
sint64 | long | Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int64s. |
fixed32 | uint | Always four bytes. More efficient than uint32 if values are often greater than 228. |
fixed64 | ulong | Always eight bytes. More efficient than uint64 if values are often greater than 256. |
sfixed32 | int | Always four bytes. |
sfixed64 | long | Always eight bytes. |
bool | bool | |
string | string | A string must always contain UTF-8 encoded or 7-bit ASCII text, and cannot be longer than 232. |
bytes | ByteString | May contain any arbitrary sequence of bytes no longer than 232. |
- 标识号:示例中的1-8表示每个字段的标识号,并不是赋值。
每个字段都有唯一的标识号,这些标识符是用来在消息的二进制格式中识别各个字段的。[1,15]之内的标识号在编码的时候会占用一个字节。[16,2047]之内的标识号则占用2个字节。所以应该为那些频繁出现的消息元素保留[1,15]之内的标识号。注:要为将来有可能添加的、频繁出现的标识号预留一些标识号,不可以使用其中的[19000-19999]标识号,Protobuf协议实现中对这些进行了预留。
Proto Editor
如图所示,工具包含以下功能:
New、Clear Message
:增加、删除message
类;
- 增加、删除、编辑
fields
字段(修饰符、类型、命名、分配标识号);
Import、Export Json File
:导入、导出json
文件(假如要修改一个已有的通信协议类,导入之前导出的Json文件再次编辑即可);
Generate Proto File
:生成.proto
文件;Create .bat
:生成.bat文件
(不再需要手动编辑编译指令)。
实现
创建窗口
- 继承
Editor Window
编辑器窗口类; Menu Item
添加打开窗口的菜单;
public class ProtoEditor : EditorWindow
[MenuItem("Multiplayer/Proto Editor")]
public static void Open()
GetWindow<ProtoEditor>("Proto Editor").Show();
定义类、字段
/// <summary>
/// 类
/// </summary>
public class Message
/// <summary>
/// 类名
/// </summary>
public string name = "New Message";
/// <summary>
/// 所有字段
/// </summary>
public List<Fields> fieldsList = new List<Fields>(0);
/// <summary>
/// 字段
/// </summary>
public class Fields
public ModifierType modifier;
public FieldsType type;
public string typeName;
public string name;
public int flag;
Modifer Type
:修饰符类型
/// <summary>
/// 修饰符类型
/// </summary>
public enum ModifierType
/// <summary>
/// 必需字段
/// </summary>
Required,
/// <summary>
/// 可选字段
/// </summary>
Optional,
/// <summary>
/// 可重复字段
/// </summary>
Repeated
Fields Type
:字段类型
这里只定义了我常用的几种类型,Custom用于自定义类型:
/// <summary>
/// 字段类型
/// </summary>
public enum FieldsType
Double,
Float,
Int,
Long,
Bool,
String,
Custom,
增删类
- 声明一个列表存储所有类
//存储所有类
private List<Message> messages = new List<Message>();
- 声明一个字典用于存储折叠栏状态(每个类可折叠查看)
//字段存储折叠状态
private readonly Dictionary<Message, bool> foldoutDic = new Dictionary<Message, bool>();
- 插入、删除
//滚动视图
scroll = GUILayout.BeginScrollView(scroll);
for (int i = 0; i < messages.Count; i++)
var message = messages[i];
GUILayout.BeginHorizontal();
foldoutDic[message] = EditorGUILayout.Foldout(foldoutDic[message], message.name, true);
//插入新类
if (GUILayout.Button("+", GUILayout.Width(20f)))
Message insertMessage = new Message();
messages.Insert(i + 1, insertMessage);
foldoutDic.Add(insertMessage, true);
Repaint();
return;
//删除该类
if (GUILayout.Button("-", GUILayout.Width(20f)))
messages.Remove(message);
foldoutDic.Remove(message);
Repaint();
return;
GUILayout.EndHorizontal();
GUILayout.EndScrollView();
- 底部新增、清空菜单:
GUILayout.BeginHorizontal();
//创建新的类
if (GUILayout.Button("New Message"))
Message message = new Message();
messages.Add(message);
foldoutDic.Add(message, true);
//清空所有类
if (GUILayout.Button("Clear Messages"))
//确认弹窗
if (EditorUtility.DisplayDialog("Confirm", "是否确认清空所有类型?", "确认", "取消"))
//清空
messages.Clear();
foldoutDic.Clear();
//重新绘制
Repaint();
GUILayout.EndHorizontal();
编辑字段
- 折叠栏为打开状态时,绘制该类具体的字段:
//如果折叠栏为打开状态 绘制具体字段内容
if (foldoutDic[message])
//编辑类名
message.name = EditorGUILayout.TextField("Name", message.name);
//字段数量为0 提供按钮创建
if (message.fieldsList.Count == 0)
if (GUILayout.Button("New Field"))
message.fieldsList.Add(new Fields(1));
else
for (int j = 0; j < message.fieldsList.Count; j++)
var item = message.fieldsList[j];
GUILayout.BeginHorizontal();
//修饰符类型
item.modifier = (ModifierType)EditorGUILayout.EnumPopup(item.modifier);
//字段类型
item.type = (FieldsType)EditorGUILayout.EnumPopup(item.type);
if (item.type == FieldsType.Custom)
item.typeName = GUILayout.TextField(item.typeName);
//编辑字段名
item.name = EditorGUILayout.TextField(item.name);
GUILayout.Label("=", GUILayout.Width(15f));
//分配标识号
item.flag = EditorGUILayout.IntField(item.flag, GUILayout.Width(50f));
//插入新字段
if (GUILayout.Button("+", GUILayout.Width(20f)))
message.fieldsList.Insert(j + 1, new Fields(message.fieldsList.Count + 1));
Repaint();
return;
//删除该字段
if (GUILayout.Button("-", GUILayout.Width(20f)))
message.fieldsList.Remove(item);
Repaint();
return;
GUILayout.EndHorizontal();
导入、导出Json文件
- 导出Json文件以及生成Proto文件之前都需要判断当前的编辑是否有效,从以下几个方面判断:
proto file name
:文件名编辑是否输入为空;message name
:类名编辑是否输入为空;- 自定义字段类型时,是否输入为空;
- 标识号是否唯一 。
为Message、Fields类添加有效性判断函数:
/// <summary>
/// 类
/// </summary>
public class Message
/// <summary>
/// 类名
/// </summary>
public string name = "New Message";
/// <summary>
/// 所有字段
/// </summary>
public List<Fields> fieldsList = new List<Fields>(0);
public bool IsValid()
bool flag = !string.IsNullOrEmpty(name);
for (int i = 0; i < fieldsList.Count; i++)
flag &= fieldsList[i].IsValid();
if (!flag) return false;
for (int j = 0; j < fieldsList.Count; j++)
if (i != j)
flag &= fieldsList[i].flag != fieldsList[j].flag;
if (!flag) return false;
return flag;
/// <summary>
/// 字段
/// </summary>
public class Fields
public ModifierType modifier;
public FieldsType type;
public string typeName;
public string name;
public int flag;
public Fields()
public Fields(int flag)
modifier = ModifierType.Required;
type = FieldsType.String;
name = "FieldsName";
typeName = "FieldsType";
this.flag = flag;
public bool IsValid()
return type != FieldsType.Custom || (type == FieldsType.Custom && !string.IsNullOrEmpty(typeName));
- 最终编辑有效性判断:
//编辑的内容是否有效
private bool ContentIsValid()
bool flag = !string.IsNullOrEmpty(fileName);
flag &= messages.Count > 0;
for (int i = 0; i < messages.Count; i++)
flag &= messages[i].IsValid();
if (!flag) break;
return flag;
- 导入、导出Json:
GUILayout.BeginHorizontal();
//导出Json
if (GUILayout.Button("Export Json File"))
if (!ContentIsValid())
EditorUtility.DisplayDialog("Error", "请按以下内容逐项检查:\\r\\n1.proto File Name是否为空\\r\\n2.message类名是否为空\\r\\n" +
"3.字段类型为自定义时 是否填写了类型名称\\r\\n4.标识号是否唯一", "ok");
else
//文件夹路径
string dirPath = Application.dataPath + workspacePath;
//文件夹不存在则创建
if (!Directory.Exists(dirPath))
Directory.CreateDirectory(dirPath);
//json文件路径
string filePath = dirPath + "/" + fileName + ".json";
if (EditorUtility.DisplayDialog("Confirm", "是否保存当前编辑内容到" + filePath, "确认", "取消"))
//序列化
string json = JsonMapper.ToJson(messages);
//写入
File.WriteAllText(filePath, json);
//刷新
AssetDatabase.Refresh();
//导入Json
if (GUILayout.Button("Import Json File"))
//选择json文件路径
string filePath = EditorUtility.OpenFilePanel("Import Json File", Application.dataPath + workspacePath, "json");
//判断路径有效性
if (File.Exists(filePath))
//读取json内容
string json = File.ReadAllText(filePath);
//清空
messages.Clear();
foldoutDic.Clear();
//反序列化
messages = JsonMapper.ToObject<List<Message>>(json);
//填充字典
for (int i = 0; i < messages.Count; i++)
foldoutDic.Add(messages[i], true);
//文件名称
FileInfo fileInfo = new FileInfo(filePath);
fileName = fileInfo.Name.Replace(".json", "");
//重新绘制
Repaint();
return;
GUILayout.EndHorizontal();
生成.proto文件
主要是字符串拼接工作:
//生成proto文件
if (GUILayout.Button("Generate Proto File"))
if (!ContentIsValid())
EditorUtility.DisplayDialog("Error", "请按以下内容逐项检查:\\r\\n1.proto File Name是否为空\\r\\n2.message类名是否为空\\r\\n" +
"3.字段类型为自定义时 是否填写了类型名称\\r\\n4.标识号是否唯一", "ok");
else
string protoFilePath = EditorUtility.SaveFilePanel("Generate Proto File", Application.dataPath, fileName, "proto");
if (!string.IsNullOrEmpty(protoFilePath))
StringBuilder protoContent = new StringBuilder();
for (int i = 0; i < messages.Count; i++)
var message = messages[i];
StringBuilder sb = new StringBuilder();
sb.Append("message " + message.name + "\\r\\n" + "\\r\\n");
for (int n = 0; n < message.fieldsList.Count; n++)
var field = message.fieldsList[n]
public TrailRenderer m_CollectTrail = null;
#if UNITY_EDITOR
if (Input.GetMouseButtonDown(0))
{
if (!m_PointDown)
{
m_PointDown = true;
m_CollectTrail.Clear();
m_CollectTrail.gameObject.SetActive(true);
}
}
else if (Input.GetMouseButtonUp(0))
{
if (m_PointDown)
{
m_PointDown = false;
m_CollectTrail.gameObject.SetActive(false);
}
}
#else
if (Input.touchCount > 0)
{
if (!m_CollectTrail.gameObject.activeSelf)
{
m_CollectTrail.Clear();
m_CollectTrail.gameObject.SetActive(true);
}
}
else
{
if (m_CollectTrail.gameObject.activeSelf)
{
m_CollectTrail.gameObject.SetActive(false);
}
}
#endif
// Update trail position.
if (m_CollectTrail.gameObject.activeSelf)
{
var screenPos = (Application.isMobilePlatform && (Input.touchCount > 0)) ? (Vector3)(Input.GetTouch(0).position) : Input.mousePosition;
var worldPos = Camera.main.ScreenToWorldPoint(screenPos);
worldPos.z = 0.0f;
m_CollectTrail.transform.position = worldPos;
}
}