using System.Collections.Generic; using UnityEditor; using UnityEngine; using UnityEngine.UI; using TMPro; using BaseGames.UI; using BaseGames.UI.Theme; using BaseGames.Core.Events; using BaseGames.Localization; using BaseGames.Editor.Localization; namespace BaseGames.Editor.UI { /// /// 加载界面脚手架:一键生成 / 更新 UI_LoadingScreen 预制件 + 默认 UI_LoadingConfig 数据表, /// 并补充默认中/英文案。对齐项目「Prefab(样式/美术)+ Config(数据)+ Theme(配色)」范式。 /// /// 生成后策划美术编辑方式: /// /// 美术:双击 UI_LoadingScreen.prefab 进 Prefab Mode,改背景图 / 进度条 sprite / 字体 / 加装饰 / 调布局。 /// 策划:选 UI_LoadingConfig.asset,改提示文案 key、标题 key、加载时长/手感。 /// 配色:改 UI_Theme_Default(带 UIThemeRole 的元素自动套用)。 /// /// /// 菜单:BaseGames/UI/控件库/生成加载界面(预制件 + 默认配置) /// public static class UILoadingScreenScaffold { private const string PrefabDir = "Assets/_Game/Prefabs/UI"; private const string ConfigDir = "Assets/_Game/Data/UI"; private const string ThemePath = "Assets/_Game/Data/UI/Themes/UI_Theme_Default.asset"; private const string PrefabName = "UI_LoadingScreen"; private const string ConfigName = "UI_LoadingConfig"; [MenuItem("BaseGames/UI/控件库/生成加载界面(预制件 + 默认配置)")] public static void GeneratePrefab() { EnsureFolder(PrefabDir); EnsureFolder(ConfigDir); var report = new List(); var theme = AssetDatabase.LoadAssetAtPath(ThemePath); var config = EnsureDefaultConfig(report); SeedLocalization(report); BuildPrefab(theme, config, report); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); var sb = new System.Text.StringBuilder("[UILoadingScreen] 加载界面已生成/更新:\n"); foreach (var r in report) sb.AppendLine(" • " + r); sb.AppendLine("用法:重跑「BaseGames/Scene/Setup/Scaffold Persistent Scene」把预制件实例化进 Persistent。"); sb.AppendLine("美术改样式 → 编辑 UI_LoadingScreen 预制件;策划改内容 → 编辑 UI_LoadingConfig;配色 → UI_Theme_Default。"); Debug.Log(sb.ToString()); } // ── 预制件构建 ─────────────────────────────────────────────────────── private static void BuildPrefab(UIThemeSO theme, LoadingScreenConfigSO config, List report) { // 根:全屏 Overlay Canvas(常驻 active;LoadingScreenManager 挂此处,靠保持 active 才能订阅事件) var root = new GameObject(PrefabName, typeof(RectTransform), typeof(Canvas), typeof(CanvasScaler), typeof(GraphicRaycaster)); var canvas = root.GetComponent(); canvas.renderMode = RenderMode.ScreenSpaceOverlay; canvas.sortingOrder = 99; var scaler = root.GetComponent(); scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize; scaler.referenceResolution = new Vector2(1920f, 1080f); scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight; scaler.matchWidthOrHeight = 0.5f; var mgr = root.AddComponent(); // LoadingRoot:铺满全屏,初始 inactive(由管理器 Show/Hide 切换)。 // 主题应用器挂在此处:每次 Show(SetActive true)时 OnEnable 应用主题配色。 var loadingRootGo = NewUIChild(root.transform, "LoadingRoot", out var loadingRootRt); Stretch(loadingRootRt); var applier = loadingRootGo.AddComponent(); if (theme != null) AssignRef(applier, "_theme", theme); // Background(铺满):美术可换 sprite,或复制多个变体作随机背景池 var bgGo = NewUIChild(loadingRootGo.transform, "Background", out var bgRt); Stretch(bgRt); var bgImg = bgGo.AddComponent(); bgImg.color = new Color(0.04f, 0.05f, 0.07f, 1f); SetEnum(bgGo.AddComponent(), "_kind", (int)UIThemeRoleKind.Graphic_Background); // ProgressBarTrack(底部居中)+ ProgressBarFill var trackGo = NewUIChild(loadingRootGo.transform, "ProgressBarTrack", out var trackRt); SetRect(trackRt, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, 90f), new Vector2(760f, 14f)); var trackImg = trackGo.AddComponent(); trackImg.sprite = Standard(); trackImg.type = Image.Type.Sliced; trackImg.color = new Color(1f, 1f, 1f, 0.12f); trackImg.raycastTarget = false; var fillGo = NewUIChild(trackGo.transform, "ProgressBarFill", out var fillRt); Stretch(fillRt); var fillImg = fillGo.AddComponent(); fillImg.sprite = Standard(); fillImg.type = Image.Type.Filled; fillImg.fillMethod = Image.FillMethod.Horizontal; fillImg.fillOrigin = (int)Image.OriginHorizontal.Left; fillImg.fillAmount = 0f; fillImg.color = new Color(0.85f, 0.80f, 0.55f, 1f); fillImg.raycastTarget = false; SetEnum(fillGo.AddComponent(), "_kind", (int)UIThemeRoleKind.Graphic_Accent); // Title(居中偏上) var titleGo = NewUIChild(loadingRootGo.transform, "Title", out var titleRt); SetRect(titleRt, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0f, 60f), new Vector2(1200f, 80f)); var title = titleGo.AddComponent(); title.text = "Loading"; title.alignment = TextAlignmentOptions.Center; title.fontSize = 56f; title.raycastTarget = false; SetEnum(titleGo.AddComponent(), "_kind", (int)UIThemeRoleKind.Text_Header); // TipText(进度条上方) var tipGo = NewUIChild(loadingRootGo.transform, "TipText", out var tipRt); SetRect(tipRt, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, 130f), new Vector2(1200f, 48f)); var tip = tipGo.AddComponent(); tip.text = ""; tip.alignment = TextAlignmentOptions.Center; tip.fontSize = 28f; tip.raycastTarget = false; SetEnum(tipGo.AddComponent(), "_kind", (int)UIThemeRoleKind.Text_Secondary); // 绑定管理器视图引用 + 数据/事件(让预制件自足,掉哪都能用) AssignRef(mgr, "_loadingRoot", loadingRootGo); AssignRef(mgr, "_progressFill", fillImg); AssignRef(mgr, "_titleText", title); AssignRef(mgr, "_tipText", tip); AssignArray(mgr, "_backgroundArts", new Object[] { bgImg }); AssignRef(mgr, "_config", config); AssignRef(mgr, "_onLoadingStarted", FindAsset("EVT_LoadingStarted")); AssignRef(mgr, "_onLoadingComplete", FindAsset("EVT_LoadingComplete")); AssignRef(mgr, "_onLoadingProgressUpdated", FindAsset("EVT_LoadingProgressUpdated")); // 关键:Canvas 根保持 active,仅 LoadingRoot 初始隐藏(否则管理器被自己关闭、永不订阅事件) loadingRootGo.SetActive(false); string path = $"{PrefabDir}/{PrefabName}.prefab"; PrefabUtility.SaveAsPrefabAsset(root, path); Object.DestroyImmediate(root); report.Add(path); } // ── 默认配置资产 ───────────────────────────────────────────────────── private static LoadingScreenConfigSO EnsureDefaultConfig(List report) { string path = $"{ConfigDir}/{ConfigName}.asset"; var cfg = AssetDatabase.LoadAssetAtPath(path); bool created = cfg == null; if (created) { cfg = ScriptableObject.CreateInstance(); AssetDatabase.CreateAsset(cfg, path); } if (created) // 仅新建时填默认,避免覆盖策划已有编辑 { var so = new SerializedObject(cfg); so.FindProperty("_titleKey").stringValue = "LOADING_TITLE"; var tips = so.FindProperty("_tipKeys"); string[] keys = { "LOADING_TIP_EXPLORE", "LOADING_TIP_SAVE", "LOADING_TIP_COMBAT" }; tips.arraySize = keys.Length; for (int i = 0; i < keys.Length; i++) tips.GetArrayElementAtIndex(i).stringValue = keys[i]; so.ApplyModifiedPropertiesWithoutUndo(); report.Add($"{path}(默认标题/提示 key)"); } return cfg; } // ── 默认本地化文案(中/英;已存在的 key 不覆盖)───────────────────────── private static void SeedLocalization(List report) { var zh = new Dictionary { { "LOADING_TITLE", "加载中…" }, { "LOADING_TIP_EXPLORE", "多探索每个角落,常有隐藏的道路与收集物。" }, { "LOADING_TIP_SAVE", "在存档点休息可保存进度并恢复状态。" }, { "LOADING_TIP_COMBAT", "把握闪避与格挡的时机,是战斗的关键。" }, }; var en = new Dictionary { { "LOADING_TITLE", "Loading…" }, { "LOADING_TIP_EXPLORE", "Explore every corner — hidden paths and collectibles await." }, { "LOADING_TIP_SAVE", "Rest at save points to save progress and restore yourself." }, { "LOADING_TIP_COMBAT", "Timing your dodge and parry is the key to combat." }, }; int added = MergeWriteUI(Language.ChineseSimplified, zh) + MergeWriteUI(Language.English, en); report.Add($"本地化新增 {added} 条(已存在的不覆盖;可用「BaseGames/Localization/表格编辑器」补译)。"); } private static int MergeWriteUI(Language lang, Dictionary kv) { var dict = LocalizationFileIO.Read(lang, LocalizationTable.UI); int added = 0; foreach (var p in kv) if (!dict.ContainsKey(p.Key)) { dict[p.Key] = p.Value; added++; } if (added > 0) LocalizationFileIO.Write(lang, LocalizationTable.UI, dict); return added; } // ── 通用助手 ───────────────────────────────────────────────────────── private static GameObject NewUIChild(Transform parent, string name, out RectTransform rt) { var go = new GameObject(name, typeof(RectTransform)); rt = (RectTransform)go.transform; rt.SetParent(parent, false); return go; } private static void Stretch(RectTransform rt) { rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.one; rt.offsetMin = Vector2.zero; rt.offsetMax = Vector2.zero; } private static void SetRect(RectTransform rt, Vector2 aMin, Vector2 aMax, Vector2 pivot, Vector2 anchoredPos, Vector2 size) { rt.anchorMin = aMin; rt.anchorMax = aMax; rt.pivot = pivot; rt.anchoredPosition = anchoredPos; rt.sizeDelta = size; } private static void SetEnum(Component c, string prop, int value) { var so = new SerializedObject(c); var p = so.FindProperty(prop); if (p != null) { p.enumValueIndex = value; so.ApplyModifiedPropertiesWithoutUndo(); } } private static void AssignRef(Object target, string prop, Object value) { var so = new SerializedObject(target); var p = so.FindProperty(prop); if (p == null) { Debug.LogWarning($"[UILoadingScreen] 未找到属性 {target.GetType().Name}.{prop}"); return; } p.objectReferenceValue = value; so.ApplyModifiedPropertiesWithoutUndo(); } private static void AssignArray(Object target, string prop, Object[] values) { var so = new SerializedObject(target); var p = so.FindProperty(prop); if (p == null || !p.isArray) return; p.arraySize = values.Length; for (int i = 0; i < values.Length; i++) p.GetArrayElementAtIndex(i).objectReferenceValue = values[i]; so.ApplyModifiedPropertiesWithoutUndo(); } private static T FindAsset(string name) where T : Object { foreach (var guid in AssetDatabase.FindAssets($"{name} t:{typeof(T).Name}")) { var path = AssetDatabase.GUIDToAssetPath(guid); var asset = AssetDatabase.LoadAssetAtPath(path); if (asset != null && asset.name == name) return asset; } return null; } private static Sprite Standard() => AssetDatabase.GetBuiltinExtraResource("UI/Skin/UISprite.psd"); private static void EnsureFolder(string dir) { string[] parts = dir.Split('/'); string cur = parts[0]; for (int i = 1; i < parts.Length; i++) { string next = $"{cur}/{parts[i]}"; if (!AssetDatabase.IsValidFolder(next)) AssetDatabase.CreateFolder(cur, parts[i]); cur = next; } } } }