256 lines
13 KiB
C#
256 lines
13 KiB
C#
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 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>();
|
||
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_PauseResumed(EVT_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;
|
||
}
|
||
}
|
||
}
|
||
}
|