From be26a78838c047e6d8862c69628d47a3578669b5 Mon Sep 17 00:00:00 2001 From: Shiliang Date: Wed, 27 May 2026 15:56:18 +0800 Subject: [PATCH] =?UTF-8?q?feat(tools):=20=E6=B7=BB=E5=8A=A0UI=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E8=84=9A=E6=9C=AC=E5=8F=8A=E9=83=A8=E7=BD=B2=E6=96=87?= =?UTF-8?q?=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增内容: 1. 创建 06_工具脚本/UI工具/ 目录,包含4个核心工具脚本 - ExternalCommandListener.cs - 外部命令监听器 - RemoteCommand.cs - 远程命令接口 - UIPrefabGenerator.cs - JSON转预制体生成器 - UIPrefabToJson.cs - 预制体转JSON导出器 2. 更新 UI界面拼接设计指南.md - 新增8.9节工具脚本部署说明 - 包含手动部署和自动部署两种方式 - 添加部署规则和验证步骤 3. 更新 README.md - 添加06_工具脚本章节索引 部署方法: - 手动复制:复制工具脚本到项目 Assets/Editor/Command/ 目录 - 自动部署:使用 DeployUITools.bat 脚本 --- 03_核心功能/UI/UI界面拼接设计指南.md | 66 +- 06_工具脚本/UI工具/ExternalCommandListener.cs | 194 ++++++ .../UI工具/ExternalCommandListener.cs.meta | 11 + 06_工具脚本/UI工具/RemoteCommand.cs | 39 ++ 06_工具脚本/UI工具/RemoteCommand.cs.meta | 11 + 06_工具脚本/UI工具/UIPrefabGenerator.cs | 624 ++++++++++++++++++ 06_工具脚本/UI工具/UIPrefabGenerator.cs.meta | 11 + 06_工具脚本/UI工具/UIPrefabToJson.cs | 379 +++++++++++ 06_工具脚本/UI工具/UIPrefabToJson.cs.meta | 11 + README.md | 3 + 10 files changed, 1348 insertions(+), 1 deletion(-) create mode 100644 06_工具脚本/UI工具/ExternalCommandListener.cs create mode 100644 06_工具脚本/UI工具/ExternalCommandListener.cs.meta create mode 100644 06_工具脚本/UI工具/RemoteCommand.cs create mode 100644 06_工具脚本/UI工具/RemoteCommand.cs.meta create mode 100644 06_工具脚本/UI工具/UIPrefabGenerator.cs create mode 100644 06_工具脚本/UI工具/UIPrefabGenerator.cs.meta create mode 100644 06_工具脚本/UI工具/UIPrefabToJson.cs create mode 100644 06_工具脚本/UI工具/UIPrefabToJson.cs.meta diff --git a/03_核心功能/UI/UI界面拼接设计指南.md b/03_核心功能/UI/UI界面拼接设计指南.md index dfd75c2..710ab48 100644 --- a/03_核心功能/UI/UI界面拼接设计指南.md +++ b/03_核心功能/UI/UI界面拼接设计指南.md @@ -347,7 +347,71 @@ UIPrefabToJson.ExportPrefabToJson( 4. **坐标系统**:使用Unity RectTransform的anchorMin/anchorMax/position/sizeDelta四元组 5. **文件锁定**:生成前确保目标预制体文件未被其他进程占用 +### 8.9 工具脚本部署 + +#### 8.9.1 工具位置 + +工具脚本位于UnityAI文档库的以下路径: + +``` +UnityAI/06_工具脚本/UI工具/ +├── ExternalCommandListener.cs # 外部命令监听器 +├── RemoteCommand.cs # 远程命令接口 +├── UIPrefabGenerator.cs # JSON转预制体生成器 +└── UIPrefabToJson.cs # 预制体转JSON导出器 +``` + +#### 8.9.2 部署步骤 + +**方法一:手动复制** + +1. 复制 `06_工具脚本/UI工具/` 目录下的所有文件 +2. 粘贴到目标项目的 `Assets/Editor/Command/` 目录中 +3. 确保目录结构如下: + ``` + Assets/Editor/Command/ + ├── ExternalCommandListener.cs + ├── RemoteCommand.cs + ├── UIPrefabGenerator.cs + └── UIPrefabToJson.cs + ``` + +**方法二:自动部署脚本** + +创建部署脚本 `DeployUITools.bat`: + +```batch +@echo off +set "SOURCE_DIR=E:\AI\UnityAI\06_工具脚本\UI工具" +set "TARGET_DIR=%cd%\Assets\Editor\Command" + +mkdir "%TARGET_DIR%" 2>nul +xcopy "%SOURCE_DIR%\*.cs" "%TARGET_DIR%\" /y +xcopy "%SOURCE_DIR%\*.cs.meta" "%TARGET_DIR%\" /y + +echo UI工具部署完成 +pause +``` + +#### 8.9.3 部署规则 + +| 检查项 | 要求 | +|-------|------| +| **目标路径** | 必须放置在 `Assets/Editor/Command/` 目录下 | +| **文件完整性** | 必须包含所有4个cs文件及其meta文件 | +| **Unity版本** | 支持Unity 2020及以上版本 | +| **依赖组件** | 需要导入 TextMeshPro 包 | + +#### 8.9.4 验证部署 + +部署完成后,在Unity编辑器中验证: + +1. 打开Unity编辑器 +2. 检查菜单 `Tools > UI` 是否存在 +3. 检查菜单 `Tools > External` 是否存在 +4. 在Console窗口确认无编译错误 + --- -**版本**: v1.1 +**版本**: v1.2 **适用**: StrayFog框架UI拼接开发人员 \ No newline at end of file diff --git a/06_工具脚本/UI工具/ExternalCommandListener.cs b/06_工具脚本/UI工具/ExternalCommandListener.cs new file mode 100644 index 0000000..270d2bf --- /dev/null +++ b/06_工具脚本/UI工具/ExternalCommandListener.cs @@ -0,0 +1,194 @@ +using UnityEditor; +using UnityEngine; +using System.IO; + +[InitializeOnLoad] +public static class ExternalCommandListener +{ + private static FileSystemWatcher watcher; + private static string triggerFilePath; + private static bool isInitialized = false; + + static ExternalCommandListener() + { + EditorApplication.update += InitializeOnFirstUpdate; + } + + private static void InitializeOnFirstUpdate() + { + if (!isInitialized) + { + isInitialized = true; + EditorApplication.update -= InitializeOnFirstUpdate; + SetupFileWatcher(); + Debug.Log("ExternalCommandListener started"); + } + } + + private static void SetupFileWatcher() + { + triggerFilePath = Path.Combine(Application.dataPath, "..", "Trigger_Command.txt"); + string watchFolder = Path.GetDirectoryName(triggerFilePath); + + if (watcher != null) watcher.Dispose(); + + watcher = new FileSystemWatcher(watchFolder, "Trigger_Command.txt"); + watcher.NotifyFilter = NotifyFilters.CreationTime | NotifyFilters.LastWrite; + watcher.Created += OnFileChanged; + watcher.Changed += OnFileChanged; + watcher.EnableRaisingEvents = true; + } + + private static void OnFileChanged(object sender, FileSystemEventArgs e) + { + if (e.ChangeType != WatcherChangeTypes.Created && e.ChangeType != WatcherChangeTypes.Changed) + return; + + EditorApplication.delayCall += ExecuteCommandFromFile; + } + + private static void ExecuteCommandFromFile() + { + try + { + if (!File.Exists(triggerFilePath)) return; + + string content = File.ReadAllText(triggerFilePath).Trim(); + File.Delete(triggerFilePath); + + ParseAndExecute(content); + } + catch (System.Exception ex) + { + Debug.LogError("Execute failed: " + ex.Message); + } + } + + private static void ParseAndExecute(string content) + { + content = content.Trim().Trim('"', '\''); + + string[] parts = content.Split(':'); + if (parts.Length == 0) return; + + string command = parts[0].Trim(); + string[] args = parts.Length > 1 ? parts[1..] : new string[0]; + + ExecuteCommand(command, args); + } + + private static void ExecuteCommand(string command, string[] args) + { + switch (command) + { + case "ReadJson": + ExecuteReadJson(args); + break; + case "GenerateUIPrefab": + ExecuteGenerateUIPrefab(args); + break; + case "ExportPrefabToJson": + ExecuteExportPrefabToJson(args); + break; + default: + Debug.LogWarning("Unknown command: " + command); + break; + } + } + + private static void ExecuteReadJson(string[] args) + { + try + { + var method = typeof(RemoteCommand).GetMethod("ReadJson", + System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static, + null, + new[] { typeof(string[]) }, + null); + + if (method != null) + { + method.Invoke(null, new object[] { args }); + Debug.Log("Executed: ReadJson(" + string.Join(", ", args) + ")"); + } + else + { + Debug.LogError("RemoteCommand.ReadJson not found"); + } + } + catch (System.Exception ex) + { + Debug.LogError("ReadJson failed: " + ex.Message); + } + } + + private static void ExecuteGenerateUIPrefab(string[] args) + { + try + { + var method = typeof(RemoteCommand).GetMethod("GenerateUIPrefab", + System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static, + null, + new[] { typeof(string[]) }, + null); + + if (method != null) + { + method.Invoke(null, new object[] { args }); + Debug.Log("Executed: GenerateUIPrefab(" + string.Join(", ", args) + ")"); + } + else + { + Debug.LogError("RemoteCommand.GenerateUIPrefab not found"); + } + } + catch (System.Exception ex) + { + Debug.LogError("GenerateUIPrefab failed: " + ex.Message); + } + } + + [MenuItem("Tools/External/Test ReadJson")] + public static void TestExecuteReadJson() + { + ExecuteReadJson(new string[0]); + } + + [MenuItem("Tools/External/Test GenerateUIPrefab")] + public static void TestExecuteGenerateUIPrefab() + { + ExecuteGenerateUIPrefab(new[] { "Assets/Editor/UI/BagWindow.json" }); + } + + private static void ExecuteExportPrefabToJson(string[] args) + { + try + { + var method = typeof(RemoteCommand).GetMethod("ExportPrefabToJson", + System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static, + null, + new[] { typeof(string[]) }, + null); + + if (method != null) + { + method.Invoke(null, new object[] { args }); + Debug.Log("Executed: ExportPrefabToJson(" + string.Join(", ", args) + ")"); + } + else + { + Debug.LogError("RemoteCommand.ExportPrefabToJson not found"); + } + } + catch (System.Exception ex) + { + Debug.LogError("ExportPrefabToJson failed: " + ex.Message); + } + } + + [MenuItem("Tools/External/Test ExportPrefabToJson")] + public static void TestExecuteExportPrefabToJson() + { + ExecuteExportPrefabToJson(new[] { "Assets/Game/GameHFResource/CN/UIWindows/BagWindow/BagWindow.prefab" }); + } +} \ No newline at end of file diff --git a/06_工具脚本/UI工具/ExternalCommandListener.cs.meta b/06_工具脚本/UI工具/ExternalCommandListener.cs.meta new file mode 100644 index 0000000..4d3174d --- /dev/null +++ b/06_工具脚本/UI工具/ExternalCommandListener.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 93f1f8f942aa53347a7e58bb4c561893 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/06_工具脚本/UI工具/RemoteCommand.cs b/06_工具脚本/UI工具/RemoteCommand.cs new file mode 100644 index 0000000..d5b0f81 --- /dev/null +++ b/06_工具脚本/UI工具/RemoteCommand.cs @@ -0,0 +1,39 @@ +public static class RemoteCommand +{ + public static void ReadJson(string[] args) + { + SFP.Error("收到参数: " + string.Join(", ", args)); + } + + public static void GenerateUIPrefab(string[] args) + { + if (args.Length == 0) + { + SFP.Error("GenerateUIPrefab: 需要提供JSON文件路径参数"); + return; + } + + string jsonPath = args[0]; + string prefabPath = args.Length > 1 ? args[1] : null; + + SFP.Error("GenerateUIPrefab: 开始生成UI, JSON路径: " + jsonPath); + + UIPrefabGenerator.GeneratePrefabFromJson(jsonPath, prefabPath); + } + + public static void ExportPrefabToJson(string[] args) + { + if (args.Length == 0) + { + SFP.Error("ExportPrefabToJson: 需要提供预制体文件路径参数"); + return; + } + + string prefabPath = args[0]; + string jsonPath = args.Length > 1 ? args[1] : null; + + SFP.Error("ExportPrefabToJson: 开始导出JSON, 预制体路径: " + prefabPath); + + UIPrefabToJson.ExportPrefabToJson(prefabPath, jsonPath); + } +} \ No newline at end of file diff --git a/06_工具脚本/UI工具/RemoteCommand.cs.meta b/06_工具脚本/UI工具/RemoteCommand.cs.meta new file mode 100644 index 0000000..1121c04 --- /dev/null +++ b/06_工具脚本/UI工具/RemoteCommand.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 359ded64689f3f64c87c561d88cead9d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/06_工具脚本/UI工具/UIPrefabGenerator.cs b/06_工具脚本/UI工具/UIPrefabGenerator.cs new file mode 100644 index 0000000..346489c --- /dev/null +++ b/06_工具脚本/UI工具/UIPrefabGenerator.cs @@ -0,0 +1,624 @@ +using System; +using System.Collections.Generic; +using System.IO; +using UnityEngine; +using UnityEngine.UI; +using UnityEditor; +using TMPro; + +public static class UIPrefabGenerator +{ + [MenuItem("Tools/UI/Generate BagWindow Prefab")] + public static void GenerateBagWindowPrefab() + { + string jsonPath = "Assets/Editor/UI/BagWindow.json"; + string prefabPath = "Assets/Game/GameHFResource/CN/UIWindows/BagWindow/BagWindow.prefab"; + + if (!File.Exists(jsonPath)) + { + Debug.LogError("JSON file not found: " + jsonPath); + return; + } + + string jsonContent = File.ReadAllText(jsonPath); + UILayoutData layoutData = JsonUtility.FromJson(jsonContent); + + GameObject root = new GameObject(layoutData.name); + root.layer = 5; + + Canvas canvas = root.AddComponent(); + canvas.renderMode = RenderMode.ScreenSpaceOverlay; + canvas.pixelPerfect = layoutData.canvas.pixelPerfect; + + root.AddComponent(); + root.AddComponent(); + + RectTransform rootRect = root.GetComponent(); + SetRectTransform(rootRect, layoutData.rectTransform); + + foreach (var childData in layoutData.children) + { + CreateUIElement(childData, root.transform); + } + + CreatePrefab(root, prefabPath); + GameObject.DestroyImmediate(root); + + AssetDatabase.Refresh(); + Debug.Log("UI Prefab generated successfully: " + prefabPath); + } + + public static void GeneratePrefabFromJson(string jsonPath, string prefabPath = null) + { + if (!File.Exists(jsonPath)) + { + Debug.LogError("JSON file not found: " + jsonPath); + return; + } + + string jsonContent = File.ReadAllText(jsonPath); + UILayoutData layoutData = JsonUtility.FromJson(jsonContent); + + if (layoutData == null) + { + Debug.LogError("Failed to parse JSON file: " + jsonPath); + return; + } + + if (string.IsNullOrEmpty(prefabPath)) + { + prefabPath = "Assets/Game/GameHFResource/CN/UIWindows/" + layoutData.name + "/" + layoutData.name + ".prefab"; + } + + GameObject root = new GameObject(layoutData.name); + root.layer = layoutData.layer; + + Canvas canvas = root.AddComponent(); + canvas.renderMode = RenderMode.ScreenSpaceOverlay; + if (layoutData.canvas != null) + { + canvas.pixelPerfect = layoutData.canvas.pixelPerfect; + } + + root.AddComponent(); + root.AddComponent(); + + RectTransform rootRect = root.GetComponent(); + SetRectTransform(rootRect, layoutData.rectTransform); + + foreach (var childData in layoutData.children) + { + CreateUIElement(childData, root.transform); + } + + CreatePrefab(root, prefabPath); + GameObject.DestroyImmediate(root); + + AssetDatabase.Refresh(); + Debug.Log("UI Prefab generated successfully: " + prefabPath); + } + + private static void CreateUIElement(UIElementData data, Transform parent) + { + GameObject obj = new GameObject(data.name); + obj.transform.SetParent(parent, false); + + RectTransform rect = obj.AddComponent(); + SetRectTransform(rect, data.rectTransform); + + switch (data.type) + { + case "Image": + case "SFUI_Image": + CreateImage(obj, data.image); + break; + case "Button": + case "SFUI_Button": + CreateButton(obj, data.button, data.image); + break; + case "TextMeshProUGUI": + case "SFUI_TextMeshProUGUI": + CreateTextMeshPro(obj, data.text); + break; + case "RectTransform": + case "SFUI_Viewport": + case "SFUI_Content": + CreateRectTransform(obj, data.layoutGroup); + break; + case "ScrollRect": + case "SFUI_ScrollRect": + CreateScrollRect(obj, data.scrollRect); + break; + case "GridLayoutGroup": + CreateGridLayoutGroup(obj, data.layoutGroup); + break; + case "VerticalLayoutGroup": + CreateVerticalLayoutGroup(obj, data.layoutGroup); + break; + case "HorizontalLayoutGroup": + CreateHorizontalLayoutGroup(obj, data.layoutGroup); + break; + case "Toggle": + case "SFUI_Toggle": + CreateToggle(obj, data.toggle); + break; + case "Slider": + case "SFUI_Slider": + CreateSlider(obj, data.slider); + break; + case "InputField": + case "SFUI_InputField": + CreateInputField(obj, data.inputField); + break; + case "CanvasScaler": + case "GraphicRaycaster": + break; + case "SFUI_ListViewItem": + CreateListViewItemPlaceholder(obj); + break; + case "Canvas": + case "SFUI_Window": + break; + default: + break; + } + + if (data.children != null) + { + foreach (var childData in data.children) + { + CreateUIElement(childData, obj.transform); + } + } + } + + private static void SetRectTransform(RectTransform rect, RectTransformData data) + { + if (data == null) return; + + rect.anchorMin = new Vector2(data.anchorMin[0], data.anchorMin[1]); + rect.anchorMax = new Vector2(data.anchorMax[0], data.anchorMax[1]); + rect.anchoredPosition3D = new Vector3(data.position[0], data.position[1], data.position[2]); + rect.sizeDelta = new Vector2(data.sizeDelta[0], data.sizeDelta[1]); + rect.pivot = new Vector2(data.pivot[0], data.pivot[1]); + + if (data.localScale != null && data.localScale.Length >= 3) + { + rect.localScale = new Vector3(data.localScale[0], data.localScale[1], data.localScale[2]); + } + + if (data.localRotation != null && data.localRotation.Length >= 3) + { + rect.localEulerAngles = new Vector3(data.localRotation[0], data.localRotation[1], data.localRotation[2]); + } + } + + private static void CreateImage(GameObject obj, ImageData data) + { + if (data == null) return; + + Image image = obj.AddComponent(); + if (!string.IsNullOrEmpty(data.sprite)) + { + Sprite sprite = AssetDatabase.LoadAssetAtPath(data.sprite); + if (sprite != null) + { + image.sprite = sprite; + } + } + + try + { + image.type = (Image.Type)Enum.Parse(typeof(Image.Type), data.type); + } + catch + { + image.type = Image.Type.Simple; + } + + image.color = new Color(data.color[0], data.color[1], data.color[2], data.color[3]); + image.raycastTarget = data.raycastTarget; + image.preserveAspect = data.preserveAspect; + } + + private static void CreateButton(GameObject obj, ButtonData buttonData, ImageData imageData) + { + Button button = obj.AddComponent