diff --git a/Editor/UI/GenerateWindow/UIGenerateQuick.cs b/Editor/UI/GenerateWindow/UIGenerateQuick.cs index 1b61511..fe16528 100644 --- a/Editor/UI/GenerateWindow/UIGenerateQuick.cs +++ b/Editor/UI/GenerateWindow/UIGenerateQuick.cs @@ -29,7 +29,7 @@ namespace AlicizaX.UI.Editor { if (CheckCanGenerate(selectedObject, config)) { - UIScriptGeneratorHelper.GenerateAndAttachScript(selectedObject, config); + UIScriptGeneratorHelper.GenerateUIBindScript(selectedObject, config); return; } } diff --git a/Editor/UI/Helper/IUIGeneratorRuleHelper.cs b/Editor/UI/Helper/IUIGeneratorRuleHelper.cs index 03851c3..7119020 100644 --- a/Editor/UI/Helper/IUIGeneratorRuleHelper.cs +++ b/Editor/UI/Helper/IUIGeneratorRuleHelper.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using System.IO; +using System.Linq; using System.Text; using AlicizaX.UI.Runtime; using UnityEditor; @@ -21,102 +23,105 @@ namespace AlicizaX.UI.Editor void WriteUIScriptContent(string className, string scriptContent, UIScriptGenerateData scriptGenerateData); bool CheckCanGenerate(GameObject targetObject, UIScriptGenerateData scriptGenerateData); + + string GetReferenceNamespace(List uiBindDatas); + + string GetVariableContent(List uiBindDatas); } - public class DefaultIuiGeneratorRuleHelper : IUIGeneratorRuleHelper + + public class DefaultUIGeneratorRuleHelper : IUIGeneratorRuleHelper { public string GetPrivateComponentByNameRule(string regexName, string componentName, EBindType bindType) { - string endPrefix = bindType == EBindType.ListCom ? "List" : string.Empty; + var endPrefix = bindType == EBindType.ListCom ? "List" : string.Empty; var common = UIGenerateConfiguration.Instance.UIGenerateCommonData; - int endNameIndex = componentName.IndexOf(common.ComCheckEndName, StringComparison.Ordinal); - - string componentSuffix = endNameIndex >= 0 ? componentName.Substring(endNameIndex + 1) : componentName; + var endNameIndex = componentName.IndexOf(common.ComCheckEndName, StringComparison.Ordinal); + var componentSuffix = endNameIndex >= 0 ? componentName.Substring(endNameIndex + 1) : componentName; return $"m{regexName}{componentSuffix}{endPrefix}"; } public string GetPublicComponentByNameRule(string variableName) { if (string.IsNullOrEmpty(variableName)) return variableName; - if (variableName.Length > 1) - return variableName.Substring(1); - return variableName; + return variableName.Length > 1 ? variableName.Substring(1) : variableName; } public string GetClassGenerateName(GameObject targetObject, UIScriptGenerateData scriptGenerateData) { var config = UIGenerateConfiguration.Instance.UIGenerateCommonData; - string prefix = config.GeneratePrefix ?? "ui"; + var prefix = config.GeneratePrefix ?? "ui"; return $"{prefix}_{targetObject.name}"; } public string GetUIResourceSavePath(GameObject targetObject, UIScriptGenerateData scriptGenerateData) { - if (targetObject == null) - return $"\"{nameof(targetObject)}\""; + if (targetObject == null) return $"\"{nameof(targetObject)}\""; + + var defaultPath = targetObject.name; + var assetPath = UIGenerateQuick.GetPrefabAssetPath(targetObject); - string defaultPath = targetObject.name; - string assetPath = UIGenerateQuick.GetPrefabAssetPath(targetObject); if (string.IsNullOrEmpty(assetPath) || !assetPath.StartsWith("Assets/", StringComparison.Ordinal)) return defaultPath; assetPath = assetPath.Replace('\\', '/'); - - switch (scriptGenerateData.LoadType) + return scriptGenerateData.LoadType switch { - case EUIResLoadType.Resources: - { - string resourcesRoot = scriptGenerateData.UIPrefabRootPath; // 例如 "Assets/Resources/UI" - string relPath = GetResourcesRelativePath(assetPath, resourcesRoot); - - if (relPath == null) - { - Debug.LogWarning($"[UI生成] 资源 {assetPath} 不在配置的 Resources 根目录下: {resourcesRoot}"); - return defaultPath; - } - - return relPath; - } - - case EUIResLoadType.AssetBundle: - { - string bundleRoot = scriptGenerateData.UIPrefabRootPath; // 例如 "Assets/Bundles/UI" - - 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; - } - - return Path.ChangeExtension(assetPath, null); - } - - default: - return defaultPath; - } + EUIResLoadType.Resources => GetResourcesPath(assetPath, scriptGenerateData, defaultPath), + EUIResLoadType.AssetBundle => GetAssetBundlePath(assetPath, scriptGenerateData, defaultPath), + _ => defaultPath + }; } - private string GetResourcesRelativePath(string assetPath, string resourcesRoot) + private static string GetResourcesPath(string assetPath, UIScriptGenerateData scriptGenerateData, string defaultPath) + { + var resourcesRoot = scriptGenerateData.UIPrefabRootPath; + var relPath = GetResourcesRelativePath(assetPath, resourcesRoot); + + if (relPath == null) + { + Debug.LogWarning($"[UI生成] 资源 {assetPath} 不在配置的 Resources 根目录下: {resourcesRoot}"); + return defaultPath; + } + + return relPath; + } + + private static string GetAssetBundlePath(string assetPath, UIScriptGenerateData scriptGenerateData, string defaultPath) + { + try + { + var defaultPackage = YooAsset.Editor.AssetBundleCollectorSettingData.Setting.GetPackage("DefaultPackage"); + if (defaultPackage?.EnableAddressable == true) + return defaultPath; + } + catch + { + // 忽略异常,继续处理 + } + + var bundleRoot = scriptGenerateData.UIPrefabRootPath; + if (!assetPath.StartsWith(bundleRoot, StringComparison.OrdinalIgnoreCase)) + { + Debug.LogWarning($"[UI生成] 资源 {assetPath} 不在配置的 AssetBundle 根目录下: {bundleRoot}"); + return defaultPath; + } + + return Path.ChangeExtension(assetPath, null); + } + + private static 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, StringComparison.OrdinalIgnoreCase)) return null; - string relPath = assetPath.Substring(resourcesRoot.Length).TrimStart('/'); + var relPath = assetPath.Substring(resourcesRoot.Length).TrimStart('/'); return Path.ChangeExtension(relPath, null); } @@ -126,47 +131,104 @@ namespace AlicizaX.UI.Editor 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"); + var scriptFolderPath = scriptGenerateData.GenerateHolderCodePath; + var scriptFilePath = Path.Combine(scriptFolderPath, $"{className}.cs"); - if (!Directory.Exists(scriptFolderPath)) - { - Directory.CreateDirectory(scriptFolderPath); - } + Directory.CreateDirectory(scriptFolderPath); - if (File.Exists(scriptFilePath)) + if (File.Exists(scriptFilePath) && IsContentUnchanged(scriptFilePath, scriptContent)) { - string oldText = File.ReadAllText(scriptFilePath, Encoding.UTF8); - if (oldText.Equals(scriptContent, StringComparison.Ordinal)) - { - // 文件未变更:标记并等待脚本 reload 去做附加 - EditorPrefs.SetString("Generate", className); - UIScriptGeneratorHelper.CheckHasAttach(); - return; - } + UIScriptGeneratorHelper.BindUIScript(); + return; } File.WriteAllText(scriptFilePath, scriptContent, Encoding.UTF8); - EditorPrefs.SetString("Generate", className); AssetDatabase.Refresh(); } + private static bool IsContentUnchanged(string filePath, string newContent) + { + var oldText = File.ReadAllText(filePath, Encoding.UTF8); + return oldText.Equals(newContent, StringComparison.Ordinal); + } + public bool CheckCanGenerate(GameObject targetObject, UIScriptGenerateData scriptGenerateData) { if (targetObject == null || scriptGenerateData == null) return false; - string assetPath = UIGenerateQuick.GetPrefabAssetPath(targetObject); + var assetPath = UIGenerateQuick.GetPrefabAssetPath(targetObject); if (string.IsNullOrEmpty(assetPath) || !assetPath.StartsWith("Assets/", StringComparison.Ordinal)) - return false; // 不在 Assets 下 + return false; assetPath = assetPath.Replace('\\', '/'); - bool result = assetPath.StartsWith(scriptGenerateData.UIPrefabRootPath, StringComparison.OrdinalIgnoreCase); - if (!result) + var isValidPath = assetPath.StartsWith(scriptGenerateData.UIPrefabRootPath, StringComparison.OrdinalIgnoreCase); + + if (!isValidPath) { Debug.LogWarning($"UI存储位置与配置生成规则不符合 请检查对应配置的UIPrefabRootPath\n[AssetPath]{assetPath}\n[ConfigPath]{scriptGenerateData.UIPrefabRootPath}"); } - return result; + return isValidPath; + } + + public string GetReferenceNamespace(List uiBindDatas) + { + var namespaceSet = new HashSet(StringComparer.Ordinal) { "UnityEngine" }; + + if (uiBindDatas?.Any(d => d.BindType == EBindType.ListCom) == true) + { + namespaceSet.Add("System.Collections.Generic"); + } + + uiBindDatas? + .Where(bindData => bindData?.BindCom?.FirstOrDefault() != null) + .Select(bindData => bindData.BindCom[0].GetType().Namespace) + .Where(ns => !string.IsNullOrEmpty(ns)) + .ToList() + .ForEach(ns => namespaceSet.Add(ns)); + + return string.Join(Environment.NewLine, namespaceSet.Select(ns => $"using {ns};")); + } + + public string GetVariableContent(List uiBindDatas) + { + if (uiBindDatas == null || uiBindDatas.Count == 0) return string.Empty; + + var variableBuilder = new StringBuilder(); + var variables = uiBindDatas + .Where(b => b != null && !string.IsNullOrEmpty(b.Name)) + .Select(b => GenerateVariableDeclaration(b)) + .Where(declaration => !string.IsNullOrEmpty(declaration)); + + return string.Join("\n\n", variables); + } + + private string GenerateVariableDeclaration(UIBindData bindData) + { + var variableName = bindData.Name; + var publicName = GetPublicComponentByNameRule(variableName); + var firstType = bindData.BindCom?.FirstOrDefault()?.GetType(); + var typeName = firstType?.Name ?? "Component"; + + var declaration = new StringBuilder(); + declaration.AppendLine("\t\t[SerializeField]"); + + switch (bindData.BindType) + { + case EBindType.None: + case EBindType.Widget: + declaration.AppendLine($"\t\tprivate {typeName} {variableName};"); + declaration.Append($"\t\tpublic {typeName} {publicName} => {variableName};"); + break; + + case EBindType.ListCom: + var count = Math.Max(0, bindData.BindCom?.Count ?? 0); + declaration.AppendLine($"\t\tprivate {typeName}[] {variableName} = new {typeName}[{count}];"); + declaration.Append($"\t\tpublic {typeName}[] {publicName} => {variableName};"); + break; + } + + return declaration.ToString(); } } } diff --git a/Editor/UI/Helper/UIScriptGeneratorHelper.cs b/Editor/UI/Helper/UIScriptGeneratorHelper.cs index 10ea07b..3f06bff 100644 --- a/Editor/UI/Helper/UIScriptGeneratorHelper.cs +++ b/Editor/UI/Helper/UIScriptGeneratorHelper.cs @@ -1,16 +1,13 @@ using System; -using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; -using System.Text; -using AlicizaX.UI.Editor; using AlicizaX.UI.Runtime; -using UnityEngine; using UnityEditor; using UnityEditor.Callbacks; using UnityEditor.SceneManagement; +using UnityEngine; namespace AlicizaX.UI.Editor { @@ -18,16 +15,15 @@ namespace AlicizaX.UI.Editor { None, Widget, - ListCom, + ListCom } - [Serializable] - internal class UIBindData + public class UIBindData { - public string Name; - public List BindCom; - public EBindType BindType; + public string Name { get; } + public List BindCom { get; } + public EBindType BindType { get; } public UIBindData(string name, List bindCom, EBindType bindType = EBindType.None) { @@ -46,68 +42,48 @@ namespace AlicizaX.UI.Editor { private static UIGenerateConfiguration _uiGenerateConfiguration; private static IUIGeneratorRuleHelper _uiGeneratorRuleHelper; + private static readonly List _uiBindDatas = new List(); + private static readonly HashSet _arrayComponents = new HashSet(StringComparer.Ordinal); - private static List _uiBindDatas = new List(); - private static HashSet _arrayComponents = new HashSet(StringComparer.Ordinal); + private static IUIGeneratorRuleHelper UIGeneratorRuleHelper => + _uiGeneratorRuleHelper ?? InitializeRuleHelper(); - public static IUIGeneratorRuleHelper UIGeneratorRuleHelper + private static UIGenerateConfiguration UIConfiguration => + _uiGenerateConfiguration ??= UIGenerateConfiguration.Instance; + + private static IUIGeneratorRuleHelper InitializeRuleHelper() { - get + var ruleHelperTypeName = UIConfiguration.UIScriptGeneratorRuleHelper; + if (string.IsNullOrWhiteSpace(ruleHelperTypeName)) { - if (_uiGeneratorRuleHelper == null || - (_uiGeneratorRuleHelper != null && !UIConfiguration.UIScriptGeneratorRuleHelper.Equals(_uiGeneratorRuleHelper.GetType().FullName, StringComparison.Ordinal))) - { - 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 {ruleHelperTypeName}"); - return null; - } - - _uiGeneratorRuleHelper = Activator.CreateInstance(ruleHelperType) as IUIGeneratorRuleHelper; - if (_uiGeneratorRuleHelper == null) - { - Debug.LogError($"UIScriptGeneratorHelper: Failed to instantiate {ruleHelperTypeName} as IUIGeneratorRuleHelper."); - } - } - - return _uiGeneratorRuleHelper; + Debug.LogError("UIScriptGeneratorHelper: UIScriptGeneratorRuleHelper not configured."); + return null; } - } - private static UIGenerateConfiguration UIConfiguration - { - get + var ruleHelperType = Type.GetType(ruleHelperTypeName); + if (ruleHelperType == null) { - if (_uiGenerateConfiguration == null) - { - _uiGenerateConfiguration = UIGenerateConfiguration.Instance; - } - - return _uiGenerateConfiguration; + 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; } private static string GetUIElementComponentType(string uiName) { 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 ?? string.Empty; - } - } - return string.Empty; + return UIConfiguration.UIElementRegexConfigs + ?.Where(pair => !string.IsNullOrEmpty(pair?.uiElementRegex)) + .FirstOrDefault(pair => uiName.StartsWith(pair.uiElementRegex, StringComparison.Ordinal)) + ?.componentType ?? string.Empty; } private static string[] SplitComponentName(string name) @@ -118,43 +94,24 @@ namespace AlicizaX.UI.Editor if (string.IsNullOrEmpty(common?.ComCheckEndName) || !name.Contains(common.ComCheckEndName)) return null; - int endIndex = name.IndexOf(common.ComCheckEndName, StringComparison.Ordinal); + var endIndex = name.IndexOf(common.ComCheckEndName, StringComparison.Ordinal); if (endIndex <= 0) return null; - string comStr = name.Substring(0, endIndex); - string split = common.ComCheckSplitName ?? "#"; - // 使用 string[] 重载并移除空项,防止错误 overload + var comStr = name.Substring(0, endIndex); + var split = common.ComCheckSplitName ?? "#"; + return comStr.Split(new[] { split }, StringSplitOptions.RemoveEmptyEntries); } - private static string GetVariableName(string key, string componentName, EBindType bindType) - { - var helper = UIGeneratorRuleHelper; - if (helper == null) - throw new InvalidOperationException("UIGeneratorRuleHelper is not configured."); - return helper.GetPrivateComponentByNameRule(key, componentName, bindType); - } - - private static void GetBindData(Transform root) + private static void CollectBindData(Transform root) { if (root == null) return; - for (int i = 0; i < root.childCount; ++i) + foreach (Transform child in root.Cast().Where(child => child != null)) { - 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; - bool isArrayComponent = !string.IsNullOrEmpty(UIConfiguration.UIGenerateCommonData.ArrayComSplitName) && - child.name.StartsWith(UIConfiguration.UIGenerateCommonData.ArrayComSplitName, StringComparison.Ordinal); + if (ShouldSkipChild(child)) continue; + var hasWidget = child.GetComponent() != null; + var isArrayComponent = IsArrayComponent(child.name); if (hasWidget) { @@ -162,69 +119,78 @@ 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); - 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++) - { - Transform sibling = root.GetChild(j); - if (sibling != null && sibling.name.Contains(text, StringComparison.Ordinal)) - { - arrayComponents.Add(sibling); - } - } - - CollectArrayComponent(arrayComponents, text); + ProcessArrayComponent(child, root); } - else // 普通组件/进一步递归 + else { CollectComponent(child); - GetBindData(child); + CollectBindData(child); } } } + private static bool ShouldSkipChild(Transform child) + { + var keywords = UIConfiguration.UIGenerateCommonData.ExcludeKeywords; + return keywords?.Any(k => + !string.IsNullOrEmpty(k) && + child.name.IndexOf(k, StringComparison.OrdinalIgnoreCase) >= 0) == true; + } + + private static bool IsArrayComponent(string componentName) + { + var splitName = UIConfiguration.UIGenerateCommonData.ArrayComSplitName; + return !string.IsNullOrEmpty(splitName) && + componentName.StartsWith(splitName, StringComparison.Ordinal); + } + + private static void ProcessArrayComponent(Transform child, Transform root) + { + var splitCode = UIConfiguration.UIGenerateCommonData.ArrayComSplitName; + var firstIndex = child.name.IndexOf(splitCode, StringComparison.Ordinal); + var lastIndex = child.name.LastIndexOf(splitCode, StringComparison.Ordinal); + + if (firstIndex < 0 || lastIndex <= firstIndex) return; + + var text = child.name.Substring(firstIndex + splitCode.Length, lastIndex - (firstIndex + splitCode.Length)); + if (string.IsNullOrEmpty(text) || _arrayComponents.Contains(text)) return; + + _arrayComponents.Add(text); + + var arrayComponents = root.Cast() + .Where(sibling => sibling.name.Contains(text, StringComparison.Ordinal)) + .ToList(); + + CollectArrayComponent(arrayComponents, text); + } + private static void CollectComponent(Transform node) { if (node == null) return; - string[] componentArray = SplitComponentName(node.name); + var componentArray = SplitComponentName(node.name); if (componentArray == null || componentArray.Length == 0) return; - foreach (var com in componentArray) + foreach (var com in componentArray.Where(com => !string.IsNullOrEmpty(com))) { - if (string.IsNullOrEmpty(com)) continue; - string typeName = GetUIElementComponentType(com); + var typeName = GetUIElementComponentType(com); if (string.IsNullOrEmpty(typeName)) continue; - Component component = node.GetComponent(typeName); - if (component != null) - { - string keyName = GetVariableName(com, node.name, EBindType.None); - if (_uiBindDatas.Exists(a => a.Name == keyName)) - { - Debug.LogError($"Duplicate key found: {keyName}"); - continue; - } - - _uiBindDatas.Add(new UIBindData(keyName, component)); - } - else + var component = node.GetComponent(typeName); + if (component == null) { Debug.LogError($"{node.name} does not have component of type {typeName}"); + continue; } + + var keyName = UIGeneratorRuleHelper.GetPrivateComponentByNameRule(com, node.name, EBindType.None); + if (_uiBindDatas.Exists(data => data.Name == keyName)) + { + Debug.LogError($"Duplicate key found: {keyName}"); + continue; + } + + _uiBindDatas.Add(new UIBindData(keyName, component)); } } @@ -233,22 +199,22 @@ namespace AlicizaX.UI.Editor 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) + if (node.name.Contains(common.ComCheckEndName, StringComparison.Ordinal) && + node.name.Contains(common.ComCheckSplitName, StringComparison.Ordinal)) { Debug.LogWarning($"{node.name} child component cannot contain rule definition symbols!"); return; } - UIHolderObjectBase component = node.GetComponent(); + var component = node.GetComponent(); if (component == null) { Debug.LogError($"{node.name} expected to be a widget but does not have UIHolderObjectBase."); return; } - string keyName = GetVariableName(string.Empty, node.name, EBindType.Widget); - if (_uiBindDatas.Exists(a => a.Name == keyName)) + var keyName = UIGeneratorRuleHelper.GetPrivateComponentByNameRule(string.Empty, node.name, EBindType.Widget); + if (_uiBindDatas.Exists(data => data.Name == keyName)) { Debug.LogError($"Duplicate key found: {keyName}"); return; @@ -259,45 +225,54 @@ 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 做基于后缀的安全排序:提取 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 (arrayNode == null || !arrayNode.Any()) return; + var componentArray = SplitComponentName(nodeName); if (componentArray == null || componentArray.Length == 0) { 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 = GetVariableName(componentArray[i], nodeName, EBindType.ListCom); - tempBindDatas.Add(new UIBindData(keyNamePreview, new List(), EBindType.ListCom)); - } + var orderedNodes = OrderArrayNodes(arrayNode); + var tempBindDatas = CreateTempBindDatas(componentArray, nodeName); - // 遍历元素并填充 - for (int index = 0; index < componentArray.Length; index++) + PopulateArrayComponents(componentArray, orderedNodes, tempBindDatas); + _uiBindDatas.AddRange(tempBindDatas); + } + + private static List OrderArrayNodes(List arrayNode) + { + var splitCode = UIConfiguration.UIGenerateCommonData.ArrayComSplitName; + return arrayNode + .Select(node => new { Node = node, Index = ExtractArrayIndex(node.name, splitCode) }) + .OrderBy(x => x.Index ?? int.MaxValue) + .Select(x => x.Node) + .ToList(); + } + + private static List CreateTempBindDatas(string[] componentArray, string nodeName) + { + return componentArray.Select((com, index) => { - string com = componentArray[index]; + var keyName = UIGeneratorRuleHelper.GetPrivateComponentByNameRule(com, nodeName, EBindType.ListCom); + return new UIBindData(keyName, new List(), EBindType.ListCom); + }).ToList(); + } + + private static void PopulateArrayComponents(string[] componentArray, List orderedNodes, List tempBindDatas) + { + for (var index = 0; index < componentArray.Length; index++) + { + var com = componentArray[index]; if (string.IsNullOrEmpty(com)) continue; - string typeName = GetUIElementComponentType(com); + var typeName = GetUIElementComponentType(com); if (string.IsNullOrEmpty(typeName)) continue; foreach (var node in orderedNodes) { - Component component = node.GetComponent(typeName); + var component = node.GetComponent(typeName); if (component != null) { tempBindDatas[index].BindCom.Add(component); @@ -308,82 +283,20 @@ namespace AlicizaX.UI.Editor } } } - - // 将结果合并到全局绑定数据 - _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; + + var lastIndex = nodeName.LastIndexOf(splitCode, StringComparison.Ordinal); + if (lastIndex < 0) return null; + + var suffix = nodeName.Substring(lastIndex + splitCode.Length); + return int.TryParse(suffix, out var idx) ? idx : (int?)null; } - private static string GetReferenceNamespace() - { - HashSet namespaceSet = new HashSet(StringComparer.Ordinal); - - namespaceSet.Add("UnityEngine"); - if (_uiBindDatas.Any(d => d.BindType == EBindType.ListCom)) - namespaceSet.Add("System.Collections.Generic"); - - foreach (var bindData in _uiBindDatas) - { - var ns = bindData.BindCom?.FirstOrDefault()?.GetType().Namespace; - if (!string.IsNullOrEmpty(ns) && !namespaceSet.Contains(ns)) - { - namespaceSet.Add(ns); - } - } - - return new StringBuilder().Append(string.Join(Environment.NewLine, namespaceSet.Select(n => $"using {n};"))).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."); - - var result = string.Join("\n\n", uiBindDatas - .Where(b => b != null && !string.IsNullOrEmpty(b.Name)) - .Select(b => - { - string variableName = b.Name; - string publicName = helper.GetPublicComponentByNameRule(variableName); - var firstType = b.BindCom?.FirstOrDefault()?.GetType(); - string typeName = firstType?.Name ?? "Component"; - - var sb = new StringBuilder(); - sb.AppendLine("\t\t[SerializeField]"); - - if (b.BindType == EBindType.None || b.BindType == EBindType.Widget) - { - sb.AppendLine($"\t\tprivate {typeName} {variableName};"); - sb.Append($"\t\tpublic {typeName} {publicName} => {variableName};"); - } - else if (b.BindType == EBindType.ListCom) - { - int count = Math.Max(0, b.BindCom?.Count ?? 0); - sb.AppendLine($"\t\tprivate {typeName}[] {variableName} = new {typeName}[{count}];"); - sb.Append($"\t\tpublic {typeName}[] {publicName} => {variableName};"); - } - - return sb.ToString(); - })); - - return variableTextBuilder.Append(result).ToString(); - } - - - public static void GenerateAndAttachScript(GameObject targetObject, UIScriptGenerateData scriptGenerateData) + public static void GenerateUIBindScript(GameObject targetObject, UIScriptGenerateData scriptGenerateData) { if (targetObject == null) throw new ArgumentNullException(nameof(targetObject)); if (scriptGenerateData == null) throw new ArgumentNullException(nameof(scriptGenerateData)); @@ -395,61 +308,59 @@ namespace AlicizaX.UI.Editor } var ruleHelper = UIGeneratorRuleHelper; - if (ruleHelper == null) - { - Debug.LogError("UIGeneratorRuleHelper not available, abort."); - return; - } - - if (!ruleHelper.CheckCanGenerate(targetObject, scriptGenerateData)) + if (ruleHelper == null || !ruleHelper.CheckCanGenerate(targetObject, scriptGenerateData)) { return; } - EditorPrefs.SetInt("InstanceId", targetObject.GetInstanceID()); - _uiBindDatas.Clear(); - _arrayComponents.Clear(); + InitializeGenerationContext(targetObject); - string className = ruleHelper.GetClassGenerateName(targetObject, scriptGenerateData); + var className = ruleHelper.GetClassGenerateName(targetObject, scriptGenerateData); if (string.IsNullOrEmpty(className)) { Debug.LogError("Generated className is empty."); return; } - GetBindData(targetObject.transform); + CollectBindData(targetObject.transform); + GenerateScript(targetObject, className, scriptGenerateData, ruleHelper); + } - string uiTemplateText = File.ReadAllText(UIGlobalPath.TemplatePath); - uiTemplateText = uiTemplateText.Replace("#ReferenceNameSpace#", GetReferenceNamespace()); - uiTemplateText = uiTemplateText.Replace("#ClassNameSpace#", scriptGenerateData.NameSpace); - uiTemplateText = uiTemplateText.Replace("#ClassName#", className); - uiTemplateText = uiTemplateText.Replace("#TagName#", ruleHelper.GetUIResourceSavePath(targetObject, scriptGenerateData)); - uiTemplateText = uiTemplateText.Replace("#LoadType#", scriptGenerateData.LoadType.ToString()); - uiTemplateText = uiTemplateText.Replace("#Variable#", GetVariableText(_uiBindDatas)); + private static void InitializeGenerationContext(GameObject targetObject) + { + EditorPrefs.SetInt("InstanceId", targetObject.GetInstanceID()); + _uiBindDatas.Clear(); + _arrayComponents.Clear(); + } - ruleHelper.WriteUIScriptContent(className, uiTemplateText, scriptGenerateData); + private static void GenerateScript(GameObject targetObject, string className, UIScriptGenerateData scriptGenerateData, IUIGeneratorRuleHelper ruleHelper) + { + var templateText = File.ReadAllText(UIGlobalPath.TemplatePath); + var processedText = ProcessTemplateText(targetObject, templateText, className, scriptGenerateData, ruleHelper); + + ruleHelper.WriteUIScriptContent(className, processedText, scriptGenerateData); + EditorPrefs.SetString("Generate", className); + } + + private static string ProcessTemplateText(GameObject targetObject, string templateText, string className, UIScriptGenerateData scriptGenerateData, IUIGeneratorRuleHelper ruleHelper) + { + return templateText + .Replace("#ReferenceNameSpace#", ruleHelper.GetReferenceNamespace(_uiBindDatas)) + .Replace("#ClassNameSpace#", scriptGenerateData.NameSpace) + .Replace("#ClassName#", className) + .Replace("#TagName#", ruleHelper.GetUIResourceSavePath(targetObject, scriptGenerateData)) + .Replace("#LoadType#", scriptGenerateData.LoadType.ToString()) + .Replace("#Variable#", ruleHelper.GetVariableContent(_uiBindDatas)); } [DidReloadScripts] - public static void CheckHasAttach() + public static void BindUIScript() { - if (!EditorPrefs.HasKey("Generate")) - return; - EditorPrefs.DeleteKey("Generate"); - _uiBindDatas.Clear(); - _arrayComponents.Clear(); + if (!EditorPrefs.HasKey("Generate")) return; - string className = EditorPrefs.GetString("Generate"); - int instanceId = EditorPrefs.GetInt("InstanceId", -1); - - if (instanceId == -1) - { - Debug.LogWarning("CheckHasAttach: InstanceId missing."); - - return; - } - - GameObject targetObject = EditorUtility.InstanceIDToObject(instanceId) as GameObject; + var className = EditorPrefs.GetString("Generate"); + var instanceId = EditorPrefs.GetInt("InstanceId", -1); + var targetObject = EditorUtility.InstanceIDToObject(instanceId) as GameObject; if (targetObject == null) { @@ -457,12 +368,20 @@ namespace AlicizaX.UI.Editor return; } + _uiBindDatas.Clear(); + _arrayComponents.Clear(); - GetBindData(targetObject.transform); - + CollectBindData(targetObject.transform); BindScriptPropertyField(targetObject, className); + CleanupContext(); Debug.Log($"Generate {className} Successfully attached to game object"); + } + private static void CleanupContext() + { + EditorPrefs.DeleteKey("Generate"); + _uiBindDatas.Clear(); + _arrayComponents.Clear(); } private static void BindScriptPropertyField(GameObject targetObject, string scriptClassName) @@ -470,98 +389,98 @@ namespace AlicizaX.UI.Editor 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()) - { - // 跳过典型的编辑器专用程序集(但只在程序集名字结束或包含 ".Editor" 时跳过) - var asmName = assembly.GetName().Name ?? string.Empty; - if (asmName.EndsWith(".Editor", StringComparison.OrdinalIgnoreCase) || - asmName.Equals("UnityEditor", StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - Type[] types= assembly.GetTypes(); - - foreach (var type in types) - { - 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; - } - + var scriptType = FindScriptType(scriptClassName); if (scriptType == null) { Debug.LogError($"Could not find the class: {scriptClassName}"); return; } - Component component = targetObject.GetOrAddComponent(scriptType); + var component = targetObject.GetOrAddComponent(scriptType); + BindFieldsToComponents(component, scriptType); + } - FieldInfo[] fields = scriptType.GetFields(BindingFlags.NonPublic | BindingFlags.Instance); - foreach (FieldInfo field in fields) + private static Type FindScriptType(string scriptClassName) + { + return AppDomain.CurrentDomain.GetAssemblies() + .Where(asm => !asm.GetName().Name.EndsWith(".Editor") && + !asm.GetName().Name.Equals("UnityEditor")) + .SelectMany(asm => asm.GetTypes()) + .FirstOrDefault(type => type.IsClass && !type.IsAbstract && + type.Name.Equals(scriptClassName, StringComparison.Ordinal)); + } + + private static void BindFieldsToComponents(Component component, Type scriptType) + { + var fields = scriptType.GetFields(BindingFlags.NonPublic | BindingFlags.Instance); + + foreach (var field in fields.Where(field => !string.IsNullOrEmpty(field.Name))) { - if (string.IsNullOrEmpty(field.Name)) continue; - var componentInObjects = _uiBindDatas.Find(data => data.Name == field.Name)?.BindCom; - if (componentInObjects == null) + var components = _uiBindDatas.Find(data => data.Name == field.Name)?.BindCom; + if (components == null) { Debug.LogError($"Field {field.Name} did not find matching component binding"); continue; } - if (field.FieldType.IsArray) + SetFieldValue(field, components, component); + } + } + + private static void SetFieldValue(FieldInfo field, IReadOnlyList components, Component targetComponent) + { + if (field.FieldType.IsArray) + { + SetArrayFieldValue(field, components, targetComponent); + } + else + { + SetSingleFieldValue(field, components, targetComponent); + } + } + + private static void SetArrayFieldValue(FieldInfo field, IReadOnlyList components, Component targetComponent) + { + var elementType = field.FieldType.GetElementType(); + if (elementType == null) + { + Debug.LogError($"Field {field.Name} has unknown element type."); + return; + } + + var array = Array.CreateInstance(elementType, components.Count); + for (var i = 0; i < components.Count; i++) + { + if (components[i] == null) continue; + + if (elementType.IsInstanceOfType(components[i])) { - Type elementType = field.FieldType.GetElementType(); - if (elementType == null) - { - 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)) - { - array.SetValue(comp, i); - } - else - { - Debug.LogError($"Element {i} type mismatch for field {field.Name}, expected {elementType.Name}, actual {comp.GetType().Name}"); - } - } - - field.SetValue(component, array); + array.SetValue(components[i], i); } else { - if (componentInObjects.Count > 0) - { - 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}"); - } - } + Debug.LogError($"Element {i} type mismatch for field {field.Name}"); } } + + field.SetValue(targetComponent, array); + } + + private static void SetSingleFieldValue(FieldInfo field, IReadOnlyList components, Component targetComponent) + { + if (components.Count == 0) return; + + var firstComponent = components[0]; + if (firstComponent == null) return; + + if (field.FieldType.IsInstanceOfType(firstComponent)) + { + field.SetValue(targetComponent, firstComponent); + } + else + { + Debug.LogError($"Field {field.Name} type mismatch"); + } } public static class PrefabChecker @@ -569,9 +488,7 @@ namespace AlicizaX.UI.Editor public static bool IsEditingPrefabAsset(GameObject go) { var prefabStage = PrefabStageUtility.GetCurrentPrefabStage(); - if (prefabStage == null) - return false; - return prefabStage.IsPartOfPrefabContents(go); + return prefabStage?.IsPartOfPrefabContents(go) == true; } public static bool IsPrefabAsset(GameObject go) @@ -579,12 +496,11 @@ namespace AlicizaX.UI.Editor if (go == null) return false; var assetType = PrefabUtility.GetPrefabAssetType(go); - if (assetType == PrefabAssetType.Regular || - assetType == PrefabAssetType.Variant || - assetType == PrefabAssetType.Model) - return true; + var isRegularPrefab = assetType == PrefabAssetType.Regular || + assetType == PrefabAssetType.Variant || + assetType == PrefabAssetType.Model; - return IsEditingPrefabAsset(go); + return isRegularPrefab || IsEditingPrefabAsset(go); } } } diff --git a/Runtime/UI/Constant/UIMetaRegistry.cs b/Runtime/UI/Constant/UIMetaRegistry.cs index 9f92a54..a431500 100644 --- a/Runtime/UI/Constant/UIMetaRegistry.cs +++ b/Runtime/UI/Constant/UIMetaRegistry.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Linq; +using System.Reflection; using System.Runtime.CompilerServices; namespace AlicizaX.UI.Runtime @@ -38,19 +40,67 @@ namespace AlicizaX.UI.Runtime } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool TryGet(RuntimeTypeHandle handle, out UIMetaInfo info) { - return _typeHandleMap.TryGetValue(handle, out info); + if (_typeHandleMap.TryGetValue(handle, out info)) + return true; + + var t = Type.GetTypeFromHandle(handle); + + if (TryReflectAndRegister(t, out info)) + return true; + + return false; } - public static bool TryGet(string type, out UIMetaInfo info) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool TryGet(string typeName, out UIMetaInfo info) { - RuntimeTypeHandle typeHandle; - if (_stringHandleMap.TryGetValue(type, out typeHandle)) + if (_stringHandleMap.TryGetValue(typeName, out var handle)) + return TryGet(handle, out info); + + + var type = AlicizaX.Utility.Assembly.GetType(typeName); + + if (type != null && TryReflectAndRegister(type, out info)) + return true; + + info = default; + return false; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static bool TryReflectAndRegister(Type uiType, out UIMetaInfo info) + { + Log.Warning($"[UI] UI未注册[{uiType.FullName}] 反射进行缓存"); + Type baseType = uiType; + Type? holderType = baseType.GetGenericArguments()[0]; + + UILayer layer = UILayer.UI; + bool fullScreen = false; + int cacheTime = 0; + + var cad = CustomAttributeData.GetCustomAttributes(uiType) + .FirstOrDefault(a => a.AttributeType.Name == nameof(WindowAttribute)); + + if (cad != null) { + var args = cad.ConstructorArguments; + if (args.Count > 0) layer = (UILayer)(args[0].Value ?? UILayer.UI); + if (args.Count > 1) fullScreen = (bool)(args[1].Value ?? false); + if (args.Count > 2) cacheTime = (int)(args[2].Value ?? 0); } - return _typeHandleMap.TryGetValue(typeHandle, out info); + if (holderType != null) + { + Register(uiType, holderType, layer, fullScreen, cacheTime); + info = _typeHandleMap[uiType.TypeHandle]; + return true; + } + + info = default; + return false; } } } diff --git a/Runtime/UI/Constant/UIResRegistry.cs b/Runtime/UI/Constant/UIResRegistry.cs index 0d9213a..c6e73d8 100644 --- a/Runtime/UI/Constant/UIResRegistry.cs +++ b/Runtime/UI/Constant/UIResRegistry.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Linq; +using System.Reflection; using System.Runtime.CompilerServices; using UnityEditor; using UnityEngine; @@ -32,6 +34,37 @@ namespace AlicizaX.UI.Runtime } public static bool TryGet(RuntimeTypeHandle handle, out UIResInfo info) - => _typeHandleMap.TryGetValue(handle, out info); + { + if (_typeHandleMap.TryGetValue(handle, out info)) + return true; + + var t = Type.GetTypeFromHandle(handle); + + if (TryReflectAndRegister(t, out info)) + return true; + + return false; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static bool TryReflectAndRegister(Type holderType, out UIResInfo info) + { + var cad = CustomAttributeData.GetCustomAttributes(holderType) + .FirstOrDefault(a => a.AttributeType.Name == nameof(UIResAttribute)); + string resLocation = string.Empty; + EUIResLoadType resLoadType = EUIResLoadType.AssetBundle; + if (cad != null) + { + var args = cad.ConstructorArguments; + if (args.Count > 0) resLocation = (string)(args[0].Value ?? string.Empty); + if (args.Count > 1) resLoadType = (EUIResLoadType)(args[1].Value ?? EUIResLoadType.AssetBundle); + Register(holderType, resLocation, resLoadType); + info = _typeHandleMap[holderType.TypeHandle]; + return true; + } + + info = default; + return false; + } } }