Files
zeling_v2/Assets/_Game/Scripts/Editor/UI/UIPauseScreenScaffold.cs
2026-06-07 11:49:55 +08:00

256 lines
13 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
{
/// <summary>
/// 暂停界面脚手架:生成 / 更新 <c>UI_PauseScreen</c> 预制件 + 默认 <c>UI_PauseMenuConfig</c> 表 + 中英文案。
/// 对齐「Prefab样式+ Config数据+ Theme配色」范式。
///
/// <para>编辑方式:</para>
/// 美术 → 双击 <c>UI_PauseScreen.prefab</c> 改暗化背景/标题/布局/按钮样式(按钮样式见 UI_MainMenu_Button
/// 策划 → 选 <c>UI_PauseMenuConfig.asset</c> 增删/重排/改标签/改动作。
///
/// 菜单BaseGames/UI/控件库/生成暂停界面(预制件 + 默认配置)
/// </summary>
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<string>();
var theme = AssetDatabase.LoadAssetAtPath<UIThemeSO>(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<string> report)
{
// 根:全屏 Overlay CanvassortOrder 50HUD 之上、加载/Splash 之下)
var root = new GameObject(PrefabName,
typeof(RectTransform), typeof(Canvas), typeof(CanvasScaler), typeof(GraphicRaycaster), typeof(CanvasGroup));
var canvas = root.GetComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
canvas.sortingOrder = 50;
var scaler = root.GetComponent<CanvasScaler>();
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
scaler.referenceResolution = new Vector2(1920f, 1080f);
scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight;
scaler.matchWidthOrHeight = 0.5f;
var ctrl = root.AddComponent<DataDrivenPauseMenuController>();
// 暗化背景(铺满,挡住下层点击)
var dimGo = NewUIChild(root.transform, "Dim", out var dimRt);
Stretch(dimRt);
var dimImg = dimGo.AddComponent<Image>();
dimImg.color = new Color(0f, 0f, 0f, 0.72f);
dimImg.raycastTarget = true;
// 主题应用器挂根(每次 OnEnable 应用配色到带 UIThemeRole 的子节点)
var applier = root.AddComponent<UIThemeApplier>();
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<TextMeshProUGUI>();
title.text = "暂停"; title.alignment = TextAlignmentOptions.Center; title.fontSize = 64f;
title.raycastTarget = false;
titleGo.AddComponent<LocalizedText>(); // key 由脚手架下面绑 PAUSE_TITLE
SetString(titleGo.GetComponent<LocalizedText>(), "_key", "PAUSE_TITLE");
SetEnum(titleGo.AddComponent<UIThemeRole>(), "_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<VerticalLayoutGroup>();
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<CanvasGroup>());
var btnPrefab = AssetDatabase.LoadAssetAtPath<GameObject>(BtnPrefabPath);
var btnView = btnPrefab != null ? btnPrefab.GetComponent<MainMenuButtonView>() : null;
if (btnView == null) report.Add($"未找到按钮预制件 {BtnPrefabPath}(请先运行「生成主菜单」)。");
AssignRef(ctrl, "_buttonPrefab", btnView);
// resume 频道与 GameManager 同源:实际资产名 EVT_PauseResumedEVT_ResumeRequested 不存在)
AssignRef(ctrl, "_onResumeRequested", FindAsset<VoidEventChannelSO>("EVT_PauseResumed")
?? FindAsset<VoidEventChannelSO>("EVT_ResumeRequested"));
AssignRef(ctrl, "_onSceneLoadRequest", FindAsset<SceneLoadRequestEventChannelSO>("EVT_SceneLoadRequest"));
string path = $"{PrefabDir}/{PrefabName}.prefab";
PrefabUtility.SaveAsPrefabAsset(root, path);
Object.DestroyImmediate(root);
report.Add(path);
}
// ── 默认配置 ─────────────────────────────────────────────────────────
private static PauseMenuConfigSO EnsureDefaultConfig(List<string> report)
{
string path = $"{ConfigDir}/{ConfigName}.asset";
var cfg = AssetDatabase.LoadAssetAtPath<PauseMenuConfigSO>(path);
bool created = cfg == null;
if (created) { cfg = ScriptableObject.CreateInstance<PauseMenuConfigSO>(); 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<string> report)
{
var zh = new Dictionary<string, string>
{
{ "PAUSE_TITLE", "暂停" },
{ "PAUSE_RESUME", "继续" },
{ "PAUSE_MAIN_MENU", "返回主菜单" },
};
var en = new Dictionary<string, string>
{
{ "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<string, string> 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<T>(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<T>(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;
}
}
}
}