当前位置: 首页 > 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;丰富二手物品交易管理经验能起到不少的促进作用…...

实战应用:基于快马ai为全栈项目快速构建集成wsl2开发环境

实战应用&#xff1a;基于快马AI为全栈项目快速构建集成WSL2开发环境 最近在准备一个全栈项目&#xff0c;需要同时开发Python Django后端和Vue.js前端。为了保持开发环境的一致性&#xff0c;我决定使用WSL2来搭建开发环境。下面记录下我的完整配置过程&#xff0c;希望能帮助…...

OpenClaw定时任务:Qwen3.5-9B每日自动抓取行业资讯

OpenClaw定时任务&#xff1a;Qwen3.5-9B每日自动抓取行业资讯 1. 为什么需要自动化资讯服务&#xff1f; 作为一个技术从业者&#xff0c;每天早晨打开电脑的第一件事就是查看行业动态。但手动浏览十几个网站、筛选重复内容、整理关键信息的过程实在太耗费时间。更糟糕的是&…...

【无标题】MySQL数据库基础实例教程单元2 学习笔记

2.1 关系数据库设计 2.1.1 数据的加工 数据设计本质上是对现实世界信息的逐步抽象和加工&#xff0c;过程分为三个阶段。首先是现实世界&#xff0c;包含客观存在的事物、业务需求和事物之间的联系。然后进入信息世界&#xff0c;把现实事物抽象为概念模型&#xff0c;方便理解…...

Qwen3.5-9B镜像安全加固:非root用户运行+端口绑定限制+HTTPS代理配置

Qwen3.5-9B镜像安全加固&#xff1a;非root用户运行端口绑定限制HTTPS代理配置 1. 项目概述 Qwen3.5-9B是一款拥有90亿参数的开源大语言模型&#xff0c;具备强大的逻辑推理、代码生成和多轮对话能力。该模型支持多模态理解&#xff08;图文输入&#xff09;和长上下文处理&a…...

ANIMATEDIFF PRO电商创新:WebAR商品试穿系统

ANIMATEDIFF PRO电商创新&#xff1a;WebAR商品试穿系统 最近跟几个做电商的朋友聊天&#xff0c;他们都在抱怨同一个问题&#xff1a;商品退货率太高了。尤其是服装鞋帽这类需要试穿的商品&#xff0c;用户光看图片和模特展示&#xff0c;根本拿不准自己穿上到底合不合适、好…...

数仓分层设计避坑指南:从ODS到ADS,我的团队踩过的5个典型雷区与优化方案

数仓分层设计避坑指南&#xff1a;从ODS到ADS&#xff0c;我的团队踩过的5个典型雷区与优化方案 三年前接手公司数据中台重构项目时&#xff0c;我们团队曾天真地认为数仓分层不过是教科书式的流程化操作。直到某次大促期间&#xff0c;凌晨三点被警报吵醒——ADS层报表查询超时…...

Qwen3-1.7B能做什么?实测写邮件、生成故事、智能聊天

Qwen3-1.7B能做什么&#xff1f;实测写邮件、生成故事、智能聊天 1. 认识Qwen3-1.7B Qwen3&#xff08;千问3&#xff09;是阿里巴巴集团开源的新一代通义千问大语言模型系列中的一员&#xff0c;1.7B版本虽然参数量不大&#xff0c;但在日常应用中表现出色。这个17亿参数的模…...

Qt图形界面开发集成AI:SmallThinker-3B-Preview实现智能桌面应用

Qt图形界面开发集成AI&#xff1a;SmallThinker-3B-Preview实现智能桌面应用 你是不是也想过&#xff0c;能不能把现在这些厉害的AI能力&#xff0c;直接塞进我们自己写的桌面软件里&#xff1f;比如&#xff0c;在写代码的时候&#xff0c;旁边就有一个能解释复杂代码片段的助…...

Python绘图进阶:掌握颜色代码与实战应用

1. Python绘图中的颜色表示方法全解析 第一次用Python画图时&#xff0c;我对着那一堆颜色参数完全摸不着头脑。为什么同样的红色可以用"red"、"(1,0,0)"、"#FF0000"这么多种方式表示&#xff1f;后来才发现&#xff0c;这些不同的颜色表示方法各…...

忍者像素绘卷参数详解:CFG值对‘火之意志’风格权重响应敏感度测试

忍者像素绘卷参数详解&#xff1a;CFG值对火之意志风格权重响应敏感度测试 1. 引言&#xff1a;像素艺术与AI的完美融合 忍者像素绘卷是一款基于Z-Image-Turbo深度优化的图像生成工具&#xff0c;它将传统忍者文化与16-Bit复古游戏美学相结合&#xff0c;创造出独特的视觉体验…...