diff --git a/Editor/UI/GenerateWindow/UIGenerateEditorWindow.cs b/Editor/UI/GenerateWindow/UIGenerateEditorWindow.cs deleted file mode 100644 index e9d2768..0000000 --- a/Editor/UI/GenerateWindow/UIGenerateEditorWindow.cs +++ /dev/null @@ -1,112 +0,0 @@ -using System.Linq; -using UnityEditor; -using UnityEngine; - -namespace AlicizaX.UI.Editor -{ - public class UIGenerateEditorWindow : EditorWindow - { - private GameObject selectedObject; - private string[] menuItems; - - [MenuItem("GameObject/UI生成绑定", priority = 10)] - public static void ShowWindow() - { - GameObject selectedObject = Selection.gameObjects.FirstOrDefault(); - if (selectedObject == null) return; - - var uiScriptConfigs = UIGenerateConfiguration.Instance.UIScriptGenerateConfigs; - if (uiScriptConfigs == null || uiScriptConfigs.Count == 0) return; - - var window = GetWindow(true, "UI 生成绑定", true); - window.selectedObject = selectedObject; - - window.menuItems = uiScriptConfigs.Select(config => $"{config.ProjectName}").ToArray(); - - var windowWidth = 300f; - var windowHeight = Mathf.Max(1, window.menuItems.Length) * 35f + 10f; - - - Vector3 objectWorldPosition = selectedObject.transform.position; - Vector2 screenPoint; - - var sceneView = SceneView.lastActiveSceneView; - if (sceneView != null) - { - - Vector2 guiPointInSceneView = HandleUtility.WorldToGUIPoint(objectWorldPosition); - - screenPoint = new Vector2(sceneView.position.x + guiPointInSceneView.x, - sceneView.position.y + guiPointInSceneView.y); - } - else if (EditorWindow.mouseOverWindow != null) - { - - Vector2 guiPoint = HandleUtility.WorldToGUIPoint(objectWorldPosition); - var host = EditorWindow.mouseOverWindow; - screenPoint = new Vector2(host.position.x + guiPoint.x, host.position.y + guiPoint.y); - } - else - { - - var mainDisplay = Display.displays.Length > 0 ? Display.displays[0] : null; - float centerX = (mainDisplay != null) ? (mainDisplay.systemWidth / 2f) : (Screen.width / 2f); - float centerY = (mainDisplay != null) ? (mainDisplay.systemHeight / 2f) : (Screen.height / 2f); - screenPoint = new Vector2(centerX, centerY); - } - - - Vector2 windowPosition = new Vector2(screenPoint.x, screenPoint.y - windowHeight - 5f); - - - float screenW = Mathf.Max(100, Display.main.systemWidth); - float screenH = Mathf.Max(100, Display.main.systemHeight); - if (windowPosition.x + windowWidth > screenW) windowPosition.x = screenW - windowWidth - 5f; - if (windowPosition.x < 5f) windowPosition.x = 5f; - if (windowPosition.y < 5f) windowPosition.y = 5f; - if (windowPosition.y + windowHeight > screenH) windowPosition.y = screenH - windowHeight - 5f; - - window.minSize = new Vector2(windowWidth, windowHeight); - window.maxSize = new Vector2(windowWidth, windowHeight); - - window.position = new Rect(windowPosition, new Vector2(windowWidth, windowHeight)); - window.ShowPopup(); - } - - private void OnGUI() - { - GUILayout.Space(5); - if (menuItems == null || menuItems.Length == 0) - { - EditorGUILayout.LabelField("没有可用配置"); - return; - } - - foreach (var item in menuItems) - { - if (GUILayout.Button(item, EditorStyles.toolbarButton, GUILayout.Height(28))) - { - GenerateScriptForConfig(selectedObject, item); - Close(); - } - - GUILayout.Space(6); - } - } - - private void GenerateScriptForConfig(GameObject selectedObject, string itemName) - { - var uiScriptConfigs = UIGenerateConfiguration.Instance.UIScriptGenerateConfigs; - var config = uiScriptConfigs.FirstOrDefault(c => $"{c.ProjectName}" == itemName); - - if (config != null) - { - UIScriptGeneratorHelper.GenerateAndAttachScript(selectedObject, config); - } - else - { - Debug.LogWarning("Configuration not found for item: " + itemName); - } - } - } -} diff --git a/Editor/UI/GenerateWindow/UIGenerateQuick.cs b/Editor/UI/GenerateWindow/UIGenerateQuick.cs new file mode 100644 index 0000000..1b61511 --- /dev/null +++ b/Editor/UI/GenerateWindow/UIGenerateQuick.cs @@ -0,0 +1,66 @@ +using System; +using System.Linq; +using UnityEditor; +using UnityEditor.SceneManagement; +using UnityEngine; + +namespace AlicizaX.UI.Editor +{ + public static class UIGenerateQuick + { + [MenuItem("GameObject/UI生成绑定", priority = 10)] + public static void ShowWindow() + { + GameObject selectedObject = Selection.gameObjects.FirstOrDefault(); + if (selectedObject == null) + { + Debug.LogError("没有选中物体!"); + return; + } + + var uiScriptConfigs = UIGenerateConfiguration.Instance.UIScriptGenerateConfigs; + if (uiScriptConfigs == null || uiScriptConfigs.Count == 0) + { + Debug.Log("没有UI生成配置 请前往UISettingWindow添加"); + return; + } + + foreach (var config in uiScriptConfigs) + { + if (CheckCanGenerate(selectedObject, config)) + { + UIScriptGeneratorHelper.GenerateAndAttachScript(selectedObject, config); + return; + } + } + + Debug.Log("没有找到符合规则路径的生成配置 请检查!"); + } + + public static bool CheckCanGenerate(GameObject targetObject, UIScriptGenerateData scriptGenerateData) + { + if (targetObject == null || scriptGenerateData == null) return false; + + string assetPath = GetPrefabAssetPath(targetObject); + if (string.IsNullOrEmpty(assetPath) || !assetPath.StartsWith("Assets/", StringComparison.Ordinal)) + return false; // 不在 Assets 下 + + assetPath = assetPath.Replace('\\', '/'); + bool result = assetPath.StartsWith(scriptGenerateData.UIPrefabRootPath, StringComparison.OrdinalIgnoreCase); + return result; + } + + public static string GetPrefabAssetPath(GameObject go) + { + var prefabAsset = PrefabUtility.GetCorrespondingObjectFromSource(go); + if (prefabAsset != null) + return AssetDatabase.GetAssetPath(prefabAsset); + + var prefabStage = PrefabStageUtility.GetCurrentPrefabStage(); + if (prefabStage != null && prefabStage.IsPartOfPrefabContents(go)) + return prefabStage.assetPath; + + return null; + } + } +} diff --git a/Editor/UI/GenerateWindow/UIGenerateEditorWindow.cs.meta b/Editor/UI/GenerateWindow/UIGenerateQuick.cs.meta similarity index 100% rename from Editor/UI/GenerateWindow/UIGenerateEditorWindow.cs.meta rename to Editor/UI/GenerateWindow/UIGenerateQuick.cs.meta diff --git a/Editor/UI/Helper/IUIGeneratorRuleHelper.cs b/Editor/UI/Helper/IUIGeneratorRuleHelper.cs index d3e4c69..03851c3 100644 --- a/Editor/UI/Helper/IUIGeneratorRuleHelper.cs +++ b/Editor/UI/Helper/IUIGeneratorRuleHelper.cs @@ -28,9 +28,8 @@ namespace AlicizaX.UI.Editor public string GetPrivateComponentByNameRule(string regexName, string componentName, EBindType bindType) { string endPrefix = bindType == EBindType.ListCom ? "List" : string.Empty; - int endNameIndex = componentName.IndexOf( - UIGenerateConfiguration.Instance.UIGenerateCommonData.ComCheckEndName, - StringComparison.Ordinal); + var common = UIGenerateConfiguration.Instance.UIGenerateCommonData; + int endNameIndex = componentName.IndexOf(common.ComCheckEndName, StringComparison.Ordinal); string componentSuffix = endNameIndex >= 0 ? componentName.Substring(endNameIndex + 1) : componentName; @@ -39,12 +38,17 @@ namespace AlicizaX.UI.Editor public string GetPublicComponentByNameRule(string variableName) { - return variableName.Substring(1); + if (string.IsNullOrEmpty(variableName)) return variableName; + if (variableName.Length > 1) + return variableName.Substring(1); + return variableName; } public string GetClassGenerateName(GameObject targetObject, UIScriptGenerateData scriptGenerateData) { - return $"{UIGenerateConfiguration.Instance.UIGenerateCommonData.GeneratePrefix}_{targetObject.name}"; + var config = UIGenerateConfiguration.Instance.UIGenerateCommonData; + string prefix = config.GeneratePrefix ?? "ui"; + return $"{prefix}_{targetObject.name}"; } public string GetUIResourceSavePath(GameObject targetObject, UIScriptGenerateData scriptGenerateData) @@ -52,15 +56,11 @@ namespace AlicizaX.UI.Editor if (targetObject == null) return $"\"{nameof(targetObject)}\""; - // 默认返回资源名 string defaultPath = targetObject.name; + string assetPath = UIGenerateQuick.GetPrefabAssetPath(targetObject); + if (string.IsNullOrEmpty(assetPath) || !assetPath.StartsWith("Assets/", StringComparison.Ordinal)) + return defaultPath; - // 获取对应的Prefab资源路径(支持场景Prefab实例 & Prefab编辑模式) - string assetPath = GetPrefabAssetPath(targetObject); - if (string.IsNullOrEmpty(assetPath) || !assetPath.StartsWith("Assets/")) - return defaultPath; // 不在 Assets 下 - - // 统一使用正斜杠 assetPath = assetPath.Replace('\\', '/'); switch (scriptGenerateData.LoadType) @@ -82,11 +82,18 @@ namespace AlicizaX.UI.Editor case EUIResLoadType.AssetBundle: { string bundleRoot = scriptGenerateData.UIPrefabRootPath; // 例如 "Assets/Bundles/UI" - var defaultPackage = YooAsset.Editor.AssetBundleCollectorSettingData.Setting.GetPackage("DefaultPackage"); - if (defaultPackage.EnableAddressable) - return defaultPath; - if (!assetPath.StartsWith(bundleRoot, System.StringComparison.OrdinalIgnoreCase)) + try + { + var defaultPackage = YooAsset.Editor.AssetBundleCollectorSettingData.Setting.GetPackage("DefaultPackage"); + if (defaultPackage != null && defaultPackage.EnableAddressable) + return defaultPath; + } + catch + { + } + + if (!assetPath.StartsWith(bundleRoot, StringComparison.OrdinalIgnoreCase)) { Debug.LogWarning($"[UI生成] 资源 {assetPath} 不在配置的 AssetBundle 根目录下: {bundleRoot}"); return defaultPath; @@ -100,43 +107,25 @@ namespace AlicizaX.UI.Editor } } - /// - /// 获取 GameObject 对应的 Prefab 资源路径,如果是 Prefab 编辑模式也可以获取 - /// - private string GetPrefabAssetPath(GameObject go) - { - var prefabAsset = PrefabUtility.GetCorrespondingObjectFromSource(go); - if (prefabAsset != null) - return AssetDatabase.GetAssetPath(prefabAsset); - - // Prefab 编辑模式 - var prefabStage = PrefabStageUtility.GetCurrentPrefabStage(); - if (prefabStage != null && prefabStage.IsPartOfPrefabContents(go)) - return prefabStage.assetPath; - - return null; - } - - /// - /// 获取 Resources.Load 可用路径(去掉扩展名),如果不在指定的 Resources 根目录返回 null - /// private string GetResourcesRelativePath(string assetPath, string resourcesRoot) { - // 统一正斜杠 + if (string.IsNullOrEmpty(assetPath) || string.IsNullOrEmpty(resourcesRoot)) return null; assetPath = assetPath.Replace('\\', '/'); resourcesRoot = resourcesRoot.Replace('\\', '/'); - if (!assetPath.StartsWith(resourcesRoot, System.StringComparison.OrdinalIgnoreCase)) + if (!assetPath.StartsWith(resourcesRoot, StringComparison.OrdinalIgnoreCase)) return null; - // 获取相对路径 string relPath = assetPath.Substring(resourcesRoot.Length).TrimStart('/'); - return Path.ChangeExtension(relPath, null); // 去掉扩展名 + return Path.ChangeExtension(relPath, null); } - public void WriteUIScriptContent(string className, string scriptContent, UIScriptGenerateData scriptGenerateData) { + if (string.IsNullOrEmpty(className)) throw new ArgumentNullException(nameof(className)); + if (scriptContent == null) throw new ArgumentNullException(nameof(scriptContent)); + if (scriptGenerateData == null) throw new ArgumentNullException(nameof(scriptGenerateData)); + string scriptFolderPath = scriptGenerateData.GenerateHolderCodePath; string scriptFilePath = Path.Combine(scriptFolderPath, className + ".cs"); @@ -147,9 +136,10 @@ namespace AlicizaX.UI.Editor if (File.Exists(scriptFilePath)) { - string oldText = File.ReadAllText(scriptFilePath); - if (oldText.Equals(scriptContent)) + string oldText = File.ReadAllText(scriptFilePath, Encoding.UTF8); + if (oldText.Equals(scriptContent, StringComparison.Ordinal)) { + // 文件未变更:标记并等待脚本 reload 去做附加 EditorPrefs.SetString("Generate", className); UIScriptGeneratorHelper.CheckHasAttach(); return; @@ -163,13 +153,14 @@ namespace AlicizaX.UI.Editor public bool CheckCanGenerate(GameObject targetObject, UIScriptGenerateData scriptGenerateData) { - string assetPath = GetPrefabAssetPath(targetObject); - if (string.IsNullOrEmpty(assetPath) || !assetPath.StartsWith("Assets/")) + if (targetObject == null || scriptGenerateData == null) return false; + + string assetPath = UIGenerateQuick.GetPrefabAssetPath(targetObject); + if (string.IsNullOrEmpty(assetPath) || !assetPath.StartsWith("Assets/", StringComparison.Ordinal)) return false; // 不在 Assets 下 - // 统一使用正斜杠 assetPath = assetPath.Replace('\\', '/'); - bool result = assetPath.StartsWith(scriptGenerateData.UIPrefabRootPath, System.StringComparison.OrdinalIgnoreCase); + bool result = assetPath.StartsWith(scriptGenerateData.UIPrefabRootPath, StringComparison.OrdinalIgnoreCase); if (!result) { Debug.LogWarning($"UI存储位置与配置生成规则不符合 请检查对应配置的UIPrefabRootPath\n[AssetPath]{assetPath}\n[ConfigPath]{scriptGenerateData.UIPrefabRootPath}"); diff --git a/Editor/UI/Helper/UIScriptGeneratorHelper.cs b/Editor/UI/Helper/UIScriptGeneratorHelper.cs index 13a8292..5ff728f 100644 --- a/Editor/UI/Helper/UIScriptGeneratorHelper.cs +++ b/Editor/UI/Helper/UIScriptGeneratorHelper.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; @@ -6,24 +7,23 @@ using System.Reflection; using System.Text; using AlicizaX.UI.Editor; using AlicizaX.UI.Runtime; -using Sirenix.Utilities.Editor; using UnityEngine; using UnityEditor; using UnityEditor.Callbacks; using UnityEditor.SceneManagement; -using UnityEngine.UI; - -public enum EBindType -{ - None, - Widget, - ListCom, -} namespace AlicizaX.UI.Editor { + public enum EBindType + { + None, + Widget, + ListCom, + } + + [Serializable] - class UIBindData + internal class UIBindData { public string Name; public List BindCom; @@ -31,42 +31,51 @@ namespace AlicizaX.UI.Editor public UIBindData(string name, List bindCom, EBindType bindType = EBindType.None) { - Name = name; - BindCom = bindCom; + Name = name ?? throw new ArgumentNullException(nameof(name)); + BindCom = bindCom ?? new List(); BindType = bindType; } public UIBindData(string name, Component bindCom, EBindType bindType = EBindType.None) + : this(name, new List { bindCom }, bindType) { - Name = name; - BindCom = new List() { bindCom }; - BindType = bindType; } } - internal static class UIScriptGeneratorHelper { private static UIGenerateConfiguration _uiGenerateConfiguration; private static IUIGeneratorRuleHelper _uiGeneratorRuleHelper; - /// - /// 设置自定义命名规则助手[4](@ref) - /// + private static List _uiBindDatas = new List(); + private static HashSet _arrayComponents = new HashSet(StringComparer.Ordinal); + public static IUIGeneratorRuleHelper UIGeneratorRuleHelper { get { - if (_uiGeneratorRuleHelper == null || (_uiGeneratorRuleHelper != null && !UIConfiguration.UIScriptGeneratorRuleHelper.Equals(_uiGeneratorRuleHelper.GetType().FullName))) + if (_uiGeneratorRuleHelper == null || + (_uiGeneratorRuleHelper != null && !UIConfiguration.UIScriptGeneratorRuleHelper.Equals(_uiGeneratorRuleHelper.GetType().FullName, StringComparison.Ordinal))) { - Type ruleHelperType = Type.GetType(UIConfiguration.UIScriptGeneratorRuleHelper); + var ruleHelperTypeName = UIConfiguration.UIScriptGeneratorRuleHelper; + if (string.IsNullOrWhiteSpace(ruleHelperTypeName)) + { + Debug.LogError("UIScriptGeneratorHelper: UIScriptGeneratorRuleHelper not configured."); + return null; + } + + Type ruleHelperType = Type.GetType(ruleHelperTypeName); if (ruleHelperType == null) { - Debug.LogError($"UIScriptGeneratorHelper: Could not load UI ScriptGeneratorHelper {UIConfiguration.UIScriptGeneratorRuleHelper}"); + Debug.LogError($"UIScriptGeneratorHelper: Could not load UI ScriptGeneratorHelper {ruleHelperTypeName}"); return null; } _uiGeneratorRuleHelper = Activator.CreateInstance(ruleHelperType) as IUIGeneratorRuleHelper; + if (_uiGeneratorRuleHelper == null) + { + Debug.LogError($"UIScriptGeneratorHelper: Failed to instantiate {ruleHelperTypeName} as IUIGeneratorRuleHelper."); + } } return _uiGeneratorRuleHelper; @@ -88,11 +97,13 @@ namespace AlicizaX.UI.Editor private static string GetVersionType(string uiName) { - foreach (var pair in UIConfiguration.UIElementRegexConfigs) + if (string.IsNullOrEmpty(uiName)) return string.Empty; + foreach (var pair in UIConfiguration.UIElementRegexConfigs ?? Enumerable.Empty()) { + if (string.IsNullOrEmpty(pair?.uiElementRegex)) continue; if (uiName.StartsWith(pair.uiElementRegex, StringComparison.Ordinal)) { - return pair.componentType; + return pair.componentType ?? string.Empty; } } @@ -101,36 +112,49 @@ namespace AlicizaX.UI.Editor private static string[] SplitComponentName(string name) { - bool hasCom = name.Contains(UIConfiguration.UIGenerateCommonData.ComCheckEndName); - if (!hasCom) return null; + if (string.IsNullOrEmpty(name)) return null; - string comStr = name.Substring(0, - name.IndexOf(UIConfiguration.UIGenerateCommonData.ComCheckEndName, StringComparison.Ordinal)); - return comStr.Split(UIConfiguration.UIGenerateCommonData.ComCheckSplitName); + var common = UIConfiguration.UIGenerateCommonData; + if (string.IsNullOrEmpty(common?.ComCheckEndName) || !name.Contains(common.ComCheckEndName)) + return null; + + int endIndex = name.IndexOf(common.ComCheckEndName, StringComparison.Ordinal); + if (endIndex <= 0) return null; + + string comStr = name.Substring(0, endIndex); + string split = common.ComCheckSplitName ?? "#"; + // 使用 string[] 重载并移除空项,防止错误 overload + return comStr.Split(new[] { split }, StringSplitOptions.RemoveEmptyEntries); } private static string GetKeyName(string key, string componentName, EBindType bindType) { - return UIGeneratorRuleHelper.GetPrivateComponentByNameRule(key, componentName, bindType); + var helper = UIGeneratorRuleHelper; + if (helper == null) + throw new InvalidOperationException("UIGeneratorRuleHelper is not configured."); + return helper.GetPrivateComponentByNameRule(key, componentName, bindType); } - private static List _uiBindDatas = new List(); - private static List _arrayComponents = new List(); - private static void GetBindData(Transform root) { + if (root == null) return; + for (int i = 0; i < root.childCount; ++i) { Transform child = root.GetChild(i); + if (child == null) continue; + + // 排除关键字 + if (UIConfiguration.UIGenerateCommonData.ExcludeKeywords != null && + UIConfiguration.UIGenerateCommonData.ExcludeKeywords.Any(k => + !string.IsNullOrEmpty(k) && child.name.IndexOf(k, StringComparison.OrdinalIgnoreCase) >= 0)) + { + continue; + } bool hasWidget = child.GetComponent() != null; - - if (UIConfiguration.UIGenerateCommonData.ExcludeKeywords.Any(k => - child.name.IndexOf(k, StringComparison.OrdinalIgnoreCase) >= 0)) - continue; - - bool isArrayComponent = child.name.StartsWith( - UIConfiguration.UIGenerateCommonData.ArrayComSplitName, StringComparison.Ordinal); + bool isArrayComponent = !string.IsNullOrEmpty(UIConfiguration.UIGenerateCommonData.ArrayComSplitName) && + child.name.StartsWith(UIConfiguration.UIGenerateCommonData.ArrayComSplitName, StringComparison.Ordinal); if (hasWidget) { @@ -138,27 +162,33 @@ namespace AlicizaX.UI.Editor } else if (isArrayComponent) { + // 提取 array 标识文本(例如 "*Item*0" -> "Item") string splitCode = UIConfiguration.UIGenerateCommonData.ArrayComSplitName; + int firstIndex = child.name.IndexOf(splitCode, StringComparison.Ordinal); int lastIndex = child.name.LastIndexOf(splitCode, StringComparison.Ordinal); - string text = child.name.Substring( - child.name.IndexOf(splitCode, StringComparison.Ordinal) + 1, - lastIndex - 1); + if (firstIndex < 0 || lastIndex <= firstIndex) continue; + + // 中间文本 + string text = child.name.Substring(firstIndex + splitCode.Length, lastIndex - (firstIndex + splitCode.Length)); + if (string.IsNullOrEmpty(text)) continue; if (_arrayComponents.Contains(text)) continue; _arrayComponents.Add(text); + // 在同一个父节点下收集包含该 text 的同级节点 List arrayComponents = new List(); for (int j = 0; j < root.childCount; j++) { - if (root.GetChild(j).name.Contains(text, StringComparison.Ordinal)) + Transform sibling = root.GetChild(j); + if (sibling != null && sibling.name.Contains(text, StringComparison.Ordinal)) { - arrayComponents.Add(root.GetChild(j)); + arrayComponents.Add(sibling); } } CollectArrayComponent(arrayComponents, text); } - else if (!isArrayComponent && !hasWidget) + else // 普通组件/进一步递归 { CollectComponent(child); GetBindData(child); @@ -168,46 +198,56 @@ namespace AlicizaX.UI.Editor private static void CollectComponent(Transform node) { + if (node == null) return; + string[] componentArray = SplitComponentName(node.name); - if (componentArray != null) + if (componentArray == null || componentArray.Length == 0) return; + + foreach (var com in componentArray) { - foreach (var com in componentArray) + if (string.IsNullOrEmpty(com)) continue; + string typeName = GetVersionType(com); + if (string.IsNullOrEmpty(typeName)) continue; + + Component component = node.GetComponent(typeName); + if (component != null) { - string typeName = GetVersionType(com); - if (string.IsNullOrEmpty(typeName)) continue; - - Component component = node.GetComponent(typeName); - if (component != null) + string keyName = GetKeyName(com, node.name, EBindType.None); + if (_uiBindDatas.Exists(a => a.Name == keyName)) { - string keyName = GetKeyName(com, node.name, EBindType.None); - if (_uiBindDatas.Exists(a => a.Name == keyName)) - { - Debug.LogError($"Duplicate key found: {keyName}"); - continue; - } + Debug.LogError($"Duplicate key found: {keyName}"); + continue; + } - _uiBindDatas.Add(new UIBindData(keyName, component)); - } - else - { - Debug.LogError($"{node.name} does not have component of type {typeName}"); - } + _uiBindDatas.Add(new UIBindData(keyName, component)); + } + else + { + Debug.LogError($"{node.name} does not have component of type {typeName}"); } } } private static void CollectWidget(Transform node) { - if (node.name.IndexOf(UIConfiguration.UIGenerateCommonData.ComCheckEndName, StringComparison.Ordinal) != -1 && - node.name.IndexOf(UIConfiguration.UIGenerateCommonData.ComCheckSplitName, StringComparison.Ordinal) != -1) + if (node == null) return; + + var common = UIConfiguration.UIGenerateCommonData; + if (node.name.IndexOf(common.ComCheckEndName, StringComparison.Ordinal) != -1 && + node.name.IndexOf(common.ComCheckSplitName, StringComparison.Ordinal) != -1) { Debug.LogWarning($"{node.name} child component cannot contain rule definition symbols!"); return; } UIHolderObjectBase component = node.GetComponent(); - string keyName = GetKeyName(string.Empty, node.name, EBindType.Widget); + if (component == null) + { + Debug.LogError($"{node.name} expected to be a widget but does not have UIHolderObjectBase."); + return; + } + string keyName = GetKeyName(string.Empty, node.name, EBindType.Widget); if (_uiBindDatas.Exists(a => a.Name == keyName)) { Debug.LogError($"Duplicate key found: {keyName}"); @@ -219,103 +259,131 @@ namespace AlicizaX.UI.Editor private static void CollectArrayComponent(List arrayNode, string nodeName) { + if (arrayNode == null || arrayNode.Count == 0) return; + + // 从 nodeName(例如 "*Item*0")取出组件描述部分(即名字中 @End 之前那部分) string[] componentArray = SplitComponentName(nodeName); - arrayNode = arrayNode.OrderBy(s => int.Parse(s.name.Split('*').Last(), - System.Globalization.CultureInfo.InvariantCulture)).ToList(); - List tempBindDatas = new List(componentArray.Length); + // 对 arrayNode 做基于后缀的安全排序:提取 last segment 作为索引(int.TryParse) + string splitCode = UIConfiguration.UIGenerateCommonData.ArrayComSplitName; + var orderedNodes = arrayNode + .Select(n => new { Node = n, RawIndex = ExtractArrayIndex(n.name, splitCode) }) + .OrderBy(x => x.RawIndex.HasValue ? x.RawIndex.Value : int.MaxValue) + .Select(x => x.Node) + .ToList(); - if (componentArray != null) + if (componentArray == null || componentArray.Length == 0) { - int index = 0; - foreach (var com in componentArray) + Debug.LogWarning($"CollectArrayComponent: {nodeName} has no component definitions."); + return; + } + + // 准备临时 bind 列表,每个 componentArray 项对应一个 UIBindData + List tempBindDatas = new List(componentArray.Length); + for (int i = 0; i < componentArray.Length; i++) + { + string keyNamePreview = GetKeyName(componentArray[i], nodeName, EBindType.ListCom); + tempBindDatas.Add(new UIBindData(keyNamePreview, new List(), EBindType.ListCom)); + } + + // 遍历元素并填充 + for (int index = 0; index < componentArray.Length; index++) + { + string com = componentArray[index]; + if (string.IsNullOrEmpty(com)) continue; + + string typeName = GetVersionType(com); + if (string.IsNullOrEmpty(typeName)) continue; + + foreach (var node in orderedNodes) { - foreach (var node in arrayNode) + Component component = node.GetComponent(typeName); + if (component != null) { - string typeName = GetVersionType(com); - if (string.IsNullOrEmpty(typeName)) continue; - - Component component = node.GetComponent(typeName); - if (component != null) - { - string keyName = GetKeyName(com, nodeName, EBindType.ListCom); - if (tempBindDatas.Count - 1 < index) - tempBindDatas.Add(new UIBindData(keyName, new List(), EBindType.ListCom)); - - tempBindDatas[index].BindCom.Add(component); - } - else - { - Debug.LogError($"{node.name} does not have component of type {typeName}"); - } + tempBindDatas[index].BindCom.Add(component); + } + else + { + Debug.LogError($"{node.name} does not have component of type {typeName}"); } - - index++; } } - _uiBindDatas.AddRange(tempBindDatas.ToArray()); + // 将结果合并到全局绑定数据 + _uiBindDatas.AddRange(tempBindDatas); + } + + private static int? ExtractArrayIndex(string nodeName, string splitCode) + { + if (string.IsNullOrEmpty(nodeName) || string.IsNullOrEmpty(splitCode)) return null; + int last = nodeName.LastIndexOf(splitCode, StringComparison.Ordinal); + if (last < 0) return null; + string suffix = nodeName.Substring(last + splitCode.Length); + if (int.TryParse(suffix, System.Globalization.NumberStyles.Integer, System.Globalization.CultureInfo.InvariantCulture, out int idx)) + return idx; + return null; } private static string GetReferenceNamespace() { - StringBuilder referenceNamespaceBuilder = new StringBuilder(); - HashSet namespaces = new HashSet(); + StringBuilder sb = new StringBuilder(); + HashSet namespaces = new HashSet(StringComparer.Ordinal); + + // 基础 namespace namespaces.Add("UnityEngine"); - referenceNamespaceBuilder.Append("using UnityEngine;\n"); + sb.AppendLine("using UnityEngine;"); + + bool needCollectionsGeneric = _uiBindDatas.Any(d => d.BindType == EBindType.ListCom); + if (needCollectionsGeneric) + { + namespaces.Add("System.Collections.Generic"); + sb.AppendLine("using System.Collections.Generic;"); + } foreach (var bindData in _uiBindDatas) { - string nameSpace = bindData.BindCom.FirstOrDefault()?.GetType().Namespace; - if (bindData.BindType == EBindType.ListCom) + var comp = bindData.BindCom?.FirstOrDefault(); + string ns = comp?.GetType().Namespace; + if (!string.IsNullOrEmpty(ns) && !namespaces.Contains(ns)) { - if (!namespaces.Contains("System.Collections.Generic")) - { - referenceNamespaceBuilder.Append("using System.Collections.Generic;\n"); - namespaces.Add("System.Collections.Generic"); - } - } - - if (!string.IsNullOrEmpty(nameSpace) && !namespaces.Contains(nameSpace)) - { - namespaces.Add(nameSpace); - referenceNamespaceBuilder.Append($"using {nameSpace};\n"); + namespaces.Add(ns); + sb.AppendLine($"using {ns};"); } } - return referenceNamespaceBuilder.ToString(); + return sb.ToString(); } private static string GetVariableText(List uiBindDatas) { + if (uiBindDatas == null || uiBindDatas.Count == 0) return string.Empty; + StringBuilder variableTextBuilder = new StringBuilder(); + var helper = UIGeneratorRuleHelper; + if (helper == null) throw new InvalidOperationException("UIGeneratorRuleHelper is not configured."); + foreach (var bindData in uiBindDatas) { + if (bindData == null) continue; string variableName = bindData.Name; - string publicName = UIGeneratorRuleHelper.GetPublicComponentByNameRule(variableName); + if (string.IsNullOrEmpty(variableName)) continue; + + string publicName = helper.GetPublicComponentByNameRule(variableName); variableTextBuilder.Append("\t\t[SerializeField]\n"); - if (bindData.BindType == EBindType.None) + var firstType = bindData.BindCom?.FirstOrDefault()?.GetType(); + string typeName = firstType?.Name ?? "Component"; + + if (bindData.BindType == EBindType.None || bindData.BindType == EBindType.Widget) { - variableTextBuilder.Append( - $"\t\tprivate {bindData.BindCom.FirstOrDefault()?.GetType().Name} {variableName};\n"); - variableTextBuilder.Append( - $"\t\tpublic {bindData.BindCom.FirstOrDefault()?.GetType().Name} {publicName} => {variableName};\n\n"); + variableTextBuilder.Append($"\t\tprivate {typeName} {variableName};\n"); + variableTextBuilder.Append($"\t\tpublic {typeName} {publicName} => {variableName};\n\n"); } else if (bindData.BindType == EBindType.ListCom) { - variableTextBuilder.Append( - $"\t\tprivate {bindData.BindCom.FirstOrDefault()?.GetType().Name}[] {variableName} = " + - $"new {bindData.BindCom.FirstOrDefault()?.GetType().Name}[{bindData.BindCom.Count}];\n"); - variableTextBuilder.Append( - $"\t\tpublic {bindData.BindCom.FirstOrDefault()?.GetType().Name}[] {publicName} => {variableName};\n\n"); - } - else if (bindData.BindType == EBindType.Widget) - { - variableTextBuilder.Append( - $"\t\tprivate {bindData.BindCom.FirstOrDefault()?.GetType().Name} {variableName};\n"); - variableTextBuilder.Append( - $"\t\tpublic {bindData.BindCom.FirstOrDefault()?.GetType().Name} {publicName} => {variableName};\n\n"); + int count = Math.Max(0, bindData.BindCom?.Count ?? 0); + variableTextBuilder.Append($"\t\tprivate {typeName}[] {variableName} = new {typeName}[{count}];\n"); + variableTextBuilder.Append($"\t\tpublic {typeName}[] {publicName} => {variableName};\n\n"); } } @@ -324,37 +392,45 @@ namespace AlicizaX.UI.Editor private static string GenerateScript(string className, string generateNameSpace) { + if (string.IsNullOrEmpty(className)) throw new ArgumentNullException(nameof(className)); + if (string.IsNullOrEmpty(generateNameSpace)) throw new ArgumentNullException(nameof(generateNameSpace)); + StringBuilder scriptBuilder = new StringBuilder(); scriptBuilder.Append(GetReferenceNamespace()); - - scriptBuilder.Append("using AlicizaX.UI.Runtime;\n"); - scriptBuilder.Append($"namespace {generateNameSpace}\n"); - scriptBuilder.Append("{\n"); - scriptBuilder.Append("\t#Attribute#\n"); - scriptBuilder.Append($"\tpublic class {className} : UIHolderObjectBase\n"); - scriptBuilder.Append("\t{\n"); - - scriptBuilder.Append("\t\tpublic const string ResTag = #Tag#;\n"); - - scriptBuilder.Append("\t\t#region Generated by Script Tool\n\n"); + scriptBuilder.AppendLine("using AlicizaX.UI.Runtime;"); + scriptBuilder.AppendLine($"namespace {generateNameSpace}"); + scriptBuilder.AppendLine("{"); + scriptBuilder.AppendLine("\t#Attribute#"); + scriptBuilder.AppendLine($"\tpublic class {className} : UIHolderObjectBase"); + scriptBuilder.AppendLine("\t{"); + scriptBuilder.AppendLine("\t\tpublic const string ResTag = #Tag#;"); + scriptBuilder.AppendLine("\t\t#region Generated by Script Tool\n"); scriptBuilder.Append(GetVariableText(_uiBindDatas)); - scriptBuilder.Append("\n\t\t#endregion\n"); - - scriptBuilder.Append("\t}\n"); - scriptBuilder.Append("}\n"); - + scriptBuilder.AppendLine("\t\t#endregion"); + scriptBuilder.AppendLine("\t}"); + scriptBuilder.AppendLine("}"); return scriptBuilder.ToString(); } public static void GenerateAndAttachScript(GameObject targetObject, UIScriptGenerateData scriptGenerateData) { + if (targetObject == null) throw new ArgumentNullException(nameof(targetObject)); + if (scriptGenerateData == null) throw new ArgumentNullException(nameof(scriptGenerateData)); + if (!PrefabChecker.IsPrefabAsset(targetObject)) { Debug.LogWarning("请将UI界面保存为对应的目录Prefab 在进行代码生成"); return; } - if (!UIGeneratorRuleHelper.CheckCanGenerate(targetObject, scriptGenerateData)) + var ruleHelper = UIGeneratorRuleHelper; + if (ruleHelper == null) + { + Debug.LogError("UIGeneratorRuleHelper not available, abort."); + return; + } + + if (!ruleHelper.CheckCanGenerate(targetObject, scriptGenerateData)) { return; } @@ -363,148 +439,190 @@ namespace AlicizaX.UI.Editor _uiBindDatas.Clear(); _arrayComponents.Clear(); - string className = UIGeneratorRuleHelper.GetClassGenerateName(targetObject, scriptGenerateData); + string className = ruleHelper.GetClassGenerateName(targetObject, scriptGenerateData); + if (string.IsNullOrEmpty(className)) + { + Debug.LogError("Generated className is empty."); + return; + } GetBindData(targetObject.transform); string scriptContent = GenerateScript(className, scriptGenerateData.NameSpace); - string tagName = $"\"{UIGeneratorRuleHelper.GetUIResourceSavePath(targetObject, scriptGenerateData)}\""; + string tagName = $"\"{ruleHelper.GetUIResourceSavePath(targetObject, scriptGenerateData)}\""; string uiAttribute = $"[UIRes({className}.ResTag, EUIResLoadType.{scriptGenerateData.LoadType})]"; scriptContent = scriptContent.Replace("#Attribute#", uiAttribute); scriptContent = scriptContent.Replace("#Tag#", tagName); - UIGeneratorRuleHelper.WriteUIScriptContent(className, scriptContent, scriptGenerateData); + ruleHelper.WriteUIScriptContent(className, scriptContent, scriptGenerateData); } [DidReloadScripts] public static void CheckHasAttach() { - bool has = EditorPrefs.HasKey("Generate"); - if (has) + if (!EditorPrefs.HasKey("Generate")) + return; + + _uiBindDatas.Clear(); + _arrayComponents.Clear(); + + string className = EditorPrefs.GetString("Generate"); + int instanceId = EditorPrefs.GetInt("InstanceId", -1); + + if (instanceId == -1) { - _uiBindDatas.Clear(); - _arrayComponents.Clear(); - string className = EditorPrefs.GetString("Generate"); - int instanceId = EditorPrefs.GetInt("InstanceId", -1); - - if (instanceId == -1) - { - return; - } - - GameObject targetObject = (GameObject)EditorUtility.InstanceIDToObject(instanceId); - - if (!targetObject) - { - Debug.Log("UI script generation attachment object missing!"); - } - + Debug.LogWarning("CheckHasAttach: InstanceId missing."); EditorPrefs.DeleteKey("Generate"); - GetBindData(targetObject.transform); - - AttachScriptToGameObject(targetObject, className); - Debug.Log($"Generate {className} Successfully attached to game object"); + return; } + + GameObject targetObject = EditorUtility.InstanceIDToObject(instanceId) as GameObject; + + if (targetObject == null) + { + Debug.LogWarning("UI script generation attachment object missing!"); + EditorPrefs.DeleteKey("Generate"); + return; + } + + // 重新收集 bind 数据并附加脚本 + GetBindData(targetObject.transform); + + AttachScriptToGameObject(targetObject, className); + Debug.Log($"Generate {className} Successfully attached to game object"); + EditorPrefs.DeleteKey("Generate"); } private static void AttachScriptToGameObject(GameObject targetObject, string scriptClassName) { + if (targetObject == null) throw new ArgumentNullException(nameof(targetObject)); + if (string.IsNullOrEmpty(scriptClassName)) throw new ArgumentNullException(nameof(scriptClassName)); + Type scriptType = null; + foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) { - if (assembly.FullName.Contains("Editor")) continue; - Type[] types = assembly.GetTypes(); + // 跳过典型的编辑器专用程序集(但只在程序集名字结束或包含 ".Editor" 时跳过) + var asmName = assembly.GetName().Name ?? string.Empty; + if (asmName.EndsWith(".Editor", StringComparison.OrdinalIgnoreCase) || + asmName.Equals("UnityEditor", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + Type[] types; + try + { + types = assembly.GetTypes(); + } + catch (ReflectionTypeLoadException e) + { + types = e.Types.Where(t => t != null).ToArray(); + } + foreach (var type in types) { - if (type.IsClass && !type.IsAbstract && type.Name.Contains(scriptClassName, StringComparison.Ordinal)) + if (type == null) continue; + if (!type.IsClass || type.IsAbstract) continue; + if (type.Name.Equals(scriptClassName, StringComparison.Ordinal) || + type.Name.Contains(scriptClassName, StringComparison.Ordinal)) { scriptType = type; + break; } } + + if (scriptType != null) break; } - if (scriptType != null) + if (scriptType == null) { - Component component = targetObject.GetOrAddComponent(scriptType); - FieldInfo[] fields = scriptType.GetFields(BindingFlags.NonPublic | BindingFlags.Instance); + Debug.LogError($"Could not find the class: {scriptClassName}"); + return; + } - foreach (FieldInfo field in fields) + Component component = targetObject.GetOrAddComponent(scriptType); + + FieldInfo[] fields = scriptType.GetFields(BindingFlags.NonPublic | BindingFlags.Instance); + foreach (FieldInfo field in fields) + { + if (string.IsNullOrEmpty(field.Name)) continue; + var componentInObjects = _uiBindDatas.Find(data => data.Name == field.Name)?.BindCom; + if (componentInObjects == null) { - List componentInObjects = _uiBindDatas.Find(data => data.Name == field.Name)?.BindCom; - if (componentInObjects != null) + Debug.LogError($"Field {field.Name} did not find matching component binding"); + continue; + } + + if (field.FieldType.IsArray) + { + Type elementType = field.FieldType.GetElementType(); + if (elementType == null) { - if (field.FieldType.IsArray) + Debug.LogError($"Field {field.Name} has unknown element type."); + continue; + } + + Array array = Array.CreateInstance(elementType, componentInObjects.Count); + for (int i = 0; i < componentInObjects.Count; i++) + { + Component comp = componentInObjects[i]; + if (comp == null) continue; + + if (elementType.IsInstanceOfType(comp)) { - Type elementType = field.FieldType.GetElementType(); - Array array = Array.CreateInstance(elementType, componentInObjects.Count); - - for (int i = 0; i < componentInObjects.Count; i++) - { - Component comp = componentInObjects[i]; - if (elementType.IsInstanceOfType(comp)) - { - array.SetValue(comp, i); - } - else - { - Debug.LogError($"Element {i} type mismatch, expected {elementType.Name}, actual {comp.GetType().Name}"); - } - } - - field.SetValue(component, array); + array.SetValue(comp, i); } else { - if (componentInObjects.Count > 0) - { - if (field.FieldType.IsInstanceOfType(componentInObjects[0])) - { - field.SetValue(component, componentInObjects[0]); - } - else - { - Debug.LogError($"Field {field.Name} type mismatch, cannot assign value"); - } - } + Debug.LogError($"Element {i} type mismatch for field {field.Name}, expected {elementType.Name}, actual {comp.GetType().Name}"); } } - else + + field.SetValue(component, array); + } + else + { + if (componentInObjects.Count > 0) { - Debug.LogError($"Field {field.Name} did not find matching component binding"); + var first = componentInObjects[0]; + if (first == null) continue; + + if (field.FieldType.IsInstanceOfType(first)) + { + field.SetValue(component, first); + } + else + { + Debug.LogError($"Field {field.Name} type mismatch, cannot assign value. Field expects {field.FieldType.Name}, actual {first.GetType().Name}"); + } } } } - else - { - Debug.LogError($"Could not find the class: {scriptClassName}"); - } } public static class PrefabChecker { public static bool IsEditingPrefabAsset(GameObject go) { - // 检查当前是否在Prefab编辑模式 var prefabStage = PrefabStageUtility.GetCurrentPrefabStage(); if (prefabStage == null) return false; - - // 检查选中对象是否属于当前PrefabStage return prefabStage.IsPartOfPrefabContents(go); } public static bool IsPrefabAsset(GameObject go) { - // 普通Asset目录中的Prefab + if (go == null) return false; + var assetType = PrefabUtility.GetPrefabAssetType(go); if (assetType == PrefabAssetType.Regular || assetType == PrefabAssetType.Variant || assetType == PrefabAssetType.Model) return true; - // Prefab编辑模式下 return IsEditingPrefabAsset(go); } }