当前位置: 首页 > news >正文

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

文章目录

  • 简介
    • 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文件

相关文章:

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

文章目录简介Protobuf 语法规则Proto Editor实现创建窗口定义类、字段增删类编辑字段导入、导出Json文件生成.proto文件生成.bat文件简介 在Socket网络编程中&#xff0c;假如使用Protobuf作为网络通信协议&#xff0c;需要了解Protobuf语法规则、编写.proto文件并通过编译指令…...

2022 OpenCV Spatial AI大赛前三名项目分享,开源、上手即用,优化了OAK智能双目相机的深度效果。

编辑&#xff1a;OAK中国 首发&#xff1a;oakchina.cn 喜欢的话&#xff0c;请多多&#x1f44d;⭐️✍ 内容可能会不定期更新&#xff0c;官网内容都是最新的&#xff0c;请查看首发地址链接。 ▌前言 Hello&#xff0c;大家好&#xff0c;这里是OAK中国&#xff0c;我是助手…...

Android 蓝牙开发——HCI log 分析(二十)

HCI log 是用来分析蓝牙设备之间的交互行为是否符合预期,是否符合蓝牙规范。对于蓝牙开发者来说,通过 HCI log 可以帮助我们更好地分析问题,理解蓝牙协议。 一、抓取HCI log 1、手机抓取HCI log 在开发者选项中打开启用蓝牙HCI信息收集日志开关,Android系统就开始自动地收…...

flask入门-4.项目实战

4. 项目实战1 1. 问答平台项目结构搭建 项目结构 config.py hostname "127.0.0.1" port 3306 username "root" password "root"database "flask_qa"# 在 app.config 中设置连接数据库的信息 SQLALCHEMY_DATABASE_URI f"…...

java 1(概要、变量与运算符)

java ——概要、变量与运算符 ✍作者&#xff1a;电子科大不知名程序员 &#x1f332;专栏&#xff1a;java学习指导 各位读者如果觉得博主写的不错&#xff0c;请诸位多多支持&#xff1b;如果有错误的地方&#xff0c;欢迎在评论区指出 目录java ——概要、变量与运算符命令行…...

​力扣解法汇总2363. 合并相似的物品

目录链接&#xff1a; 力扣编程题-解法汇总_分享记录-CSDN博客 GitHub同步刷题项目&#xff1a; https://github.com/September26/java-algorithms 原题链接&#xff1a;力扣 描述&#xff1a; 给你两个二维整数数组 items1 和 items2 &#xff0c;表示两个物品集合。每个数…...

2022年终总结-找回初心

和“那个夏天”群聊的几位死党聊完天后&#xff0c;发现自己已经忘了初心2年有余了&#xff0c;也是这次聊天让我重新燃起了要继续努力奋斗的想法。那就说一说2022年我过得如何吧。2022年过完春节刚来公司的几天就传来了一个好消息&#xff0c;我涨薪了。在没有涨薪之前私下有时…...

Allegro如何打开或者关闭DFA规则设置操作指导

Allegro如何打开或者关闭DFA规则设置操作指导 在用Allegro做PCB布局的时候,器件与器件之间的DFA规则可以避免器件出现装配问题。如下图 当DFA规则设置好之后,如何打开或者关闭规则,具体操作如下 点击Setup点击Constraints...

kind kubernetes 集群内如何通过 helm 部署定制化 Prometheus-Operator?

文章目录1. Prometheus 简介2. Prometheus 优势3. Prometheus 架构图4. Prometheus-Operator 简介5. Prometheus-Operator 架构图6. 环境准备7. Kind 部署 Kubernetes7.1 安装 Ingress-nginx 组件7.2 安装 Metric Server 组件8. helm 快速安装 Prometheus-Operator9. 定制 Prom…...

流媒体付服务器 ZLMediaKit 学习记录

1.官方github&#xff1a;ZLMediaKit 依赖于 media-server 库 #国内用户推荐从同步镜像网站gitee下载 git clone --depth 1 https://gitee.com/xia-chu/ZLMediaKit cd ZLMediaKit #千万不要忘记执行这句命令 git submodule update --init 之后 cd ZLMediaKit mkdir build…...

2023年了还不会写软件测试简历吗,那就来看这里吧,怎么样才能更容易让HR看到你的简历

作为软件测试的从业者&#xff0c;面试或者被面试都是常有的事。 可是不管怎样&#xff0c;和简历有着理不清的关系&#xff0c;面试官要通过简历了解面试者的基本信息、过往经历等。 面试者希望通过简历把自己最好的一面体现给面试官&#xff0c;所以在这场博弈中&#xff0…...

第四阶段08-基于element-ui的vue2.0脚手架(续)

42. VUE脚手架项目嵌套路由 在配置路由&#xff08;配置/src/router/index.js&#xff09;时&#xff0c;如果配置的路由对象是routes常量的直接数组元素&#xff0c;则此路由配置的视图会显示在App.vue的<router-view/>中。 在设计视图时&#xff0c;可能会出现<ro…...

数据库设计规范

三范式首先&#xff0c;设计数据库&#xff0c;要尽可能的满足三范式&#xff0c;遵循三范式开发会减少数据冗余、提升系统可扩展性和查询性能。第一范式的目标是确保每列的原子性如果每列都是不可再分的最小数据单元&#xff08;也称为最小的原子单元&#xff09;&#xff0c;…...

深入浅出PaddlePaddle函数——paddle.Tensor

分类目录&#xff1a;《深入浅出PaddlePaddle函数》总目录 Tensor是Paddle中最为基础的数据结构&#xff0c;有几种创建Tensor的不同方式&#xff1a; 用预先存在的数据创建1个Tensor&#xff0c;请参考paddle.to_tensor创建一个指定shape的Tensor&#xff0c;请参考paddle.on…...

docker删除已停止的容器

一、docker删除已停止的容器 1、根据容器的状态&#xff0c;删除Exited状态的容器 先停止容器、再删除镜像中的容器、最后删除none的镜像。执行命令如下&#xff1a; docker stop $(docker ps -a | grep "Exited" | awk {print $1 }) #停止容器 docker rm $(docke…...

JS#1 引入方式和基础语法

JavaScript(JS)是一门跨平台, 面向对象的脚本语言, 来控制网页行为的, 它能够是网页可交互一. 引入方式内部脚本与外部脚本内部脚本: 将JS代码定义在HTML页面中外部脚本: 将JS代码定义在外部JS文件中, 然后引入到HTML页面中注意: 在HTML中,JS代码必须位于<script></sc…...

面了一个测试工程师,明显感觉他背了很多面试题...

最近有朋友去字节面试&#xff0c;面试前后进行了20天左右&#xff0c;包含4轮电话面试、1轮笔试、1轮主管视频面试、1轮hr视频面试。 据他所说&#xff0c;80%的人都会栽在第一轮面试&#xff0c;要不是他面试前做足准备&#xff0c;估计都坚持不完后面几轮面试。 其实&…...

C#生成缩略图

using System;using System.Collections.Generic;using System.Drawing;using System.Drawing.Drawing2D;using System.Drawing.Imaging;using System.Text;namespace learun.util{public enum ThumbnailMode{/// <summary>/// 指定宽度&#xff0c;高度按照比例缩放/// …...

算法 # SimHash 算法:文本相似度、文本去重、海量文本快速查询

SimHash SimHash 是 Google 发明的海量网页去重的高效算法,将原始的文本映射为 64 位的二进制串,然后通过比较二进制的差异进而表示原始文本内容的差异。 传统的 Hash 算法只负责将原始内容尽量均匀随机地映射为一个 hash 值,原理上相当于伪随机数产生算法。SimHash 本身属…...

Java程序设计-JSP程序设计-SSM校园二手交易系统

摘 要 网络的广泛应用给生活带来了十分的便利。所以把二手物品交易管理与现在网络相结合&#xff0c;利用java技术建设二手物品交易系统&#xff0c;实现二手物品交易的信息化。则对于进一步提高二手物品交易管理发展&#xff0c;丰富二手物品交易管理经验能起到不少的促进作用…...

Windows Defender终极移除指南:高效释放系统资源的13项完整方案

Windows Defender终极移除指南&#xff1a;高效释放系统资源的13项完整方案 【免费下载链接】windows-defender-remover A tool which is uses to remove Windows Defender in Windows 8.x, Windows 10 (every version) and Windows 11. 项目地址: https://gitcode.com/gh_mi…...

YOLOv8头部改进全攻略:从SEAM到MultiSEAM的代码实现与效果对比

YOLOv8头部改进全攻略&#xff1a;从SEAM到MultiSEAM的代码实现与效果对比 在目标检测领域&#xff0c;YOLO系列模型因其卓越的实时性能而广受欢迎。YOLOv8作为最新一代的代表&#xff0c;其头部结构的设计直接影响着检测精度与速度。本文将深入探讨两种创新性头部改进方案——…...

五年磨剑与二十年深耕:5 年与 20 年程序员的差距,远不止代码本身

在信息技术飞速迭代的今天&#xff0c;程序员这一职业始终站在时代前沿。有人说&#xff0c;程序员是吃 “青春饭” 的行业&#xff0c;年轻意味着精力充沛、学习速度快、能熬夜加班&#xff1b;也有人说&#xff0c;真正的技术高手&#xff0c;往往藏在十几年甚至二十余年的行…...

Omni-Vision Sanctuary 赋能前端设计:自动生成 UI 组件与视觉素材实战

Omni-Vision Sanctuary 赋能前端设计&#xff1a;自动生成 UI 组件与视觉素材实战 1. 前端设计的效率困境 每天早上9点&#xff0c;前端开发团队的设计评审会总是充满火药味。"这个按钮样式还需要再调整一下"、"图标风格不太统一"、"背景图看起来不…...

从零开始:手把手教你用Python脚本创建第一个USD场景(附完整代码)

从零构建USD场景&#xff1a;Python实战指南与核心技巧解析 1. USD技术体系与开发环境搭建 Universal Scene Description&#xff08;USD&#xff09;作为Pixar推出的开源3D场景描述框架&#xff0c;正在重塑数字内容创作流程。这套技术最初为应对《勇敢传说》中复杂的场景管理…...

UE5 Android打包环境配置避坑指南:从4.26到5.6的版本适配与Pico部署

1. 环境准备&#xff1a;从零搭建UE5 Android开发环境 第一次用UE5给Pico设备打包Android应用时&#xff0c;我对着报错提示折腾了整整三天。后来才发现&#xff0c;问题出在环境配置的版本兼容性上。不同版本的UE5对JDK、SDK、NDK的要求差异很大&#xff0c;特别是从4.26升级…...

华为认证HCIA入门指南:网络工程师的第一课

1. 华为认证体系全解析&#xff1a;从HCIA到HCIE的进阶之路 第一次接触华为认证的朋友可能会被HCIA、HCIP、HCIE这一串缩写搞晕。简单来说&#xff0c;这就像网络工程师的"小学、中学、大学"三级成长体系。我当年考HCIA时也花了不少时间才理清这些概念&#xff0c;现…...

R 4.5中DESeq2用于微生物组?:权威验证——3篇Nature Microbiology复现实验揭示其在低丰度菌群中的FDR失控风险

第一章&#xff1a;R 4.5中DESeq2用于微生物组分析的范式跃迁R 4.5版本对S4对象系统、并行计算支持及Bioconductor 3.19生态的深度整合&#xff0c;显著重塑了DESeq2在微生物组研究中的应用逻辑。传统上依赖OTU表与稀疏归一化&#xff08;如CSS&#xff09;的流程&#xff0c;正…...

Midscene.js:用自然语言重新定义跨平台UI自动化测试

Midscene.js&#xff1a;用自然语言重新定义跨平台UI自动化测试 【免费下载链接】midscene AI-powered, vision-driven UI automation for every platform. 项目地址: https://gitcode.com/GitHub_Trending/mid/midscene 想象一下&#xff0c;你只需要用自然语言描述&qu…...

WebPShop终极指南:让你的Photoshop完美支持WebP格式

WebPShop终极指南&#xff1a;让你的Photoshop完美支持WebP格式 【免费下载链接】WebPShop Photoshop plug-in for opening and saving WebP images 项目地址: https://gitcode.com/gh_mirrors/we/WebPShop 还在为Photoshop无法原生支持WebP格式而烦恼吗&#xff1f;&am…...