using System.Collections.Generic; using UnityEditor; using UnityEngine; using UnityEngine.UI; using TMPro; using BaseGames.UI; using BaseGames.UI.MainMenu; using BaseGames.UI.Theme; using BaseGames.Core.Events; using BaseGames.Localization; using BaseGames.Editor.Localization; namespace BaseGames.Editor.UI { /// /// 暂停界面脚手架:生成 / 更新 UI_PauseScreen 预制件 + 默认 UI_PauseMenuConfig 表 + 中英文案。 /// 对齐「Prefab(样式)+ Config(数据)+ Theme(配色)」范式。 /// /// 编辑方式: /// 美术 → 双击 UI_PauseScreen.prefab 改暗化背景/标题/布局/按钮样式(按钮样式见 UI_MainMenu_Button); /// 策划 → 选 UI_PauseMenuConfig.asset 增删/重排/改标签/改动作。 /// /// 菜单:BaseGames/UI/控件库/生成暂停界面(预制件 + 默认配置) /// public static class UIPauseScreenScaffold { private const string PrefabDir = "Assets/_Game/Prefabs/UI"; private const string ControlsDir = "Assets/_Game/Prefabs/UI/Controls"; 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_PauseScreen"; private const string ConfigName = "UI_PauseMenuConfig"; private const string BtnPrefabPath = "Assets/_Game/Prefabs/UI/Controls/UI_MainMenu_Button.prefab"; [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("[UIPauseScreen] 暂停界面已生成/更新:\n"); foreach (var r in report) sb.AppendLine(" • " + r); sb.AppendLine("用法:重跑「BaseGames/Scene/Setup/Scaffold Persistent Scene」实例化进 Persistent(作为 PanelId.Pause 面板)。"); Debug.Log(sb.ToString()); } // ── 预制件构建 ─────────────────────────────────────────────────────── private static void BuildPrefab(UIThemeSO theme, PauseMenuConfigSO config, List report) { // 根:全屏 Overlay Canvas(sortOrder 50:HUD 之上、加载/Splash 之下) var root = new GameObject(PrefabName, typeof(RectTransform), typeof(Canvas), typeof(CanvasScaler), typeof(GraphicRaycaster), typeof(CanvasGroup)); var canvas = root.GetComponent(); canvas.renderMode = RenderMode.ScreenSpaceOverlay; canvas.sortingOrder = 50; 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 ctrl = root.AddComponent(); // 暗化背景(铺满,挡住下层点击) var dimGo = NewUIChild(root.transform, "Dim", out var dimRt); Stretch(dimRt); var dimImg = dimGo.AddComponent(); dimImg.color = new Color(0f, 0f, 0f, 0.72f); dimImg.raycastTarget = true; // 主题应用器挂根(每次 OnEnable 应用配色到带 UIThemeRole 的子节点) var applier = root.AddComponent(); if (theme != null) AssignRef(applier, "_theme", theme); // 标题 var titleGo = NewUIChild(root.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, 220f), new Vector2(800f, 90f)); var title = titleGo.AddComponent(); title.text = "暂停"; title.alignment = TextAlignmentOptions.Center; title.fontSize = 64f; title.raycastTarget = false; titleGo.AddComponent(); // key 由脚手架下面绑 PAUSE_TITLE SetString(titleGo.GetComponent(), "_key", "PAUSE_TITLE"); SetEnum(titleGo.AddComponent(), "_kind", (int)UIThemeRoleKind.Text_Header); // 按钮容器(居中竖排;运行时据表生成按钮) var menuGo = NewUIChild(root.transform, "MenuPanel", out var menuRt); SetRect(menuRt, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0f, -40f), new Vector2(520f, 360f)); var vlg = menuGo.AddComponent(); vlg.spacing = 12f; vlg.childAlignment = TextAnchor.MiddleCenter; vlg.childControlWidth = true; vlg.childControlHeight = true; vlg.childForceExpandWidth = true; vlg.childForceExpandHeight = false; // 绑定控制器 AssignRef(ctrl, "_config", config); AssignRef(ctrl, "_container", menuRt); AssignRef(ctrl, "_canvasGroup", root.GetComponent()); var btnPrefab = AssetDatabase.LoadAssetAtPath(BtnPrefabPath); var btnView = btnPrefab != null ? btnPrefab.GetComponent() : null; if (btnView == null) report.Add($"未找到按钮预制件 {BtnPrefabPath}(请先运行「生成主菜单」)。"); AssignRef(ctrl, "_buttonPrefab", btnView); // resume 频道与 GameManager 同源:实际资产名 EVT_PauseResumed(EVT_ResumeRequested 不存在) AssignRef(ctrl, "_onResumeRequested", FindAsset("EVT_PauseResumed") ?? FindAsset("EVT_ResumeRequested")); AssignRef(ctrl, "_onSceneLoadRequest", FindAsset("EVT_SceneLoadRequest")); string path = $"{PrefabDir}/{PrefabName}.prefab"; PrefabUtility.SaveAsPrefabAsset(root, path); Object.DestroyImmediate(root); report.Add(path); } // ── 默认配置 ───────────────────────────────────────────────────────── private static PauseMenuConfigSO 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 items = new (string key, PauseMenuAction a)[] { ("PAUSE_RESUME", PauseMenuAction.Resume), ("MENU_SETTINGS", PauseMenuAction.OpenSettings), ("PAUSE_MAIN_MENU", PauseMenuAction.ReturnToMainMenu), ("MENU_QUIT", PauseMenuAction.Quit), }; var so = new SerializedObject(cfg); var prop = so.FindProperty("_items"); prop.arraySize = items.Length; for (int i = 0; i < items.Length; i++) { var el = prop.GetArrayElementAtIndex(i); el.FindPropertyRelative("labelKey").stringValue = items[i].key; el.FindPropertyRelative("action").enumValueIndex = (int)items[i].a; el.FindPropertyRelative("sceneKey").stringValue = ""; } so.ApplyModifiedPropertiesWithoutUndo(); report.Add($"{path}(默认 {items.Length} 项)"); } return cfg; } // ── 默认本地化(已存在的 key 不覆盖)──────────────────────────────────── private static void SeedLocalization(List report) { var zh = new Dictionary { { "PAUSE_TITLE", "暂停" }, { "PAUSE_RESUME", "继续" }, { "PAUSE_MAIN_MENU", "返回主菜单" }, }; var en = new Dictionary { { "PAUSE_TITLE", "Paused" }, { "PAUSE_RESUME", "Resume" }, { "PAUSE_MAIN_MENU", "Return to Title" }, }; int added = MergeWriteUI(Language.ChineseSimplified, zh) + MergeWriteUI(Language.English, en); report.Add($"本地化新增 {added} 条(已存在不覆盖;MENU_SETTINGS/MENU_QUIT 复用主菜单译文)。"); } 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 SetString(Component c, string prop, string value) { if (c == null) return; var so = new SerializedObject(c); var p = so.FindProperty(prop); if (p != null) { p.stringValue = 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($"[UIPauseScreen] 未找到属性 {target.GetType().Name}.{prop}"); return; } p.objectReferenceValue = value; 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 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; } } } }