[docs][refactor]: 将旧 01~06 中文教程合并整理为 modules/frameworks 文件夹结构

- 删除 01~05 旧中文教程目录
- 将 03 核心功能教程拆分为各 framework 模块的子文档
- 06 工具脚本移入 modules/frameworks/编辑器工具/UI工具/
- 非框架内容合并进 rules/20-开发规则.md 与 rules/40-工作流清单.md
- 更新所有交叉链接与文档关联表

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
lms
2026-07-07 18:36:15 +08:00
parent 5e5b9f8c80
commit ee1ff7f444
77 changed files with 6263 additions and 1133 deletions

View File

@@ -0,0 +1,54 @@
# 编辑器工具
## 分类
- 编辑器工具
## 目的
汇总 Unity Editor 内使用的 StrayFog 相关编辑器扩展和项目编辑器资源。
---
## SF Editor
- **路径**`Assets/StrayFog/Editor/`
- **程序集**`SFEditor`
- **职责**
- StrayFog 编辑器扩展
- 打包、UGUI、SpriteAtlas、表格路径选择等编辑器工具
---
## Project Editor Assets
- **路径**`Assets/Editor/`
- **职责**
- 项目编辑器资源与脚本
- 引导菜单、编辑器插件
---
## 使用规则
- 编辑器工具可以读取/生成核心运行时代码,但运行时代码不要反向依赖 Editor 程序集。
- 新增 Editor 工具时,优先在 `Assets/StrayFog/Editor/``Assets/Editor/` 下按功能分类存放。
---
## 相关模块
- [通用框架](../通用框架/README.md)
- [资源管理](../资源管理/README.md)
---
## 文档关联
> 当本文档发生变更时,应同步检查以下关联文档,避免信息不一致。
| 文档 | 关联原因 |
|---|---|
| [modules/模块索引.md](../../模块索引.md) | 模块索引是项目知识总入口 |
| [rules/10-架构说明.md](../../../rules/10-架构说明.md) | 编辑器工具层的架构说明 |
| [rules/30-模块登记规则.md](../../../rules/30-模块登记规则.md) | 模块登记规则定义分类和状态口径 |

View File

@@ -0,0 +1,176 @@
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);
}
}
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);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 93f1f8f942aa53347a7e58bb4c561893
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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);
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 359ded64689f3f64c87c561d88cead9d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,584 @@
using System;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEngine.UI;
using UnityEditor;
using TMPro;
public static class UIPrefabGenerator
{
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<UILayoutData>(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>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
if (layoutData.canvas != null)
{
canvas.pixelPerfect = layoutData.canvas.pixelPerfect;
}
root.AddComponent<CanvasScaler>();
root.AddComponent<GraphicRaycaster>();
RectTransform rootRect = root.GetComponent<RectTransform>();
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<RectTransform>();
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<Image>();
if (!string.IsNullOrEmpty(data.sprite))
{
Sprite sprite = AssetDatabase.LoadAssetAtPath<Sprite>(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<Button>();
if (buttonData != null)
{
button.interactable = buttonData.interactable;
try
{
button.transition = (Selectable.Transition)Enum.Parse(typeof(Selectable.Transition), buttonData.transition);
}
catch
{
button.transition = Selectable.Transition.ColorTint;
}
}
if (imageData != null)
{
CreateImage(obj, imageData);
}
}
private static void CreateTextMeshPro(GameObject obj, TextData data)
{
if (data == null) return;
TextMeshProUGUI text = obj.AddComponent<TextMeshProUGUI>();
text.text = data.text;
text.fontSize = data.fontSize;
text.color = new Color(data.color[0], data.color[1], data.color[2], data.color[3]);
if (!string.IsNullOrEmpty(data.font))
{
TMP_FontAsset fontAsset = AssetDatabase.LoadAssetAtPath<TMP_FontAsset>(data.font);
if (fontAsset != null)
{
text.font = fontAsset;
}
}
if (!string.IsNullOrEmpty(data.materialPreset))
{
Material material = AssetDatabase.LoadAssetAtPath<Material>(data.materialPreset);
if (material != null)
{
text.material = material;
}
}
try
{
text.alignment = (TextAlignmentOptions)Enum.Parse(typeof(TextAlignmentOptions), data.alignment);
}
catch
{
text.alignment = TextAlignmentOptions.Center;
}
try
{
text.verticalAlignment = (VerticalAlignmentOptions)Enum.Parse(typeof(VerticalAlignmentOptions), data.verticalAlignment);
}
catch
{
text.verticalAlignment = VerticalAlignmentOptions.Middle;
}
text.richText = data.richText;
try
{
text.overflowMode = (TextOverflowModes)Enum.Parse(typeof(TextOverflowModes), data.overflowMode);
}
catch
{
text.overflowMode = TextOverflowModes.Truncate;
}
text.enableWordWrapping = data.enableWordWrapping;
}
private static void CreateRectTransform(GameObject obj, LayoutGroupData layoutData)
{
if (layoutData != null)
{
switch (layoutData.type)
{
case "GridLayoutGroup":
CreateGridLayoutGroup(obj, layoutData);
break;
case "VerticalLayoutGroup":
CreateVerticalLayoutGroup(obj, layoutData);
break;
case "HorizontalLayoutGroup":
CreateHorizontalLayoutGroup(obj, layoutData);
break;
}
}
}
private static void CreateGridLayoutGroup(GameObject obj, LayoutGroupData layoutData)
{
if (layoutData == null) return;
GridLayoutGroup grid = obj.AddComponent<GridLayoutGroup>();
grid.cellSize = new Vector2(layoutData.cellSize[0], layoutData.cellSize[1]);
grid.spacing = new Vector2(layoutData.spacing[0], layoutData.spacing[1]);
grid.constraint = (GridLayoutGroup.Constraint)layoutData.constraint;
try
{
grid.childAlignment = (TextAnchor)Enum.Parse(typeof(TextAnchor), layoutData.childAlignment);
}
catch
{
grid.childAlignment = TextAnchor.UpperLeft;
}
grid.padding = new RectOffset(layoutData.padding[0], layoutData.padding[1], layoutData.padding[2], layoutData.padding[3]);
}
private static void CreateVerticalLayoutGroup(GameObject obj, LayoutGroupData layoutData)
{
if (layoutData == null) return;
VerticalLayoutGroup vertical = obj.AddComponent<VerticalLayoutGroup>();
vertical.spacing = layoutData.spacing[1];
try
{
vertical.childAlignment = (TextAnchor)Enum.Parse(typeof(TextAnchor), layoutData.childAlignment);
}
catch
{
vertical.childAlignment = TextAnchor.UpperLeft;
}
vertical.padding = new RectOffset(layoutData.padding[0], layoutData.padding[1], layoutData.padding[2], layoutData.padding[3]);
}
private static void CreateHorizontalLayoutGroup(GameObject obj, LayoutGroupData layoutData)
{
if (layoutData == null) return;
HorizontalLayoutGroup horizontal = obj.AddComponent<HorizontalLayoutGroup>();
horizontal.spacing = layoutData.spacing[0];
try
{
horizontal.childAlignment = (TextAnchor)Enum.Parse(typeof(TextAnchor), layoutData.childAlignment);
}
catch
{
horizontal.childAlignment = TextAnchor.UpperLeft;
}
horizontal.padding = new RectOffset(layoutData.padding[0], layoutData.padding[1], layoutData.padding[2], layoutData.padding[3]);
}
private static void CreateScrollRect(GameObject obj, ScrollRectData data)
{
if (data == null) return;
ScrollRect scrollRect = obj.AddComponent<ScrollRect>();
scrollRect.horizontal = data.horizontal;
scrollRect.vertical = data.vertical;
try
{
scrollRect.movementType = (ScrollRect.MovementType)Enum.Parse(typeof(ScrollRect.MovementType), data.movementType);
}
catch
{
scrollRect.movementType = ScrollRect.MovementType.Clamped;
}
}
private static void CreateToggle(GameObject obj, ToggleData data)
{
if (data == null) return;
Toggle toggle = obj.AddComponent<Toggle>();
toggle.isOn = data.isOn;
try
{
toggle.toggleTransition = (Toggle.ToggleTransition)Enum.Parse(typeof(Toggle.ToggleTransition), data.toggleTransition);
}
catch
{
toggle.toggleTransition = Toggle.ToggleTransition.Fade;
}
try
{
toggle.transition = (Selectable.Transition)Enum.Parse(typeof(Selectable.Transition), data.transition);
}
catch
{
toggle.transition = Selectable.Transition.ColorTint;
}
}
private static void CreateSlider(GameObject obj, SliderData data)
{
if (data == null) return;
Slider slider = obj.AddComponent<Slider>();
slider.minValue = data.minValue;
slider.maxValue = data.maxValue;
slider.value = data.value;
slider.wholeNumbers = data.wholeNumbers;
try
{
slider.direction = (Slider.Direction)Enum.Parse(typeof(Slider.Direction), data.direction);
}
catch
{
slider.direction = Slider.Direction.LeftToRight;
}
}
private static void CreateInputField(GameObject obj, InputFieldData data)
{
if (data == null) return;
InputField inputField = obj.AddComponent<InputField>();
inputField.text = data.text;
inputField.characterLimit = data.characterLimit;
try
{
inputField.contentType = (InputField.ContentType)Enum.Parse(typeof(InputField.ContentType), data.contentType);
}
catch
{
inputField.contentType = InputField.ContentType.Standard;
}
try
{
inputField.inputType = (InputField.InputType)Enum.Parse(typeof(InputField.InputType), data.inputType);
}
catch
{
inputField.inputType = InputField.InputType.Standard;
}
}
private static void CreateListViewItemPlaceholder(GameObject obj)
{
}
private static void CreatePrefab(GameObject root, string path)
{
string directory = Path.GetDirectoryName(path);
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
PrefabUtility.SaveAsPrefabAsset(root, path);
}
}
[Serializable]
public class UILayoutData
{
public string name;
public string type;
public int layer;
public RectTransformData rectTransform;
public CanvasData canvas;
public List<UIElementData> children;
}
[Serializable]
public class UIElementData
{
public string name;
public string type;
public RectTransformData rectTransform;
public ImageData image;
public ButtonData button;
public TextData text;
public LayoutGroupData layoutGroup;
public ScrollRectData scrollRect;
public ToggleData toggle;
public SliderData slider;
public InputFieldData inputField;
public List<CustomComponentData> customComponents;
public List<UIElementData> children;
}
[Serializable]
public class RectTransformData
{
public float[] anchorMin;
public float[] anchorMax;
public float[] position;
public float[] sizeDelta;
public float[] pivot;
public float[] localScale;
public float[] localRotation;
}
[Serializable]
public class CanvasData
{
public string renderMode;
public bool pixelPerfect;
}
[Serializable]
public class ImageData
{
public string sprite;
public string type;
public float[] color;
public bool raycastTarget;
public bool preserveAspect;
}
[Serializable]
public class ButtonData
{
public bool interactable;
public string transition;
public string navigationMode;
}
[Serializable]
public class TextData
{
public string text;
public float fontSize;
public string font;
public string materialPreset;
public string alignment;
public string verticalAlignment;
public float[] color;
public bool richText;
public string overflowMode;
public bool enableWordWrapping;
}
[Serializable]
public class LayoutGroupData
{
public string type;
public float[] cellSize;
public float[] spacing;
public int constraint;
public string childAlignment;
public int[] padding;
}
[Serializable]
public class ScrollRectData
{
public bool horizontal;
public bool vertical;
public string movementType;
public string horizontalScrollbarVisibility;
public string verticalScrollbarVisibility;
}
[Serializable]
public class ToggleData
{
public bool isOn;
public string toggleTransition;
public string transition;
}
[Serializable]
public class SliderData
{
public float minValue;
public float maxValue;
public float value;
public bool wholeNumbers;
public string direction;
}
[Serializable]
public class InputFieldData
{
public string text;
public string placeholder;
public int characterLimit;
public string contentType;
public string inputType;
}
[Serializable]
public class CustomComponentData
{
public string typeName;
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 31deea0e57bec2f4893548b0a41853cb
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,366 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using UnityEngine;
using UnityEngine.UI;
using UnityEditor;
using TMPro;
public static class UIPrefabToJson
{
public static void ExportPrefabToJson(string prefabPath, string jsonPath = null)
{
if (!AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath))
{
Debug.LogError("Prefab not found: " + prefabPath);
return;
}
if (string.IsNullOrEmpty(jsonPath))
{
jsonPath = prefabPath.Replace(".prefab", ".json");
}
GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
UILayoutData layoutData = ConvertPrefabToLayoutData(prefab);
string jsonContent = JsonUtility.ToJson(layoutData, true);
string fullPath = Path.Combine(Application.dataPath, "..", jsonPath);
string directory = Path.GetDirectoryName(fullPath);
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
File.WriteAllText(fullPath, jsonContent);
AssetDatabase.Refresh();
Debug.Log("JSON exported successfully: " + jsonPath);
}
private static UILayoutData ConvertPrefabToLayoutData(GameObject prefab)
{
UILayoutData data = new UILayoutData();
data.name = prefab.name;
data.type = "Canvas";
data.layer = prefab.layer;
Canvas canvas = prefab.GetComponent<Canvas>();
if (canvas != null)
{
data.canvas = new CanvasData();
data.canvas.renderMode = canvas.renderMode.ToString();
data.canvas.pixelPerfect = canvas.pixelPerfect;
}
RectTransform rectTransform = prefab.GetComponent<RectTransform>();
if (rectTransform != null)
{
data.rectTransform = ConvertRectTransform(rectTransform);
}
data.children = new List<UIElementData>();
foreach (Transform child in prefab.transform)
{
UIElementData childData = ConvertTransformToElement(child);
if (childData != null)
{
data.children.Add(childData);
}
}
return data;
}
private static UIElementData ConvertTransformToElement(Transform transform)
{
UIElementData data = new UIElementData();
data.name = transform.name;
RectTransform rectTransform = transform.GetComponent<RectTransform>();
if (rectTransform != null)
{
data.rectTransform = ConvertRectTransform(rectTransform);
}
Component[] components = transform.GetComponents<Component>();
List<string> componentTypes = new List<string>();
foreach (Component comp in components)
{
if (comp == null) continue;
string compTypeName = comp.GetType().Name;
componentTypes.Add(compTypeName);
switch (compTypeName)
{
case "Image":
case "SFUI_Image":
data.type = compTypeName;
data.image = ConvertImage((Image)comp);
break;
case "Button":
case "SFUI_Button":
data.type = compTypeName;
data.button = ConvertButton((Button)comp);
break;
case "TextMeshProUGUI":
case "SFUI_TextMeshProUGUI":
data.type = compTypeName;
data.text = ConvertText((TextMeshProUGUI)comp);
break;
case "ScrollRect":
case "SFUI_ScrollRect":
data.type = compTypeName;
data.scrollRect = ConvertScrollRect((ScrollRect)comp);
break;
case "GridLayoutGroup":
data.type = compTypeName;
data.layoutGroup = ConvertGridLayoutGroup((GridLayoutGroup)comp);
break;
case "VerticalLayoutGroup":
data.type = compTypeName;
data.layoutGroup = ConvertVerticalLayoutGroup((VerticalLayoutGroup)comp);
break;
case "HorizontalLayoutGroup":
data.type = compTypeName;
data.layoutGroup = ConvertHorizontalLayoutGroup((HorizontalLayoutGroup)comp);
break;
case "Toggle":
case "SFUI_Toggle":
data.type = compTypeName;
data.toggle = ConvertToggle((Toggle)comp);
break;
case "Slider":
case "SFUI_Slider":
data.type = compTypeName;
data.slider = ConvertSlider((Slider)comp);
break;
case "InputField":
case "SFUI_InputField":
data.type = compTypeName;
data.inputField = ConvertInputField((InputField)comp);
break;
case "RectTransform":
if (string.IsNullOrEmpty(data.type))
data.type = compTypeName;
break;
case "Canvas":
if (string.IsNullOrEmpty(data.type))
data.type = compTypeName;
break;
case "CanvasScaler":
data.type = compTypeName;
break;
case "GraphicRaycaster":
data.type = compTypeName;
break;
default:
if (compTypeName != "Transform" && compTypeName != "RectTransform")
{
if (data.customComponents == null)
data.customComponents = new List<CustomComponentData>();
data.customComponents.Add(ConvertCustomComponent(comp));
}
break;
}
}
if (string.IsNullOrEmpty(data.type))
{
data.type = "Transform";
}
if (transform.childCount > 0)
{
data.children = new List<UIElementData>();
foreach (Transform child in transform)
{
UIElementData childData = ConvertTransformToElement(child);
if (childData != null)
{
data.children.Add(childData);
}
}
}
return data;
}
private static RectTransformData ConvertRectTransform(RectTransform rect)
{
RectTransformData data = new RectTransformData();
data.anchorMin = new float[] { rect.anchorMin.x, rect.anchorMin.y };
data.anchorMax = new float[] { rect.anchorMax.x, rect.anchorMax.y };
data.position = new float[] { rect.anchoredPosition3D.x, rect.anchoredPosition3D.y, rect.anchoredPosition3D.z };
data.sizeDelta = new float[] { rect.sizeDelta.x, rect.sizeDelta.y };
data.pivot = new float[] { rect.pivot.x, rect.pivot.y };
data.localScale = new float[] { rect.localScale.x, rect.localScale.y, rect.localScale.z };
data.localRotation = new float[] { rect.localEulerAngles.x, rect.localEulerAngles.y, rect.localEulerAngles.z };
return data;
}
private static ImageData ConvertImage(Image image)
{
ImageData data = new ImageData();
data.type = image.type.ToString();
data.color = new float[] { image.color.r, image.color.g, image.color.b, image.color.a };
data.raycastTarget = image.raycastTarget;
data.preserveAspect = image.preserveAspect;
if (image.sprite != null)
{
string spritePath = AssetDatabase.GetAssetPath(image.sprite);
if (!string.IsNullOrEmpty(spritePath))
{
data.sprite = spritePath;
}
}
return data;
}
private static ButtonData ConvertButton(Button button)
{
ButtonData data = new ButtonData();
data.interactable = button.interactable;
data.transition = button.transition.ToString();
data.navigationMode = button.navigation.mode.ToString();
return data;
}
private static TextData ConvertText(TextMeshProUGUI text)
{
TextData data = new TextData();
data.text = text.text;
data.fontSize = text.fontSize;
data.color = new float[] { text.color.r, text.color.g, text.color.b, text.color.a };
data.alignment = text.alignment.ToString();
data.verticalAlignment = text.verticalAlignment.ToString();
data.richText = text.richText;
data.overflowMode = text.overflowMode.ToString();
data.enableWordWrapping = text.enableWordWrapping;
if (text.font != null)
{
string fontPath = AssetDatabase.GetAssetPath(text.font);
if (!string.IsNullOrEmpty(fontPath))
{
data.font = fontPath;
}
}
if (text.material != null)
{
string materialPath = AssetDatabase.GetAssetPath(text.material);
if (!string.IsNullOrEmpty(materialPath))
{
data.materialPreset = materialPath;
}
}
return data;
}
private static ScrollRectData ConvertScrollRect(ScrollRect scrollRect)
{
ScrollRectData data = new ScrollRectData();
data.horizontal = scrollRect.horizontal;
data.vertical = scrollRect.vertical;
data.movementType = scrollRect.movementType.ToString();
data.horizontalScrollbarVisibility = scrollRect.horizontalScrollbarVisibility.ToString();
data.verticalScrollbarVisibility = scrollRect.verticalScrollbarVisibility.ToString();
return data;
}
private static LayoutGroupData ConvertGridLayoutGroup(GridLayoutGroup layout)
{
LayoutGroupData data = new LayoutGroupData();
data.type = "GridLayoutGroup";
data.cellSize = new float[] { layout.cellSize.x, layout.cellSize.y };
data.spacing = new float[] { layout.spacing.x, layout.spacing.y };
data.constraint = (int)layout.constraint;
data.childAlignment = layout.childAlignment.ToString();
data.padding = new int[] { layout.padding.left, layout.padding.right, layout.padding.top, layout.padding.bottom };
return data;
}
private static LayoutGroupData ConvertVerticalLayoutGroup(VerticalLayoutGroup layout)
{
LayoutGroupData data = new LayoutGroupData();
data.type = "VerticalLayoutGroup";
data.spacing = new float[] { layout.spacing, layout.spacing };
data.childAlignment = layout.childAlignment.ToString();
data.padding = new int[] { layout.padding.left, layout.padding.right, layout.padding.top, layout.padding.bottom };
return data;
}
private static LayoutGroupData ConvertHorizontalLayoutGroup(HorizontalLayoutGroup layout)
{
LayoutGroupData data = new LayoutGroupData();
data.type = "HorizontalLayoutGroup";
data.spacing = new float[] { layout.spacing, layout.spacing };
data.childAlignment = layout.childAlignment.ToString();
data.padding = new int[] { layout.padding.left, layout.padding.right, layout.padding.top, layout.padding.bottom };
return data;
}
private static ToggleData ConvertToggle(Toggle toggle)
{
ToggleData data = new ToggleData();
data.isOn = toggle.isOn;
data.toggleTransition = toggle.toggleTransition.ToString();
data.transition = toggle.transition.ToString();
return data;
}
private static SliderData ConvertSlider(Slider slider)
{
SliderData data = new SliderData();
data.minValue = slider.minValue;
data.maxValue = slider.maxValue;
data.value = slider.value;
data.wholeNumbers = slider.wholeNumbers;
data.direction = slider.direction.ToString();
return data;
}
private static InputFieldData ConvertInputField(InputField inputField)
{
InputFieldData data = new InputFieldData();
data.text = inputField.text;
if (inputField.placeholder != null)
{
Text textPlaceholder = inputField.placeholder as Text;
if (textPlaceholder != null)
{
data.placeholder = textPlaceholder.text;
}
else
{
TextMeshProUGUI tmpPlaceholder = inputField.placeholder as TextMeshProUGUI;
if (tmpPlaceholder != null)
{
data.placeholder = tmpPlaceholder.text;
}
}
}
data.characterLimit = inputField.characterLimit;
data.contentType = inputField.contentType.ToString();
data.inputType = inputField.inputType.ToString();
return data;
}
private static CustomComponentData ConvertCustomComponent(Component comp)
{
CustomComponentData data = new CustomComponentData();
data.typeName = comp.GetType().Name;
return data;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 33906f611de8963438be24b30bc8592e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,248 @@
# UI 拼接工具
## 概述
UI 拼接工具提供 JSON 配置与 Unity 预制体的双向转换,帮助 UI 相关人员通过 JSON 配置文件快速生成 UI 预制体,同时支持从现有预制体反向导出 JSON 用于版本控制或重新生成。
## 核心组件
| 组件 | 职责 | 文件路径 |
|-----|------|---------|
| **UIPrefabGenerator** | 解析 JSON 并生成 UI 预制体 | `Assets/Editor/Command/UIPrefabGenerator.cs` |
| **UIPrefabToJson** | 从预制体反向导出 JSON | `Assets/Editor/Command/UIPrefabToJson.cs` |
| **RemoteCommand** | 提供外部调用接口 | `Assets/Editor/Command/RemoteCommand.cs` |
| **ExternalCommandListener** | 监听外部命令触发文件 | `Assets/Editor/Command/ExternalCommandListener.cs` |
工具脚本在文档库中的位置(已并入编辑器工具模块):
```
modules/frameworks/编辑器工具/UI工具/
├── ExternalCommandListener.cs # 外部命令监听器
├── RemoteCommand.cs # 远程命令接口
├── UIPrefabGenerator.cs # JSON 转预制体生成器
└── UIPrefabToJson.cs # 预制体转 JSON 导出器
```
## JSON 配置文件结构
```json
{
"name": "BagWindow",
"width": 1024,
"height": 768,
"elements": [
{
"type": "Image",
"name": "BgPanel",
"anchorMin": [0, 0],
"anchorMax": [1, 1],
"position": [0, 0, 0],
"sizeDelta": [0, 0],
"sprite": "Assets/Game/GameHFResource/CN/UIWindows/BagWindow/Sprites/Bg.png"
},
{
"type": "Text",
"name": "TitleText",
"anchorMin": [0.5, 0.9],
"anchorMax": [0.5, 0.9],
"position": [0, 0, 0],
"sizeDelta": [200, 40],
"text": "背包",
"font": "Assets/Game/GameHFResource/CN/Fonts/FZY4K-M05S SDF.asset",
"fontSize": 28,
"color": [1, 1, 1, 1]
}
]
}
```
## 支持的 UI 元素类型
| 类型 | 对应 Unity 组件 | 关键属性 |
|-----|-------------|---------|
| **Image** | Image | sprite, color, type, preserveAspect |
| **TextMeshProUGUI** | TextMeshProUGUI | text, font, fontSize, color, alignment |
| **Button** | Button | interactable, transition, navigationMode |
| **Slider** | Slider | minValue, maxValue, value, wholeNumbers, direction |
| **Toggle** | Toggle | isOn, toggleTransition, transition |
| **InputField** | InputField | text, placeholder, characterLimit, contentType |
| **ScrollRect** | ScrollRect | horizontal, vertical, movementType |
| **GridLayoutGroup** | GridLayoutGroup | cellSize, spacing, constraint, padding |
| **VerticalLayoutGroup** | VerticalLayoutGroup | spacing, childAlignment, padding |
| **HorizontalLayoutGroup** | HorizontalLayoutGroup | spacing, childAlignment, padding |
## 外部命令触发机制
### 触发方式
通过创建/修改 `Trigger_Command.txt` 文件触发 UI 生成:
```
GenerateUIPrefab:Assets/Editor/UI/BagWindow.json
```
### 命令格式
```
<命令名>:<参数1>:<参数2>:...
```
**支持的命令**
| 命令 | 参数 | 说明 |
|-----|------|------|
| **GenerateUIPrefab** | `jsonPath` [, `prefabPath`] | 根据 JSON 生成 UI 预制体 |
| **ExportPrefabToJson** | `prefabPath` [, `jsonPath`] | 从预制体反向导出 JSON |
| **ReadJson** | [参数列表] | 读取 JSON 配置(测试用) |
### 完整执行流程
```
┌─────────────────────────────────────────────────────────────────────┐
│ 外部系统写入命令到 Trigger_Command.txt │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ ExternalCommandListener (FileSystemWatcher) 检测到文件变化 │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ 解析命令格式GenerateUIPrefab:Assets/Editor/UI/BagWindow.json │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ 通过反射调用 RemoteCommand.GenerateUIPrefab(string[] args) │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ UIPrefabGenerator.GeneratePrefabFromJson(jsonPath, prefabPath) │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ 生成预制体到指定路径Assets/Game/GameHFResource/CN/UIWindows/... │
└─────────────────────────────────────────────────────────────────────┘
```
## 使用示例
### 方式一:外部命令触发
```bash
# 正向:从 JSON 生成预制体
echo "GenerateUIPrefab:Assets/Editor/UI/BagWindow.json" > "F:\JMNet\CrystalBattle_Client\Trigger_Command.txt"
# 反向:从预制体导出 JSON
echo "ExportPrefabToJson:Assets/Game/GameHFResource/CN/UIWindows/BagWindow/BagWindow.prefab" > "F:\JMNet\CrystalBattle_Client\Trigger_Command.txt"
```
### 方式二Unity 编辑器菜单
**正向生成**
1. 打开 Unity 编辑器
2. 点击菜单 `Tools > External > Test GenerateUIPrefab`
3. 查看 Console 窗口日志确认执行结果
**反向导出**
1. 打开 Unity 编辑器
2. 点击菜单 `Tools > UI > Export Prefab to JSON`
3. 选择要导出的预制体文件
### 方式三:代码调用
```csharp
// 正向:从 JSON 生成预制体
UIPrefabGenerator.GeneratePrefabFromJson(
"Assets/Editor/UI/BagWindow.json",
"Assets/Game/GameHFResource/CN/UIWindows/BagWindow/BagWindow.prefab"
);
// 反向:从预制体导出 JSON
UIPrefabToJson.ExportPrefabToJson(
"Assets/Game/GameHFResource/CN/UIWindows/BagWindow/BagWindow.prefab",
"Assets/Editor/UI/BagWindow.json"
);
```
## 注意事项
1. **路径格式**JSON 中的资源路径使用 `/` 分隔符,且为 Unity 项目相对路径
2. **资源引用**:确保 JSON 中引用的 Sprite、Font 等资源已存在于项目中
3. **编码格式**JSON 文件使用 UTF-8 编码
4. **坐标系统**:使用 Unity RectTransform 的 anchorMin/anchorMax/position/sizeDelta 四元组
5. **文件锁定**:生成前确保目标预制体文件未被其他进程占用
## 部署步骤
### 方法一:手动复制
1. 复制 `modules/frameworks/编辑器工具/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\modules\frameworks\编辑器工具\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
```
### 部署检查项
| 检查项 | 要求 |
|-------|------|
| **目标路径** | 必须放置在 `Assets/Editor/Command/` 目录下 |
| **文件完整性** | 必须包含所有 4 个 cs 文件及其 meta 文件 |
| **Unity 版本** | 支持 Unity 2020 及以上版本 |
| **依赖组件** | 需要导入 TextMeshPro 包 |
### 验证部署
部署完成后,在 Unity 编辑器中验证:
1. 打开 Unity 编辑器
2. 检查菜单 `Tools > UI` 是否存在
3. 检查菜单 `Tools > External` 是否存在
4. 在 Console 窗口确认无编译错误
---
**版本**: v1.2
**适用**: StrayFog 框架 UI 拼接开发人员
---
## 文档关联
> 当本文档发生变更时,应同步检查以下关联文档,避免信息不一致。
| 文档 | 关联原因 |
|---|---|
| [编辑器工具入口](README.md) | 本文件是编辑器工具模块的子文档 |
| [UI 窗口系统](../UI窗口系统/README.md) | UI 拼接工具服务于 UI 窗口系统 |
| [UI 界面拼接设计指南](../UI窗口系统/界面拼接.md) | 设计指南说明 JSON 驱动的界面拼接原则 |
| [modules/模块索引.md](../../模块索引.md) | 模块索引是项目知识总入口 |
| [rules/10-架构说明.md](../../../rules/10-架构说明.md) | 架构说明定义编辑器工具层与运行时层边界 |
| [rules/20-开发规则.md](../../../rules/20-开发规则.md) | 编辑器工具开发规则互相引用 |
| [rules/30-模块登记规则.md](../../../rules/30-模块登记规则.md) | 模块登记规则定义分类和状态口径 |