UI系统组件
This commit is contained in:
759
Assets/_Game/Scripts/Editor/UI/UIControlLibraryScaffold.cs
Normal file
759
Assets/_Game/Scripts/Editor/UI/UIControlLibraryScaffold.cs
Normal file
@@ -0,0 +1,759 @@
|
||||
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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user