ProtoEditor - 如何在Unity中实现一个Protobuf通信协议类编辑器

news2024/11/25 23:21:23

文章目录

  • 简介
    • Protobuf 语法规则
    • Proto Editor
  • 实现
    • 创建窗口
    • 定义类、字段
    • 增删类
    • 编辑字段
    • 导入、导出Json文件
    • 生成.proto文件
    • 生成.bat文件


简介

Socket网络编程中,假如使用Protobuf作为网络通信协议,需要了解Protobuf语法规则、编写.proto文件并通过编译指令将.proto文件转化为.cs脚本文件,本文介绍如何在Unity中实现一个编辑器工具来使开发人员不再需要关注这些语法规则、编译指令,以及更便捷的编辑和修改.proto文件内容。工具已上传至SKFramework框架Package Manager中:

SKFramework PackageManager

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 TypeC# TypeNotes
doubledouble
floatfloat
int32intUses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint32 instead.
int64longUses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint64 instead.
uint32uintUses variable-length encoding.
uint64ulongUses variable-length encoding.
sint32intUses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int32s.
sint64longUses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int64s.
fixed32uintAlways four bytes. More efficient than uint32 if values are often greater than 228.
fixed64ulongAlways eight bytes. More efficient than uint64 if values are often greater than 256.
sfixed32intAlways four bytes.
sfixed64longAlways eight bytes.
boolbool
stringstringA string must always contain UTF-8 encoded or 7-bit ASCII text, and cannot be longer than 232.
bytesByteStringMay contain any arbitrary sequence of bytes no longer than 232.
  • 标识号:示例中的1-8表示每个字段的标识号,并不是赋值。

每个字段都有唯一的标识号,这些标识符是用来在消息的二进制格式中识别各个字段的。[1,15]之内的标识号在编码的时候会占用一个字节。[16,2047]之内的标识号则占用2个字节。所以应该为那些频繁出现的消息元素保留[1,15]之内的标识号。:要为将来有可能添加的、频繁出现的标识号预留一些标识号,不可以使用其中的[19000-19999]标识号,Protobuf协议实现中对这些进行了预留。

Proto Editor

Proto Editor

如图所示,工具包含以下功能:

  • New、Clear Message:增加、删除message类;

New、Clear Message

  • 增加、删除、编辑fields字段(修饰符、类型、命名、分配标识号);

增删字段

  • Import、Export Json File:导入、导出json文件(假如要修改一个已有的通信协议类,导入之前导出的Json文件再次编辑即可);

Import Json File

  • Generate Proto File:生成.proto文件;
  • Create .bat:生成.bat文件(不再需要手动编辑编译指令)。

生成的.proto & .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];
                    //缩进
                    sb.Append("    ");
                    //修饰符
                    sb.Append(field.modifier.ToString().ToLower());
                    //空格
                    sb.Append(" ");
                    //如果是自定义类型 拼接typeName 
                    switch (field.type)
                    {
                        case FieldsType.Int: sb.Append("int32"); break;
                        case FieldsType.Long: sb.Append("int64"); break;
                        case FieldsType.Custom: sb.Append(field.typeName); break;
                        default: sb.Append(field.type.ToString().ToLower()); break;
                    }
                    //空格
                    sb.Append(" ");
                    //字段名
                    sb.Append(field.name);
                    //等号
                    sb.Append(" = ");
                    //标识号
                    sb.Append(field.flag);
                    //分号及换行符
                    sb.Append(";\r\n");
                }
                sb.Append("}\r\n");
                protoContent.Append(sb.ToString());
            }
            //写入文件
            File.WriteAllText(protoFilePath, protoContent.ToString());
            //刷新(假设路径在工程内 可以避免手动刷新才看到)
            AssetDatabase.Refresh();
            //打开该文件夹
            FileInfo fileInfo = new FileInfo(protoFilePath);
            Process.Start(fileInfo.Directory.FullName);
        }
    }
}

生成.bat文件

  • 使用OpenFolderPanel打开protogen.exe文件所在的文件夹,.bat文件需要生成在该文件夹下:

protogen.exe

  • 获取proto文件夹下的所有.proto文件的名称,拼接编译指令:
//创建.bat文件
if (GUILayout.Button("Create .bat"))
{
    //选择路径(protogen.exe所在的文件夹路径)
    string rootPath = EditorUtility.OpenFolderPanel("Create .bat file(protogen.exe所在的文件夹)", Application.dataPath, string.Empty);
    //取消
    if (string.IsNullOrEmpty(rootPath)) return;
    //protogen.exe文件路径
    string protogenPath = rootPath + "/protogen.exe";
    //不是protogen.exe所在的文件夹路径
    if (!File.Exists(protogenPath))
    {
        EditorUtility.DisplayDialog("Error", "请选择protogen.exe所在的文件夹路径", "ok");
    }
    else
    {
        string protoPath = rootPath + "/proto";
        DirectoryInfo di = new DirectoryInfo(protoPath);
        //获取所有.proto文件信息
        FileInfo[] protos = di.GetFiles("*.proto");
        //使用StringBuilder拼接字符串
        StringBuilder sb = new StringBuilder();
        //遍历
        for (int i = 0; i < protos.Length; i++)
        {
            string proto = protos[i].Name;
            //拼接编译指令
            sb.Append(rootPath + @"/protogen.exe -i:proto\" + proto + @" -o:cs\" + proto.Replace(".proto", ".cs") + "\r\n");
        }
        sb.Append("pause");

        //生成".bat文件"
        string batPath = $"{rootPath}/run.bat";
        File.WriteAllText(batPath, sb.ToString());
        //打开该文件夹
        Process.Start(rootPath);
    }
}

最终运行.bat文件,就可以将.proto文件转化为.cs脚本文件:

运行.bat文件

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/377565.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【编程思想】计算机领域的所有问题都可以通过增加一个间接的中间层来解决

文章目录计算机领域的所有问题都可以通过增加一个间接的中间层来解决一、间接中间层可以解决计算机领域的问题二、操作系统如何通过间接中间层解决计算机问题三 结论七层网络协议中体现的分层思想概述物理层数据链路层网络层传输层会话层表示层应用层代码实例计算机存储体系设计…

回归预测 | MATLAB实现GRU(门控循环单元)多输入单输出(多指标评价)

回归预测 | MATLAB实现GRU(门控循环单元)多输入单输出(多指标评价) 文章目录 回归预测 | MATLAB实现GRU(门控循环单元)多输入单输出(多指标评价)预测效果基本介绍程序设计参考资料预测效果 基本介绍 GRU神经网络是LST

git学习记录/菜鸟教程(基于Gitcode)

首先说明下为何使用Gitcode而不是hub或lab&#xff1a;只是因为国外的网站访问太慢了&#xff0c;而且还要翻译从初次使用开始说&#xff1a;首先安装Git&#xff0c;一路next就可以&#xff0c;安装好后打开&#xff0c;输入git version如果有显示版本号&#xff0c;说明安装成…

操作系统——9.进程控制

这篇文章&#xff0c;我们主要来讲一下操作系统中的进程控制功能 目录 1.概述 2.进程控制的定义 3.进程控制的实现 4.进程控制相关的原语 ​编辑​编辑​编辑​编辑5. 小结 1.概述 首先&#xff0c;我们一起来看一下这篇文章内容的大体框架&#xff1a; 2.进程控制的定…

MongoDB 类replace替换字符串指定内容

目录 需求介绍 技术分析 技术积累 1、replaceOne 语法 2、javascript语法 实战演示 1、查询满足条件的数据 2、在mongodb语法中融入javascript语法并执行 3、查看刚刚被修改的数据 需求介绍 根据业务发展&#xff0c;现在需要对已经存在的数据进行处理&#xff0c;需…

在线安装ESP32和ESP8266 Arduino开发环境

esp32和esp8266都是乐鑫科技开发的单片机产品&#xff0c;esp8266价格便宜开发板只需要十多块钱就可以买到&#xff0c;而esp32是esp8266的升级版本&#xff0c;比esp8266的功能和性能更强大&#xff0c;开发板价格大约二十多元就可以买到。 使用Arduino开发esp32和esp8266需要…

并发replace操作导致的死锁问题

背景 批量对一张表进行replace into操作&#xff0c;每个SQL操作1000条数据&#xff0c;最近有同事反馈使用并发replace操作的时候&#xff0c;遇到了死锁的问题。针对这个问题&#xff0c;我看了看表的结构&#xff0c;发现表中有一个主键&#xff0c;一个唯一索引&#xff0c…

【深度学习环境】Docker

1. Docker 相关安装配置 1.1 docker 安装 参考&#xff1a;https://www.runoob.com/docker/ubuntu-docker-install.html 1.2 nvidia-docker 安装 参考&#xff1a;https://zhuanlan.zhihu.com/p/37519492 1.3 代理加速 参考&#xff1a;https://yeasy.gitbook.io/docker_…

Java学习之路001——基础语法以及IDEA的基础使用

【说明】以下内容&#xff0c;选取于网上搜索进行的排版。如有冲突&#xff0c;请联系作者删除。 一、第一个Hello World程序 1.1 开发工具介绍 eclipse IntelliJ IDEA 1.2 案例开发步骤 首先定义一个类class 类名 在类定义后加上一对大括号{} 在大括号中间添加一个主(ma…

别克GL8改装完工,一起来看看效果

①豪华商务头等舱 别克GL8作为商务车&#xff0c;不管是家用还是商务接待&#xff0c;原车内饰都太掉档次了&#xff0c;所以车主要求全部换掉。>>织布座椅换成航空座椅 主副驾&#xff1a;改装纳帕皮 中排&#xff1a;改装水晶宝座豪华版航空座椅&#xff0c;带通风、加…

Dataway 让 Spring Boot 不再需要 Controller、Service、DAO、Mapper 简单接口直接开发。

新的sql语法可以先看一下官网&#xff0c;部署起来之后会用到Dataql&#xff1a; DataQL - 数据查询语言https://www.dataql.net/先看一下效果 接下来来实现一下。 1 创建spring boot项目 导入依赖 <!--begin dataWay--><!--hasor-spring 负责 Spring 和 Hasor 框架之…

【操作系统】进程管理

进程与线程 1. 进程 进程是资源分配的基本单位 进程控制块 (Process Control Block, PCB) 描述进程的基本信息和运行状态&#xff0c;所谓的创建进程和撤销进程&#xff0c;都是指对 PCB 的操作。 下图显示了 4 个程序创建了 4 个进程&#xff0c;这 4 个进程可以并发地执行…

实战|掌握Linux内存监视:free命令详解与使用技巧

文章目录前言一. free命令介绍二. 语法格式及常用选项三. 参考案例3.1 查看free相关的信息3.2 以MB的形式显示内存的使用情况3.3 以总和的形式显示内存的使用情况3.4 周期性的查询内存的使用情况3.5 以更人性化的形式来查看内存的结果输出四. free在脚本中的应用总结前言 大家…

LAMP项目部署实战2

部署Discuz!论坛 一、Discuz&#xff01;论坛概述&#xff1a; 1&#xff09;Discuz&#xff01;论坛是基于phpmysql进行开发的一套开源的论坛系统。 2&#xff09;下载源代码&#xff1a; 下载地址&#xff1a;码云DiscuzX: Discuz! X 官方 Git&#xff0c;简体中文 UTF8 版…

2023年,35岁测试工程师只能被“优化裁员”吗?肯定不是····

国内的互联网行业发展较快&#xff0c;所以造成了技术研发类员工工作强度比较大&#xff0c;同时技术的快速更新又需要员工不断的学习新的技术。因此淘汰率也比较高&#xff0c;超过35岁的基层研发类员工&#xff0c;往往因为家庭原因、身体原因&#xff0c;比较难以跟得上工作…

mongo数据备份

目录1. mongo单机安装2. mongo(replica set)部署3. mongodump 与 mongorestore工具使用4.rsync工具使用服务端配置客户端配置客户端推送与拉取文件5. 完整mongo全量备份脚本恢复全量备份数据6. 完整mongo增量备份脚本(基于oplog)恢复增量备份数据7.备份策略1. mongo单机安装 m…

高数:极限的定义

目录 极限的定义&#xff1a; 数列极限的几何意义&#xff1a; 由极限的定义得出的极限的两个结论&#xff1a; ​编辑 极限的第三个结论&#xff1a; 例题 方法1&#xff1a; ​编辑 方法2&#xff1a; ​编辑 方法3&#xff1a; ​编辑 极限的定义&#xff1a; 如何理…

JDK8常用新特性的原理与代码演示

Lambda Lambda 表达式&#xff0c;也可称为闭包&#xff0c;Lambda 允许把函数作为一个方法的参数。 格式 (参数列表) -> {代码块} (parameters) -> expression 或 (parameters) ->{ statements; }前置条件 lambda表达式是一段执行某种功能的代码块&#xff0c;需要…

数据结构与算法——4时间复杂度分析2(常见的大O阶)

这篇文章是时间复杂度分析的第二篇。在前一篇文章中&#xff0c;我们从0推导出了为什么要用时间复杂度&#xff0c;时间复杂度如何分析以及时间复杂度的表示三部分内容。这篇文章&#xff0c;是对一些常用的时间复杂度进行一个总结&#xff0c;相当于是一个小结论 1.常见的大O…

ESFP型人格的特征,ESFP型人格的优势和劣势分析

ESFP型人格的特征ESFP&#xff08;表演者型人格&#xff09;是人群中的开心果。他们外向&#xff0c;友善&#xff0c;包容&#xff0c;有他们在的地方总是充满着活泼的氛围。ESFP对于新的朋友&#xff0c;新的环境适应良好&#xff0c;他们是完完全全的社交动物&#xff0c;对…