UI 系统

This commit is contained in:
2026-06-08 11:26:17 +08:00
parent 1897658a00
commit b582317692
94 changed files with 33540 additions and 3726 deletions

View File

@@ -0,0 +1,309 @@
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using BaseGames.UI;
using BaseGames.UI.Theme;
using BaseGames.Core.Events;
using BaseGames.Localization;
using BaseGames.Editor.Localization;
namespace BaseGames.Editor.UI
{
/// <summary>
/// 护符面板脚手架:生成 <c>UI_CharmPanel</c> 预制件CharmEquipPanel + 已装备区 + 收藏目录 + 凹槽进度条 + 卡片模板)。
/// 数据来源运行期 ServiceLocator → IEquipmentServiceEquipmentManager变更经 EVT_EquipmentChanged 反应式重建。
///
/// 该面板既可 standalone 打开PanelId.CharmPanel也由 InventoryHub 内嵌为「护符」Tab脚手架内嵌时中和 Canvas 链 + 隐藏 chrome
/// 美术 → 改 UI_CharmPanel / 卡片样式;策划 → 改护符数据(CharmSO/CharmCatalogSO)。
/// 菜单BaseGames/UI/控件库/生成护符面板(预制件)
/// </summary>
public static class UICharmScaffold
{
private const string PrefabDir = "Assets/_Game/Prefabs/UI";
private const string ThemePath = "Assets/_Game/Data/UI/Themes/UI_Theme_Default.asset";
private const string PrefabName = "UI_CharmPanel";
[MenuItem("BaseGames/UI/控件库/生成护符面板(预制件)")]
public static void GeneratePrefab()
{
EnsureFolder(PrefabDir);
var report = new List<string>();
var theme = AssetDatabase.LoadAssetAtPath<UIThemeSO>(ThemePath);
SeedLocalization(report);
BuildPrefab(theme, report);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
var sb = new System.Text.StringBuilder("[UICharm] 护符面板已生成:\n");
foreach (var r in report) sb.AppendLine(" • " + r);
sb.AppendLine("运行期读 IEquipmentServiceEquipmentManager重跑「Scaffold Inventory Hub」把它收编为护符 Tab。");
Debug.Log(sb.ToString());
}
private static void SeedLocalization(List<string> report)
{
var zh = new Dictionary<string, string>
{
{ "MENU_CHARM", "护符" }, { "CHARM_TITLE", "护符" },
{ "CHARM_EQUIPPED", "已装备" }, { "CHARM_CATALOG", "收藏" }, { "CHARM_NOTCHES", "凹槽" },
};
var en = new Dictionary<string, string>
{
{ "MENU_CHARM", "Charms" }, { "CHARM_TITLE", "Charms" },
{ "CHARM_EQUIPPED", "Equipped" }, { "CHARM_CATALOG", "Collected" }, { "CHARM_NOTCHES", "Notches" },
};
int added = MergeWriteUI(Language.ChineseSimplified, zh) + MergeWriteUI(Language.English, en);
report.Add($"本地化新增 {added} 条(已存在不覆盖)。");
}
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 void BuildPrefab(UIThemeSO theme, List<string> report)
{
var root = new GameObject(PrefabName,
typeof(RectTransform), typeof(Canvas), typeof(CanvasScaler), typeof(GraphicRaycaster), typeof(CanvasGroup));
Stretch((RectTransform)root.transform);
SetupCanvas(root, 61);
var panel = root.AddComponent<CharmEquipPanel>();
var applier = root.AddComponent<UIThemeApplier>();
if (theme != null) AssignRef(applier, "_theme", theme);
// 全屏遮罩chrome内嵌时隐藏
var overlayGo = NewUIChild(root.transform, "Overlay", out var overlayRt);
Stretch(overlayRt);
var overlay = overlayGo.AddComponent<Image>(); overlay.color = new Color(0f, 0f, 0f, 0.6f);
// 主框
var boxGo = NewUIChild(root.transform, "DialogBox", out var boxRt);
SetRect(boxRt, C(0.5f), C(0.5f), C(0.5f), Vector2.zero, new Vector2(1280f, 720f));
var box = boxGo.AddComponent<Image>();
box.sprite = Std(); box.type = Image.Type.Sliced; box.color = new Color(0.10f, 0.11f, 0.15f, 0.98f);
SetEnum(boxGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Graphic_Background);
// 标题
var titleGo = NewUIChild(boxGo.transform, "Title", out var titleRt);
SetRect(titleRt, C(0.5f, 1f), C(0.5f, 1f), C(0.5f, 1f), new Vector2(0f, -50f), new Vector2(600f, 60f));
var title = titleGo.AddComponent<TextMeshProUGUI>();
title.alignment = TextAlignmentOptions.Center; title.fontSize = 40f; title.raycastTarget = false;
titleGo.AddComponent<LocalizedText>();
SetString(titleGo.GetComponent<LocalizedText>(), "_key", "CHARM_TITLE");
SetEnum(titleGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Text_Header);
// 凹槽进度条(已用/总数)
var notchGo = NewUIChild(boxGo.transform, "NotchBar", out var notchRt);
SetRect(notchRt, C(0.5f, 1f), C(0.5f, 1f), C(0.5f, 1f), new Vector2(0f, -110f), new Vector2(420f, 34f));
var notchBg = notchGo.AddComponent<Image>(); notchBg.color = new Color(1f, 1f, 1f, 0.08f);
notchBg.sprite = Std(); notchBg.type = Image.Type.Sliced;
var fillGo = NewUIChild(notchGo.transform, "Fill", out var fillRt);
Stretch(fillRt);
var notchFill = fillGo.AddComponent<Image>();
notchFill.color = new Color(0.85f, 0.80f, 0.55f, 0.85f);
notchFill.sprite = Std(); notchFill.type = Image.Type.Filled;
notchFill.fillMethod = Image.FillMethod.Horizontal; notchFill.fillOrigin = 0; notchFill.fillAmount = 0f;
var notchTextGo = NewUIChild(notchGo.transform, "NotchText", out var notchTextRt);
Stretch(notchTextRt);
var notchText = notchTextGo.AddComponent<TextMeshProUGUI>();
notchText.alignment = TextAlignmentOptions.Center; notchText.fontSize = 20f; notchText.raycastTarget = false;
SetEnum(notchTextGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Text_Primary);
// 已装备区(左)
var equippedLabel = MakeSectionLabel(boxGo.transform, "EquippedLabel", "CHARM_EQUIPPED",
C(0f, 1f), new Vector2(60f, -160f), new Vector2(420f, 34f));
var equippedGo = NewUIChild(boxGo.transform, "EquippedContainer", out var equippedRt);
SetRect(equippedRt, C(0f, 1f), C(0f, 1f), C(0f, 1f), new Vector2(40f, -200f), new Vector2(440f, 460f));
var eqGrid = equippedGo.AddComponent<GridLayoutGroup>();
eqGrid.cellSize = new Vector2(200f, 96f); eqGrid.spacing = new Vector2(8f, 8f);
eqGrid.childAlignment = TextAnchor.UpperLeft; eqGrid.constraint = GridLayoutGroup.Constraint.FixedColumnCount;
eqGrid.constraintCount = 2;
// 收藏目录(右,滚动)
var catalogLabel = MakeSectionLabel(boxGo.transform, "CatalogLabel", "CHARM_CATALOG",
C(1f, 1f), new Vector2(-490f, -160f), new Vector2(440f, 34f));
var scrollGo = NewUIChild(boxGo.transform, "CatalogScroll", out var scrollRt);
SetRect(scrollRt, C(1f, 1f), C(1f, 1f), C(1f, 1f), new Vector2(-40f, -200f), new Vector2(680f, 460f));
var scrollImg = scrollGo.AddComponent<Image>(); scrollImg.color = new Color(1f, 1f, 1f, 0.03f);
var scroll = scrollGo.AddComponent<ScrollRect>();
scroll.horizontal = false; scroll.vertical = true;
var viewportGo = NewUIChild(scrollGo.transform, "Viewport", out var viewportRt);
Stretch(viewportRt);
var vpImg = viewportGo.AddComponent<Image>(); vpImg.color = new Color(1f, 1f, 1f, 0.01f);
viewportGo.AddComponent<RectMask2D>();
var catalogGo = NewUIChild(viewportGo.transform, "CatalogContainer", out var catalogRt);
SetRect(catalogRt, C(0f, 1f), C(1f, 1f), C(0.5f, 1f), Vector2.zero, new Vector2(0f, 0f));
var catGrid = catalogGo.AddComponent<GridLayoutGroup>();
catGrid.cellSize = new Vector2(200f, 96f); catGrid.spacing = new Vector2(8f, 8f);
catGrid.childAlignment = TextAnchor.UpperLeft; catGrid.constraint = GridLayoutGroup.Constraint.FixedColumnCount;
catGrid.constraintCount = 3; catGrid.padding = new RectOffset(8, 8, 8, 8);
var catFitter = catalogGo.AddComponent<ContentSizeFitter>();
catFitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
AssignRef(scroll, "m_Content", catalogRt);
AssignRef(scroll, "m_Viewport", viewportRt);
// 卡片模板CharmCardViewinactive
var cardGo = BuildCardTemplate(boxGo.transform);
// 关闭按钮chrome内嵌时隐藏
var closeGo = NewUIChild(boxGo.transform, "Btn_Close", out var closeRt);
SetRect(closeRt, C(0.5f, 0f), C(0.5f, 0f), C(0.5f, 0f), new Vector2(0f, 36f), new Vector2(240f, 56f));
var closeImg = closeGo.AddComponent<Image>(); closeImg.sprite = Std(); closeImg.type = Image.Type.Sliced;
closeImg.color = new Color(1f, 1f, 1f, 0.06f);
var closeBtn = closeGo.AddComponent<Button>(); closeBtn.targetGraphic = closeImg;
var closeLblGo = NewUIChild(closeGo.transform, "Label", out var closeLblRt);
Stretch(closeLblRt);
var closeLbl = closeLblGo.AddComponent<TextMeshProUGUI>();
closeLbl.alignment = TextAlignmentOptions.Center; closeLbl.fontSize = 26f;
closeLblGo.AddComponent<LocalizedText>();
SetString(closeLblGo.GetComponent<LocalizedText>(), "_key", "BTN_BACK");
SetEnum(closeLblGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Text_Primary);
// 绑定 CharmEquipPanel
AssignRef(panel, "_notchText", notchText);
AssignRef(panel, "_notchBarFill", notchFill);
AssignRef(panel, "_equippedContainer", equippedGo.transform);
AssignRef(panel, "_catalogContainer", catalogGo.transform);
AssignRef(panel, "_charmCardTemplate", cardGo);
AssignRef(panel, "_btnClose", closeBtn);
AssignRef(panel, "_onEquipmentChanged", FindAsset<VoidEventChannelSO>("EVT_EquipmentChanged"));
if (FindAsset<VoidEventChannelSO>("EVT_EquipmentChanged") == null)
report.Add("EVT_EquipmentChanged 未找到_onEquipmentChanged 留空(运行 BaseGames ▸ Events ▸ Create Event Channels。");
root.SetActive(false);
PrefabUtility.SaveAsPrefabAsset(root, $"{PrefabDir}/{PrefabName}.prefab");
Object.DestroyImmediate(root);
report.Add($"{PrefabDir}/{PrefabName}.prefab");
}
// 护符卡片模板CharmCardView + icon/name/notchCost/desc/badge/button整卡为按钮
private static GameObject BuildCardTemplate(Transform parent)
{
var cardGo = NewUIChild(parent, "CharmCardTemplate", out var cardRt);
cardRt.sizeDelta = new Vector2(200f, 96f);
var bg = cardGo.AddComponent<Image>(); bg.color = new Color(1f, 1f, 1f, 0.05f);
bg.sprite = Std(); bg.type = Image.Type.Sliced;
var btn = cardGo.AddComponent<Button>(); btn.targetGraphic = bg;
var view = cardGo.AddComponent<CharmCardView>();
var iconGo = NewUIChild(cardGo.transform, "Icon", out var iconRt);
SetRect(iconRt, C(0f, 0.5f), C(0f, 0.5f), C(0f, 0.5f), new Vector2(12f, 0f), new Vector2(64f, 64f));
var icon = iconGo.AddComponent<Image>(); icon.preserveAspect = true; icon.raycastTarget = false;
var nameGo = NewUIChild(cardGo.transform, "Name", out var nameRt);
SetRect(nameRt, C(0f, 1f), C(1f, 1f), C(0f, 1f), new Vector2(88f, -10f), new Vector2(-96f, 28f));
var nameTxt = nameGo.AddComponent<TextMeshProUGUI>();
nameTxt.fontSize = 20f; nameTxt.raycastTarget = false; nameTxt.alignment = TextAlignmentOptions.TopLeft;
SetEnum(nameGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Text_Header);
var costGo = NewUIChild(cardGo.transform, "NotchCost", out var costRt);
SetRect(costRt, C(1f, 1f), C(1f, 1f), C(1f, 1f), new Vector2(-10f, -10f), new Vector2(40f, 28f));
var costTxt = costGo.AddComponent<TextMeshProUGUI>();
costTxt.fontSize = 18f; costTxt.alignment = TextAlignmentOptions.TopRight; costTxt.raycastTarget = false;
SetEnum(costGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Text_Secondary);
var descGo = NewUIChild(cardGo.transform, "Desc", out var descRt);
SetRect(descRt, C(0f, 0f), C(1f, 0f), C(0f, 0f), new Vector2(88f, 8f), new Vector2(-96f, 44f));
var descTxt = descGo.AddComponent<TextMeshProUGUI>();
descTxt.fontSize = 15f; descTxt.alignment = TextAlignmentOptions.TopLeft; descTxt.raycastTarget = false;
descTxt.enableWordWrapping = true;
SetEnum(descGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Text_Secondary);
var badgeGo = NewUIChild(cardGo.transform, "Badge", out var badgeRt);
SetRect(badgeRt, C(1f, 0f), C(1f, 0f), C(1f, 0f), new Vector2(-12f, 10f), new Vector2(28f, 28f));
var badgeTxt = badgeGo.AddComponent<TextMeshProUGUI>();
badgeTxt.fontSize = 22f; badgeTxt.alignment = TextAlignmentOptions.Center; badgeTxt.raycastTarget = false;
SetEnum(badgeGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Graphic_Accent);
// 绑定 CharmCardView 字段
AssignRef(view, "_icon", icon);
AssignRef(view, "_iconRoot", iconGo);
AssignRef(view, "_nameText", nameTxt);
AssignRef(view, "_notchCostText", costTxt);
AssignRef(view, "_descriptionText", descTxt);
AssignRef(view, "_equipBadgeText", badgeTxt);
AssignRef(view, "_actionButton", btn);
cardGo.SetActive(false);
return cardGo;
}
private static TMP_Text MakeSectionLabel(Transform parent, string name, string locKey,
Vector2 anchor, Vector2 pos, Vector2 size)
{
var go = NewUIChild(parent, name, out var rt);
SetRect(rt, anchor, anchor, anchor, pos, size);
var t = go.AddComponent<TextMeshProUGUI>();
t.fontSize = 24f; t.alignment = TextAlignmentOptions.Left; t.raycastTarget = false;
go.AddComponent<LocalizedText>();
SetString(go.GetComponent<LocalizedText>(), "_key", locKey);
SetEnum(go.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Text_Header);
return t;
}
// ── 助手(对照 UIFormSkillScaffold────────────────────────────────────
private static Vector2 C(float x = 0.5f, float y = 0.5f) => new Vector2(x, y);
private static GameObject NewUIChild(Transform parent, string name, out RectTransform rt)
{
var go = new GameObject(name, typeof(RectTransform));
rt = (RectTransform)go.transform; rt.SetParent(parent, false);
return go;
}
private static void Stretch(RectTransform rt)
{ rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.one; rt.offsetMin = Vector2.zero; rt.offsetMax = Vector2.zero; }
private static void SetRect(RectTransform rt, Vector2 aMin, Vector2 aMax, Vector2 pivot, Vector2 pos, Vector2 size)
{ rt.anchorMin = aMin; rt.anchorMax = aMax; rt.pivot = pivot; rt.anchoredPosition = pos; rt.sizeDelta = size; }
private static void SetEnum(Component c, string prop, int value)
{ var so = new SerializedObject(c); var p = so.FindProperty(prop); if (p != null) { p.enumValueIndex = value; so.ApplyModifiedPropertiesWithoutUndo(); } }
private static void SetString(Component c, string prop, string value)
{ if (c == null) return; var so = new SerializedObject(c); var p = so.FindProperty(prop); if (p != null) { p.stringValue = value; so.ApplyModifiedPropertiesWithoutUndo(); } }
private static void AssignRef(Object target, string prop, Object value)
{ var so = new SerializedObject(target); var p = so.FindProperty(prop); if (p != null) { p.objectReferenceValue = value; so.ApplyModifiedPropertiesWithoutUndo(); } }
private static T FindAsset<T>(string name) where T : Object
{
foreach (var guid in AssetDatabase.FindAssets($"{name} t:{typeof(T).Name}"))
{
var path = AssetDatabase.GUIDToAssetPath(guid);
var a = AssetDatabase.LoadAssetAtPath<T>(path);
if (a != null && a.name == name) return a;
}
return null;
}
private static void SetupCanvas(GameObject go, int sortOrder)
{
var canvas = go.GetComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay; canvas.sortingOrder = sortOrder;
var scaler = go.GetComponent<CanvasScaler>();
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
scaler.referenceResolution = new Vector2(1920f, 1080f);
scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight;
scaler.matchWidthOrHeight = 0.5f;
}
private static Sprite Std() => AssetDatabase.GetBuiltinExtraResource<Sprite>("UI/Skin/UISprite.psd");
private static void EnsureFolder(string dir)
{
string[] parts = dir.Split('/'); string cur = parts[0];
for (int i = 1; i < parts.Length; i++) { string n = $"{cur}/{parts[i]}"; if (!AssetDatabase.IsValidFolder(n)) AssetDatabase.CreateFolder(cur, parts[i]); cur = n; }
}
}
}