Files
zeling_v2/Assets/_Game/Scripts/Editor/UI/UIControlLibraryScaffold.cs
2026-06-06 09:00:11 +08:00

760 lines
38 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.Settings;
using BaseGames.UI.MainMenu;
using BaseGames.UI.Theme;
using BaseGames.Localization;
using BaseGames.Editor.Localization;
namespace BaseGames.Editor.UI
{
/// <summary>
/// UI 通用控件库脚手架。
///
/// 一键生成 / 更新 themed 控件预制件UIButton / UISelectableRow / UISlider / UIDropdown /
/// UISimplePanel / UITabGroup并提供"向选中节点放置控件"菜单,使策划可拖拽即用。
///
/// 预制件输出目录Assets/_Game/Prefabs/UI/Controls/(命名前缀 UI_符合 AssetFolderSpec
/// 构建方式复用 Unity/TMP 的 DefaultControls 标准层级,再挂上本项目的封装组件与 UIThemeRole 标记。
///
/// 菜单BaseGames/UI/控件库/...
/// </summary>
public static class UIControlLibraryScaffold
{
private const string ControlsDir = "Assets/_Game/Prefabs/UI/Controls";
private const string ThemeDir = "Assets/_Game/Data/UI/Themes";
private const string ThemeName = "UI_Theme_Default";
// 生成期间的默认主题GenerateAll 开头确保存在;各 Build 方法读取)
private static UIThemeSO s_theme;
// 预制件文件名
private const string PfButton = "UI_Control_Button";
private const string PfRow = "UI_Control_SelectableRow";
private const string PfSlider = "UI_Control_Slider";
private const string PfDropdown = "UI_Control_Dropdown";
private const string PfPanel = "UI_Control_Panel";
private const string PfTabBar = "UI_Control_TabBar";
// ── 生成 ─────────────────────────────────────────────────────────────
[MenuItem("BaseGames/UI/控件库/生成或更新控件预制件")]
public static void GenerateAll()
{
EnsureFolder(ControlsDir);
var report = new List<string>();
s_theme = EnsureDefaultTheme(report);
BuildButton(report);
BuildSelectableRow(report);
BuildSlider(report);
BuildDropdown(report);
BuildPanel(report);
BuildTabBar(report);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
var sb = new System.Text.StringBuilder("[UIControlLibrary] 控件预制件已生成/更新:\n");
foreach (var r in report) sb.AppendLine(" • " + r);
sb.AppendLine($"目录:{ControlsDir}/(占位配色,可挂 UIThemeApplier + UIThemeSO 统一主题)");
Debug.Log(sb.ToString());
}
// ── 放置 ─────────────────────────────────────────────────────────────
[MenuItem("BaseGames/UI/控件库/向选中节点放置 ▸ Button")]
private static void PlaceButton() => Place(PfButton);
[MenuItem("BaseGames/UI/控件库/向选中节点放置 ▸ SelectableRow")]
private static void PlaceRow() => Place(PfRow);
[MenuItem("BaseGames/UI/控件库/向选中节点放置 ▸ Slider")]
private static void PlaceSlider() => Place(PfSlider);
[MenuItem("BaseGames/UI/控件库/向选中节点放置 ▸ Dropdown")]
private static void PlaceDropdown() => Place(PfDropdown);
[MenuItem("BaseGames/UI/控件库/向选中节点放置 ▸ Panel")]
private static void PlacePanel() => Place(PfPanel);
[MenuItem("BaseGames/UI/控件库/向选中节点放置 ▸ TabBar")]
private static void PlaceTabBar() => Place(PfTabBar);
private static void Place(string prefabName)
{
string path = $"{ControlsDir}/{prefabName}.prefab";
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(path);
if (prefab == null)
{
EditorUtility.DisplayDialog("控件库",
$"未找到预制件:{path}\n请先执行「生成或更新控件预制件」。", "确定");
return;
}
// 父节点:当前选中的 Transform否则活动场景中的第一个 Canvas
Transform parent = Selection.activeTransform;
if (parent == null)
{
var canvas = Object.FindObjectOfType<Canvas>();
parent = canvas != null ? canvas.transform : null;
}
var instance = (GameObject)PrefabUtility.InstantiatePrefab(prefab, parent);
if (instance == null) return;
Undo.RegisterCreatedObjectUndo(instance, $"Place {prefabName}");
Selection.activeGameObject = instance;
EditorGUIUtility.PingObject(instance);
}
// ── 默认主题 ─────────────────────────────────────────────────────────
private static UIThemeSO EnsureDefaultTheme(List<string> report)
{
EnsureFolder(ThemeDir);
string path = $"{ThemeDir}/{ThemeName}.asset";
var theme = AssetDatabase.LoadAssetAtPath<UIThemeSO>(path);
if (theme == null)
{
theme = ScriptableObject.CreateInstance<UIThemeSO>(); // 字段含默认配色
AssetDatabase.CreateAsset(theme, path);
report.Add($"{path}(默认主题,可调色板/字体)");
}
return theme;
}
// ── 各控件构建 ───────────────────────────────────────────────────────
private static void BuildButton(List<string> report)
{
var go = TMP_DefaultControls.CreateButton(TmpResources());
go.name = PfButton;
Size(go, 200, 48);
var uiBtn = GetOrAdd<UIButton>(go);
if (s_theme != null) AssignRef(uiBtn, "_theme", s_theme);
SetEnum(GetOrAdd<UIThemeRole>(go), "_kind", (int)UIThemeRoleKind.Button);
var label = go.GetComponentInChildren<TMP_Text>();
if (label != null)
{
label.text = "Button";
SetEnum(GetOrAdd<UIThemeRole>(label.gameObject), "_kind", (int)UIThemeRoleKind.Text_Primary);
}
SaveAsPrefab(go, PfButton, report);
}
private static void BuildSelectableRow(List<string> report)
{
var go = NewUI(PfRow, 320, 48);
var bg = go.AddComponent<Image>();
bg.sprite = Standard(); bg.type = Image.Type.Sliced;
bg.color = new Color(1f, 1f, 1f, 0.06f);
var btn = go.AddComponent<Button>();
btn.targetGraphic = bg;
var row = go.AddComponent<UISelectableRow>();
// 选中高亮(铺底,置于内容之下,默认隐藏)
var highlight = NewUIChild(go.transform, "SelectedHighlight", out var hlRt);
Stretch(hlRt);
var hlImg = highlight.AddComponent<Image>();
hlImg.color = new Color(0.20f, 0.65f, 1f, 0.35f);
SetEnum(highlight.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Graphic_Accent);
highlight.SetActive(false);
// 图标
var iconGo = NewUIChild(go.transform, "Icon", out var iconRt);
iconRt.anchorMin = new Vector2(0, 0.5f); iconRt.anchorMax = new Vector2(0, 0.5f);
iconRt.pivot = new Vector2(0, 0.5f); iconRt.anchoredPosition = new Vector2(10, 0);
iconRt.sizeDelta = new Vector2(32, 32);
var icon = iconGo.AddComponent<Image>(); icon.enabled = false;
// 标签
var labelGo = NewUIChild(go.transform, "Label", out var labelRt);
labelRt.anchorMin = new Vector2(0, 0); labelRt.anchorMax = new Vector2(1, 1);
labelRt.offsetMin = new Vector2(52, 0); labelRt.offsetMax = new Vector2(-12, 0);
var label = labelGo.AddComponent<TextMeshProUGUI>();
label.text = "Row"; label.alignment = TextAlignmentOptions.MidlineLeft; label.fontSize = 20;
SetEnum(labelGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Text_Primary);
AssignRef(row, "_button", btn);
AssignRef(row, "_label", label);
AssignRef(row, "_icon", icon);
AssignRef(row, "_selectedHighlight", highlight);
SaveAsPrefab(go, PfRow, report);
}
private static void BuildSlider(List<string> report)
{
var go = DefaultControls.CreateSlider(UiResources());
go.name = PfSlider;
Size(go, 240, 24);
var slider = go.GetComponent<Slider>();
var ui = GetOrAdd<UISlider>(go);
// 数值标签(右侧)
var labelGo = NewUIChild(go.transform, "ValueLabel", out var labelRt);
labelRt.anchorMin = new Vector2(1, 0.5f); labelRt.anchorMax = new Vector2(1, 0.5f);
labelRt.pivot = new Vector2(0, 0.5f); labelRt.anchoredPosition = new Vector2(8, 0);
labelRt.sizeDelta = new Vector2(48, 24);
var label = labelGo.AddComponent<TextMeshProUGUI>();
label.text = "0"; label.alignment = TextAlignmentOptions.MidlineLeft; label.fontSize = 18;
SetEnum(labelGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Text_Secondary);
AssignRef(ui, "_slider", slider);
AssignRef(ui, "_valueLabel", label);
SaveAsPrefab(go, PfSlider, report);
}
private static void BuildDropdown(List<string> report)
{
var go = TMP_DefaultControls.CreateDropdown(TmpResources());
go.name = PfDropdown;
Size(go, 200, 40);
var dd = go.GetComponent<TMP_Dropdown>();
var ui = GetOrAdd<UIDropdown>(go);
AssignRef(ui, "_dropdown", dd);
SaveAsPrefab(go, PfDropdown, report);
}
private static void BuildPanel(List<string> report)
{
var go = NewUI(PfPanel, 480, 320);
var bg = go.AddComponent<Image>();
bg.sprite = Background(); bg.type = Image.Type.Sliced;
bg.color = new Color(0.06f, 0.07f, 0.10f, 0.96f);
SetEnum(go.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Graphic_Background);
go.AddComponent<CanvasGroup>();
var applier = go.AddComponent<UIThemeApplier>();
if (s_theme != null) AssignRef(applier, "_theme", s_theme);
var panel = go.AddComponent<UISimplePanel>();
AssignRef(panel, "_canvasGroup", go.GetComponent<CanvasGroup>());
// 标题
var titleGo = NewUIChild(go.transform, "Title", out var titleRt);
titleRt.anchorMin = new Vector2(0, 1); titleRt.anchorMax = new Vector2(1, 1);
titleRt.pivot = new Vector2(0.5f, 1); titleRt.anchoredPosition = new Vector2(0, -16);
titleRt.sizeDelta = new Vector2(-32, 40);
var title = titleGo.AddComponent<TextMeshProUGUI>();
title.text = "Panel"; title.alignment = TextAlignmentOptions.Top; title.fontSize = 28;
SetEnum(titleGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Text_Header);
SaveAsPrefab(go, PfPanel, report);
}
private static void BuildTabBar(List<string> report)
{
var go = NewUI(PfTabBar, 480, 360);
var tabGroup = go.AddComponent<UITabGroup>();
// 头部按钮行
var headerGo = NewUIChild(go.transform, "Header", out var headerRt);
headerRt.anchorMin = new Vector2(0, 1); headerRt.anchorMax = new Vector2(1, 1);
headerRt.pivot = new Vector2(0.5f, 1); headerRt.anchoredPosition = Vector2.zero;
headerRt.sizeDelta = new Vector2(0, 44);
var hLayout = headerGo.AddComponent<HorizontalLayoutGroup>();
hLayout.spacing = 4; hLayout.childForceExpandWidth = true; hLayout.childForceExpandHeight = true;
// 内容容器
var contentGo = NewUIChild(go.transform, "Content", out var contentRt);
contentRt.anchorMin = Vector2.zero; contentRt.anchorMax = Vector2.one;
contentRt.offsetMin = new Vector2(0, 0); contentRt.offsetMax = new Vector2(0, -48);
var tabs = new (GameObject content, Button btn, GameObject hl)[2];
for (int i = 0; i < 2; i++)
{
var (btn, hl) = MakeTabHeader(headerGo.transform, $"Tab{i}Header", $"Tab {i + 1}");
var tabContent = NewUIChild(contentGo.transform, $"Tab{i}Content", out var tcRt);
Stretch(tcRt);
var tcImg = tabContent.AddComponent<Image>();
tcImg.color = new Color(1f, 1f, 1f, 0.03f);
var lblGo = NewUIChild(tabContent.transform, "Label", out var lblRt);
Stretch(lblRt);
var lbl = lblGo.AddComponent<TextMeshProUGUI>();
lbl.text = $"Tab {i + 1} 内容"; lbl.alignment = TextAlignmentOptions.Center; lbl.fontSize = 22;
if (i != 0) tabContent.SetActive(false);
tabs[i] = (tabContent, btn, hl);
}
WireTabGroup(tabGroup, tabs);
SaveAsPrefab(go, PfTabBar, report);
}
private static (Button btn, GameObject highlight) MakeTabHeader(Transform parent, string name, string text)
{
var go = NewUIChild(parent, name, out _);
var bg = go.AddComponent<Image>();
bg.sprite = Standard(); bg.type = Image.Type.Sliced;
bg.color = new Color(1f, 1f, 1f, 0.08f);
var btn = go.AddComponent<Button>(); btn.targetGraphic = bg;
var uiBtn = GetOrAdd<UIButton>(go);
if (s_theme != null) AssignRef(uiBtn, "_theme", s_theme);
var hl = NewUIChild(go.transform, "Highlight", out var hlRt);
Stretch(hlRt);
var hlImg = hl.AddComponent<Image>();
hlImg.color = new Color(0.20f, 0.65f, 1f, 0.30f);
hl.SetActive(false);
var lblGo = NewUIChild(go.transform, "Label", out var lblRt);
Stretch(lblRt);
var lbl = lblGo.AddComponent<TextMeshProUGUI>();
lbl.text = text; lbl.alignment = TextAlignmentOptions.Center; lbl.fontSize = 20;
SetEnum(lblGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Text_Primary);
return (btn, hl);
}
private static void WireTabGroup(UITabGroup tabGroup, (GameObject content, Button btn, GameObject hl)[] tabs)
{
var so = new SerializedObject(tabGroup);
var prop = so.FindProperty("_tabs");
prop.arraySize = tabs.Length;
for (int i = 0; i < tabs.Length; i++)
{
var el = prop.GetArrayElementAtIndex(i);
el.FindPropertyRelative("content").objectReferenceValue = tabs[i].content;
el.FindPropertyRelative("headerButton").objectReferenceValue = tabs[i].btn;
el.FindPropertyRelative("headerHighlight").objectReferenceValue = tabs[i].hl;
}
so.ApplyModifiedPropertiesWithoutUndo();
}
// ══ 设置面板(数据驱动)════════════════════════════════════════════════
private const string SchemaDir = "Assets/_Game/Data/UI";
private const string SchemaName = "UI_SettingsSchema";
private const string PfSettingHeader = "UI_Setting_Header";
private const string PfSettingSlider = "UI_Setting_SliderRow";
private const string PfSettingToggle = "UI_Setting_ToggleRow";
private const string PfSettingDrop = "UI_Setting_DropdownRow";
private const string PfSettingsPanel = "UI_SettingsPanel";
[MenuItem("BaseGames/UI/控件库/生成设置面板(行预制件 + 默认表 + 面板)")]
public static void GenerateSettings()
{
EnsureFolder(ControlsDir);
EnsureFolder(SchemaDir);
var report = new List<string>();
s_theme = EnsureDefaultTheme(report);
var header = BuildSettingHeader(report);
var slider = BuildSettingSliderRow(report);
var toggle = BuildSettingToggleRow(report);
var dropdown = BuildSettingDropdownRow(report);
var schema = EnsureDefaultSchema(report);
BuildSettingsPanel(report, header, slider, toggle, dropdown, schema);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
var sb = new System.Text.StringBuilder("[UIControlLibrary] 数据驱动设置面板已生成:\n");
foreach (var r in report) sb.AppendLine(" • " + r);
sb.AppendLine("标签 KeySET_*请用「BaseGames/Localization/表格编辑器」补译文;改 UI_SettingsSchema 即可增删/重排设置项。");
Debug.Log(sb.ToString());
}
private static GameObject BuildSettingHeader(List<string> report)
{
var go = NewUI(PfSettingHeader, 480, 32);
var lblGo = NewUIChild(go.transform, "Label", out var rt);
Stretch(rt);
var lbl = lblGo.AddComponent<TextMeshProUGUI>();
lbl.text = "Section"; lbl.fontSize = 22; lbl.alignment = TextAlignmentOptions.MidlineLeft;
lblGo.AddComponent<BaseGames.Localization.LocalizedText>();
SetEnum(lblGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Text_Header);
return SaveAsPrefab(go, PfSettingHeader, report);
}
private static GameObject BuildSettingSliderRow(List<string> report)
{
var go = SettingRowRoot(PfSettingSlider, out _);
var sliderGo = DefaultControls.CreateSlider(UiResources());
sliderGo.name = "Slider"; sliderGo.transform.SetParent(go.transform, false);
var sle = sliderGo.AddComponent<LayoutElement>(); sle.flexibleWidth = 1; sle.preferredHeight = 20;
var slider = sliderGo.GetComponent<Slider>();
var uiSlider = sliderGo.AddComponent<UISlider>();
var valGo = NewUIChild(go.transform, "Value", out _);
var val = valGo.AddComponent<TextMeshProUGUI>();
val.text = "0"; val.fontSize = 18; val.alignment = TextAlignmentOptions.MidlineRight;
var vle = valGo.AddComponent<LayoutElement>(); vle.preferredWidth = 56;
SetEnum(valGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Text_Secondary);
AssignRef(uiSlider, "_slider", slider);
AssignRef(uiSlider, "_valueLabel", val);
return SaveAsPrefab(go, PfSettingSlider, report);
}
private static GameObject BuildSettingToggleRow(List<string> report)
{
var go = SettingRowRoot(PfSettingToggle, out _);
var toggleGo = DefaultControls.CreateToggle(UiResources());
toggleGo.name = "Toggle"; toggleGo.transform.SetParent(go.transform, false);
var tle = toggleGo.AddComponent<LayoutElement>(); tle.preferredWidth = 30; tle.preferredHeight = 30;
return SaveAsPrefab(go, PfSettingToggle, report);
}
private static GameObject BuildSettingDropdownRow(List<string> report)
{
var go = SettingRowRoot(PfSettingDrop, out _);
var ddGo = TMP_DefaultControls.CreateDropdown(TmpResources());
ddGo.name = "Dropdown"; ddGo.transform.SetParent(go.transform, false);
var dle = ddGo.AddComponent<LayoutElement>(); dle.flexibleWidth = 1; dle.preferredHeight = 32;
var dd = ddGo.GetComponent<TMP_Dropdown>();
var ui = ddGo.AddComponent<UIDropdown>();
AssignRef(ui, "_dropdown", dd);
return SaveAsPrefab(go, PfSettingDrop, report);
}
/// <summary>构建带左侧本地化标签的设置行根HorizontalLayout。返回根out 标签 TMP。</summary>
private static GameObject SettingRowRoot(string name, out TMP_Text label)
{
var go = NewUI(name, 480, 44);
var h = go.AddComponent<HorizontalLayoutGroup>();
h.spacing = 12; h.childAlignment = TextAnchor.MiddleLeft;
h.childForceExpandWidth = false; h.childForceExpandHeight = false;
h.childControlWidth = true; h.childControlHeight = true;
h.padding = new RectOffset(8, 8, 4, 4);
var lblGo = NewUIChild(go.transform, "Label", out _);
label = lblGo.AddComponent<TextMeshProUGUI>();
label.text = "Label"; label.fontSize = 20; label.alignment = TextAlignmentOptions.MidlineLeft;
var le = lblGo.AddComponent<LayoutElement>(); le.preferredWidth = 200; le.flexibleWidth = 0;
lblGo.AddComponent<BaseGames.Localization.LocalizedText>();
SetEnum(lblGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Text_Primary);
return go;
}
private static SettingsSchemaSO EnsureDefaultSchema(List<string> report)
{
string path = $"{SchemaDir}/{SchemaName}.asset";
var schema = AssetDatabase.LoadAssetAtPath<SettingsSchemaSO>(path);
bool created = schema == null;
if (created) { schema = ScriptableObject.CreateInstance<SettingsSchemaSO>(); AssetDatabase.CreateAsset(schema, path); }
// 仅在新建时填充默认项,避免覆盖策划已有编辑
if (created)
{
var items = new (bool h, string key, SettingKey s)[]
{
(true, "SET_SECTION_AUDIO", default),
(false, "SET_MASTER_VOLUME", SettingKey.MasterVolume),
(false, "SET_BGM_VOLUME", SettingKey.BGMVolume),
(false, "SET_SFX_VOLUME", SettingKey.SFXVolume),
(false, "SET_AMBIENT_VOLUME", SettingKey.AmbientVolume),
(true, "SET_SECTION_DISPLAY", default),
(false, "SET_VSYNC", SettingKey.VSync),
(false, "SET_TARGET_FPS", SettingKey.TargetFPS),
(true, "SET_SECTION_ACCESS", default),
(false, "SET_UI_SCALE", SettingKey.UIScale),
(false, "SET_COLORBLIND", SettingKey.ColorblindMode),
(false, "SET_SCREEN_SHAKE", SettingKey.ScreenShake),
(true, "SET_SECTION_LANGUAGE", default),
(false, "SET_LANGUAGE", SettingKey.Language),
};
var so = new SerializedObject(schema);
var prop = so.FindProperty("_items");
prop.arraySize = items.Length;
for (int i = 0; i < items.Length; i++)
{
var el = prop.GetArrayElementAtIndex(i);
el.FindPropertyRelative("isHeader").boolValue = items[i].h;
el.FindPropertyRelative("labelKey").stringValue = items[i].key;
el.FindPropertyRelative("key").enumValueIndex = (int)items[i].s;
}
so.ApplyModifiedPropertiesWithoutUndo();
report.Add($"{path}(默认 {items.Length} 项,可增删/重排)");
}
return schema;
}
private static void BuildSettingsPanel(List<string> report, GameObject header, GameObject slider,
GameObject toggle, GameObject dropdown, SettingsSchemaSO schema)
{
var go = NewUI(PfSettingsPanel, 540, 640);
var bg = go.AddComponent<Image>();
bg.sprite = Background(); bg.type = Image.Type.Sliced;
bg.color = new Color(0.06f, 0.07f, 0.10f, 0.96f);
SetEnum(go.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Graphic_Background);
var applier = go.AddComponent<UIThemeApplier>();
if (s_theme != null) AssignRef(applier, "_theme", s_theme);
var panel = go.AddComponent<DataDrivenSettingsPanel>();
// 行容器(竖向布局)
var content = NewUIChild(go.transform, "Content", out var crt);
crt.anchorMin = Vector2.zero; crt.anchorMax = Vector2.one;
crt.offsetMin = new Vector2(16, 16); crt.offsetMax = new Vector2(-16, -16);
var v = content.AddComponent<VerticalLayoutGroup>();
v.spacing = 6; v.childForceExpandWidth = true; v.childForceExpandHeight = false;
v.childControlWidth = true; v.childControlHeight = true;
v.childAlignment = TextAnchor.UpperCenter;
AssignRef(panel, "_schema", schema);
AssignRef(panel, "_container", content.transform);
AssignRef(panel, "_headerPrefab", header);
AssignRef(panel, "_sliderRowPrefab", slider);
AssignRef(panel, "_toggleRowPrefab", toggle);
AssignRef(panel, "_dropdownRowPrefab", dropdown);
SaveAsPrefab(go, PfSettingsPanel, report);
}
// ══ 主菜单(数据驱动)════════════════════════════════════════════════
private const string PfMenuButton = "UI_MainMenu_Button";
private const string MenuConfigName = "UI_MainMenuConfig";
[MenuItem("BaseGames/UI/控件库/生成主菜单(按钮预制件 + 默认表)")]
public static void GenerateMainMenu()
{
EnsureFolder(ControlsDir);
EnsureFolder(SchemaDir);
var report = new List<string>();
s_theme = EnsureDefaultTheme(report);
BuildMainMenuButton(report);
EnsureDefaultMenuConfig(report);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
var sb = new System.Text.StringBuilder("[UIControlLibrary] 数据驱动主菜单已生成:\n");
foreach (var r in report) sb.AppendLine(" • " + r);
sb.AppendLine("标签 MENU_* 请补译文;改 UI_MainMenuConfig 即可增删/重排菜单项。");
sb.AppendLine("用法:在 Scene_MainMenu 挂 DataDrivenMainMenuController指定 config/container/buttonPrefab + 子面板引用。");
Debug.Log(sb.ToString());
}
private static GameObject BuildMainMenuButton(List<string> report)
{
var go = NewUI(PfMenuButton, 300, 56);
var bg = go.AddComponent<Image>();
bg.sprite = Standard(); bg.type = Image.Type.Sliced; bg.color = new Color(1f, 1f, 1f, 0.06f);
var btn = go.AddComponent<Button>(); btn.targetGraphic = bg;
var uiBtn = GetOrAdd<UIButton>(go);
if (s_theme != null) AssignRef(uiBtn, "_theme", s_theme);
var view = go.AddComponent<MainMenuButtonView>();
var iconGo = NewUIChild(go.transform, "Icon", out var irt);
irt.anchorMin = new Vector2(0, 0.5f); irt.anchorMax = new Vector2(0, 0.5f); irt.pivot = new Vector2(0, 0.5f);
irt.anchoredPosition = new Vector2(14, 0); irt.sizeDelta = new Vector2(32, 32);
var icon = iconGo.AddComponent<Image>(); icon.enabled = false;
var lblGo = NewUIChild(go.transform, "Label", out var lrt);
lrt.anchorMin = Vector2.zero; lrt.anchorMax = Vector2.one;
lrt.offsetMin = new Vector2(56, 0); lrt.offsetMax = new Vector2(-12, 0);
var lbl = lblGo.AddComponent<TextMeshProUGUI>();
lbl.text = "Menu Item"; lbl.alignment = TextAlignmentOptions.MidlineLeft; lbl.fontSize = 24;
var loc = lblGo.AddComponent<BaseGames.Localization.LocalizedText>();
SetEnum(lblGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Text_Primary);
AssignRef(view, "_button", btn);
AssignRef(view, "_label", loc);
AssignRef(view, "_icon", icon);
return SaveAsPrefab(go, PfMenuButton, report);
}
private static MainMenuConfigSO EnsureDefaultMenuConfig(List<string> report)
{
string path = $"{SchemaDir}/{MenuConfigName}.asset";
var cfg = AssetDatabase.LoadAssetAtPath<MainMenuConfigSO>(path);
bool created = cfg == null;
if (created) { cfg = ScriptableObject.CreateInstance<MainMenuConfigSO>(); AssetDatabase.CreateAsset(cfg, path); }
if (created)
{
var items = new (string key, MainMenuAction a, bool req)[]
{
("MENU_NEW_GAME", MainMenuAction.NewGame, false),
("MENU_CONTINUE", MainMenuAction.Continue, true),
("MENU_SETTINGS", MainMenuAction.OpenSettings, false),
("MENU_CREDITS", MainMenuAction.OpenCredits, false),
("MENU_QUIT", MainMenuAction.Quit, false),
};
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("requiresSave").boolValue = items[i].req;
el.FindPropertyRelative("sceneKey").stringValue = "";
}
so.ApplyModifiedPropertiesWithoutUndo();
report.Add($"{path}(默认 {items.Length} 项菜单)");
}
return cfg;
}
// ══ 本地化补全(数据驱动面板的 SET_*/MENU_* 标签)══════════════════════
[MenuItem("BaseGames/UI/控件库/补充设置与菜单本地化(中/英)")]
public static void SeedDataDrivenUILocalization()
{
var zh = new Dictionary<string, string>
{
{ "SET_SECTION_AUDIO", "音频" },
{ "SET_MASTER_VOLUME", "主音量" },
{ "SET_BGM_VOLUME", "音乐" },
{ "SET_SFX_VOLUME", "音效" },
{ "SET_AMBIENT_VOLUME", "环境音" },
{ "SET_SECTION_DISPLAY", "画面" },
{ "SET_VSYNC", "垂直同步" },
{ "SET_TARGET_FPS", "目标帧率" },
{ "SET_SECTION_ACCESS", "辅助功能" },
{ "SET_UI_SCALE", "界面缩放" },
{ "SET_COLORBLIND", "色盲模式" },
{ "SET_SCREEN_SHAKE", "屏幕震动" },
{ "SET_SECTION_LANGUAGE", "语言" },
{ "SET_LANGUAGE", "语言" },
{ "SET_FPS_UNLIMITED", "无限" },
{ "SET_COLORBLIND_0", "关闭" },
{ "SET_COLORBLIND_1", "红色弱Protanopia" },
{ "SET_COLORBLIND_2", "绿色弱Deuteranopia" },
{ "SET_COLORBLIND_3", "蓝色弱Tritanopia" },
{ "MENU_NEW_GAME", "新游戏" },
{ "MENU_CONTINUE", "继续" },
{ "MENU_SETTINGS", "设置" },
{ "MENU_CREDITS", "制作团队" },
{ "MENU_QUIT", "退出" },
};
var en = new Dictionary<string, string>
{
{ "SET_SECTION_AUDIO", "Audio" },
{ "SET_MASTER_VOLUME", "Master Volume" },
{ "SET_BGM_VOLUME", "Music" },
{ "SET_SFX_VOLUME", "Sound Effects" },
{ "SET_AMBIENT_VOLUME", "Ambience" },
{ "SET_SECTION_DISPLAY", "Display" },
{ "SET_VSYNC", "V-Sync" },
{ "SET_TARGET_FPS", "Target FPS" },
{ "SET_SECTION_ACCESS", "Accessibility" },
{ "SET_UI_SCALE", "UI Scale" },
{ "SET_COLORBLIND", "Colorblind Mode" },
{ "SET_SCREEN_SHAKE", "Screen Shake" },
{ "SET_SECTION_LANGUAGE", "Language" },
{ "SET_LANGUAGE", "Language" },
{ "SET_FPS_UNLIMITED", "Unlimited" },
{ "SET_COLORBLIND_0", "Off" },
{ "SET_COLORBLIND_1", "Protanopia" },
{ "SET_COLORBLIND_2", "Deuteranopia" },
{ "SET_COLORBLIND_3", "Tritanopia" },
{ "MENU_NEW_GAME", "New Game" },
{ "MENU_CONTINUE", "Continue" },
{ "MENU_SETTINGS", "Settings" },
{ "MENU_CREDITS", "Credits" },
{ "MENU_QUIT", "Quit" },
};
int added = MergeWriteUI(Language.ChineseSimplified, zh)
+ MergeWriteUI(Language.English, en);
Debug.Log($"[UIControlLibrary] 已补充设置/菜单本地化(新增 {added} 条,已存在的不覆盖)。" +
"日/韩缺省走英文回退可用「BaseGames/Localization/表格编辑器」补译。");
}
/// <summary>把缺失的 key 合并写入指定语言的 UI 表(已存在的保留,不覆盖)。返回新增数。</summary>
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 NewUI(string name, float w, float h)
{
var go = new GameObject(name, typeof(RectTransform));
((RectTransform)go.transform).sizeDelta = new Vector2(w, h);
return go;
}
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 Size(GameObject go, float w, float h)
{
if (go.transform is RectTransform rt) rt.sizeDelta = new Vector2(w, h);
}
private static void Stretch(RectTransform rt)
{
rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.one;
rt.offsetMin = Vector2.zero; rt.offsetMax = Vector2.zero;
}
private static T GetOrAdd<T>(GameObject go) where T : Component
=> go.GetComponent<T>() ?? go.AddComponent<T>();
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($"[UIControlLibrary] 未找到属性 {target.GetType().Name}.{prop}"); return; }
p.objectReferenceValue = value;
so.ApplyModifiedPropertiesWithoutUndo();
}
private static GameObject SaveAsPrefab(GameObject go, string name, List<string> report)
{
string path = $"{ControlsDir}/{name}.prefab";
var asset = PrefabUtility.SaveAsPrefabAsset(go, path);
Object.DestroyImmediate(go);
report.Add(path);
return asset;
}
private static void EnsureFolder(string dir)
{
string[] parts = dir.Split('/');
string cur = parts[0]; // "Assets"
for (int i = 1; i < parts.Length; i++)
{
string next = $"{cur}/{parts[i]}";
if (!AssetDatabase.IsValidFolder(next)) AssetDatabase.CreateFolder(cur, parts[i]);
cur = next;
}
}
// ── 内建 UI 资源(默认皮肤 sprite──────────────────────────────────────
private static Sprite Standard() => AssetDatabase.GetBuiltinExtraResource<Sprite>("UI/Skin/UISprite.psd");
private static Sprite Background() => AssetDatabase.GetBuiltinExtraResource<Sprite>("UI/Skin/Background.psd");
private static Sprite Knob() => AssetDatabase.GetBuiltinExtraResource<Sprite>("UI/Skin/Knob.psd");
private static Sprite Checkmark() => AssetDatabase.GetBuiltinExtraResource<Sprite>("UI/Skin/Checkmark.psd");
private static Sprite DropArrow() => AssetDatabase.GetBuiltinExtraResource<Sprite>("UI/Skin/DropdownArrow.psd");
private static Sprite Mask() => AssetDatabase.GetBuiltinExtraResource<Sprite>("UI/Skin/UIMask.psd");
private static Sprite InputBg() => AssetDatabase.GetBuiltinExtraResource<Sprite>("UI/Skin/InputFieldBackground.psd");
private static DefaultControls.Resources UiResources() => new DefaultControls.Resources
{
standard = Standard(), background = Background(), inputField = InputBg(),
knob = Knob(), checkmark = Checkmark(), dropdown = DropArrow(), mask = Mask(),
};
private static TMP_DefaultControls.Resources TmpResources() => new TMP_DefaultControls.Resources
{
standard = Standard(), background = Background(), inputField = InputBg(),
knob = Knob(), checkmark = Checkmark(), dropdown = DropArrow(), mask = Mask(),
};
}
}