760 lines
38 KiB
C#
760 lines
38 KiB
C#
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("标签 Key(SET_*)请用「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(),
|
||
};
|
||
}
|
||
}
|