UI 系统
This commit is contained in:
250
Assets/_Game/Scripts/Editor/UI/UIAbilityOverviewScaffold.cs
Normal file
250
Assets/_Game/Scripts/Editor/UI/UIAbilityOverviewScaffold.cs
Normal file
@@ -0,0 +1,250 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
using BaseGames.UI;
|
||||
using BaseGames.UI.Theme;
|
||||
using BaseGames.Player;
|
||||
using BaseGames.Core.Events;
|
||||
using BaseGames.Localization;
|
||||
using BaseGames.Editor.Localization;
|
||||
|
||||
namespace BaseGames.Editor.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 能力解锁总览脚手架:生成 <c>UI_AbilityCell</c> + <c>UI_AbilityOverviewPanel</c> 预制件
|
||||
/// + 默认 <c>UI_AbilityMeta</c> 表(参照 AbilityTypeDrawer 分组)+ ABL_* 中英文案。
|
||||
/// 面板据 AbilityMetaSO 列出全部能力 + 已解锁(亮)/未解锁(暗+锁)态,订阅 EVT_AbilityUnlocked 实时刷新。
|
||||
///
|
||||
/// 菜单:BaseGames/UI/控件库/生成能力解锁总览(预制件 + 默认表)
|
||||
/// </summary>
|
||||
public static class UIAbilityOverviewScaffold
|
||||
{
|
||||
private const string PrefabDir = "Assets/_Game/Prefabs/UI";
|
||||
private const string ControlsDir = "Assets/_Game/Prefabs/UI/Controls";
|
||||
private const string ConfigDir = "Assets/_Game/Data/UI";
|
||||
private const string ThemePath = "Assets/_Game/Data/UI/Themes/UI_Theme_Default.asset";
|
||||
private const string PanelName = "UI_AbilityOverviewPanel";
|
||||
private const string CellName = "UI_AbilityCell";
|
||||
private const string MetaName = "UI_AbilityMeta";
|
||||
|
||||
// 默认能力项(参照 Editor/Equipment/AbilityTypeDrawer.Groups):type, labelKey, 中文, 英文
|
||||
private static readonly (AbilityType t, string key, string zh, string en)[] Defaults =
|
||||
{
|
||||
(AbilityType.WallCling, "ABL_WALLCLING", "贴墙悬挂", "Wall Cling"),
|
||||
(AbilityType.WallJump, "ABL_WALLJUMP", "墙跳", "Wall Jump"),
|
||||
(AbilityType.Dash, "ABL_DASH", "冲刺", "Dash"),
|
||||
(AbilityType.DoubleJump, "ABL_DOUBLEJUMP", "二段跳", "Double Jump"),
|
||||
(AbilityType.SuperJump, "ABL_SUPERJUMP", "超级跳", "Super Jump"),
|
||||
(AbilityType.Swim, "ABL_SWIM", "游泳", "Swim"),
|
||||
(AbilityType.DownDash, "ABL_DOWNDASH", "下冲刺", "Down Dash"),
|
||||
(AbilityType.InvincibleDash,"ABL_INVINCIBLEDASH","无敌冲刺","Invincible Dash"),
|
||||
(AbilityType.Spell1, "ABL_SPELL1", "法术槽 1", "Spell Slot 1"),
|
||||
(AbilityType.Spell2, "ABL_SPELL2", "法术槽 2", "Spell Slot 2"),
|
||||
(AbilityType.Spell3, "ABL_SPELL3", "法术槽 3", "Spell Slot 3"),
|
||||
(AbilityType.SpiritForm, "ABL_SPIRITFORM", "灵魄形态", "Spirit Form"),
|
||||
(AbilityType.SpiritDash, "ABL_SPIRITDASH", "灵魄冲刺", "Spirit Dash"),
|
||||
(AbilityType.FormTianHun, "ABL_FORM_TIANHUN", "天魂", "Sky Soul"),
|
||||
(AbilityType.FormDiHun, "ABL_FORM_DIHUN", "地魂", "Earth Soul"),
|
||||
(AbilityType.FormMingHun, "ABL_FORM_MINGHUN", "命魂", "Death Soul"),
|
||||
(AbilityType.Parry, "ABL_PARRY", "弹反", "Parry"),
|
||||
(AbilityType.ChargeAttack, "ABL_CHARGEATTACK", "蓄力攻击", "Charge Attack"),
|
||||
(AbilityType.DownSlash, "ABL_DOWNSLASH", "下斩", "Down Slash"),
|
||||
(AbilityType.Interact, "ABL_INTERACT", "互动", "Interact"),
|
||||
(AbilityType.FastTravel, "ABL_FASTTRAVEL", "快速旅行", "Fast Travel"),
|
||||
};
|
||||
|
||||
[MenuItem("BaseGames/UI/控件库/生成能力解锁总览(预制件 + 默认表)")]
|
||||
public static void GeneratePrefab()
|
||||
{
|
||||
EnsureFolder(PrefabDir); EnsureFolder(ControlsDir); EnsureFolder(ConfigDir);
|
||||
var report = new List<string>();
|
||||
var theme = AssetDatabase.LoadAssetAtPath<UIThemeSO>(ThemePath);
|
||||
var meta = EnsureMeta(report);
|
||||
SeedLocalization(report);
|
||||
var cell = BuildCellPrefab(theme, report);
|
||||
BuildPanelPrefab(theme, meta, cell, report);
|
||||
|
||||
AssetDatabase.SaveAssets(); AssetDatabase.Refresh();
|
||||
var sb = new System.Text.StringBuilder("[UIAbilityOverview] 能力总览已生成:\n");
|
||||
foreach (var r in report) sb.AppendLine(" • " + r);
|
||||
sb.AppendLine("策划改 UI_AbilityMeta 增删/重排能力;美术改 UI_AbilityCell / 面板预制件改样式。");
|
||||
Debug.Log(sb.ToString());
|
||||
}
|
||||
|
||||
private static AbilityMetaSO EnsureMeta(List<string> report)
|
||||
{
|
||||
string path = $"{ConfigDir}/{MetaName}.asset";
|
||||
var meta = AssetDatabase.LoadAssetAtPath<AbilityMetaSO>(path);
|
||||
bool created = meta == null;
|
||||
if (created) { meta = ScriptableObject.CreateInstance<AbilityMetaSO>(); AssetDatabase.CreateAsset(meta, path); }
|
||||
if (created)
|
||||
{
|
||||
var so = new SerializedObject(meta);
|
||||
var p = so.FindProperty("_items");
|
||||
p.arraySize = Defaults.Length;
|
||||
for (int i = 0; i < Defaults.Length; i++)
|
||||
{
|
||||
var el = p.GetArrayElementAtIndex(i);
|
||||
el.FindPropertyRelative("type").enumValueFlag = (int)(uint)Defaults[i].t;
|
||||
el.FindPropertyRelative("labelKey").stringValue = Defaults[i].key;
|
||||
}
|
||||
so.ApplyModifiedPropertiesWithoutUndo();
|
||||
report.Add($"{path}(默认 {Defaults.Length} 项能力)");
|
||||
}
|
||||
return meta;
|
||||
}
|
||||
|
||||
private static void SeedLocalization(List<string> report)
|
||||
{
|
||||
var zh = new Dictionary<string, string> { { "ABL_OVERVIEW_TITLE", "能力" } };
|
||||
var en = new Dictionary<string, string> { { "ABL_OVERVIEW_TITLE", "Abilities" } };
|
||||
foreach (var d in Defaults) { zh[d.key] = d.zh; en[d.key] = d.en; }
|
||||
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 AbilityCellView BuildCellPrefab(UIThemeSO theme, List<string> report)
|
||||
{
|
||||
var root = new GameObject(CellName, typeof(RectTransform), typeof(CanvasGroup));
|
||||
((RectTransform)root.transform).sizeDelta = new Vector2(180f, 150f);
|
||||
var view = root.AddComponent<AbilityCellView>();
|
||||
|
||||
var bg = root.AddComponent<Image>();
|
||||
bg.sprite = Std(); bg.type = Image.Type.Sliced; bg.color = new Color(1f, 1f, 1f, 0.05f);
|
||||
|
||||
var iconGo = NewUIChild(root.transform, "Icon", out var iconRt);
|
||||
SetRect(iconRt, C(0.5f, 1f), C(0.5f, 1f), C(0.5f, 1f), new Vector2(0f, -50f), new Vector2(72f, 72f));
|
||||
var icon = iconGo.AddComponent<Image>(); icon.preserveAspect = true; icon.raycastTarget = false;
|
||||
|
||||
var nameGo = NewUIChild(root.transform, "Name", out var nameRt);
|
||||
SetRect(nameRt, C(0.5f, 0f), C(0.5f, 0f), C(0.5f, 0f), new Vector2(0f, 18f), new Vector2(-12f, 44f));
|
||||
var nameTmp = nameGo.AddComponent<TextMeshProUGUI>();
|
||||
nameTmp.alignment = TextAlignmentOptions.Center; nameTmp.fontSize = 20f; nameTmp.enableWordWrapping = true;
|
||||
nameTmp.raycastTarget = false;
|
||||
var loc = nameGo.AddComponent<LocalizedText>();
|
||||
SetEnum(nameGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Text_Primary);
|
||||
|
||||
// 锁定遮罩(未解锁时显示):半透明暗罩(美术可替换为锁图标 sprite)
|
||||
var lockGo = NewUIChild(root.transform, "LockedOverlay", out var lockRt);
|
||||
Stretch(lockRt);
|
||||
var lockImg = lockGo.AddComponent<Image>(); lockImg.color = new Color(0f, 0f, 0f, 0.55f); lockImg.raycastTarget = false;
|
||||
lockGo.SetActive(false);
|
||||
|
||||
var so = new SerializedObject(view);
|
||||
so.FindProperty("_icon").objectReferenceValue = icon;
|
||||
so.FindProperty("_label").objectReferenceValue = loc;
|
||||
so.FindProperty("_lockedOverlay").objectReferenceValue = lockGo;
|
||||
so.FindProperty("_group").objectReferenceValue = root.GetComponent<CanvasGroup>();
|
||||
so.ApplyModifiedPropertiesWithoutUndo();
|
||||
|
||||
string path = $"{ControlsDir}/{CellName}.prefab";
|
||||
var asset = PrefabUtility.SaveAsPrefabAsset(root, path);
|
||||
Object.DestroyImmediate(root);
|
||||
report.Add(path);
|
||||
return asset.GetComponent<AbilityCellView>();
|
||||
}
|
||||
|
||||
// ── 面板预制件 ───────────────────────────────────────────────────────
|
||||
private static void BuildPanelPrefab(UIThemeSO theme, AbilityMetaSO meta, AbilityCellView cell, List<string> report)
|
||||
{
|
||||
var root = new GameObject(PanelName,
|
||||
typeof(RectTransform), typeof(Canvas), typeof(CanvasScaler), typeof(GraphicRaycaster), typeof(CanvasGroup));
|
||||
Stretch((RectTransform)root.transform);
|
||||
SetupCanvas(root, 60);
|
||||
var panel = root.AddComponent<DataDrivenAbilityPanel>();
|
||||
var applier = root.AddComponent<UIThemeApplier>();
|
||||
if (theme != null) AssignRef(applier, "_theme", theme);
|
||||
|
||||
var overlayGo = NewUIChild(root.transform, "Overlay", out var ovRt);
|
||||
Stretch(ovRt);
|
||||
overlayGo.AddComponent<Image>().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(1120f, 760f));
|
||||
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, -48f), new Vector2(-60f, 56f));
|
||||
var title = titleGo.AddComponent<TextMeshProUGUI>();
|
||||
title.alignment = TextAlignmentOptions.Center; title.fontSize = 40f; title.raycastTarget = false;
|
||||
var titleLoc = titleGo.AddComponent<LocalizedText>();
|
||||
SetString(titleLoc, "_key", "ABL_OVERVIEW_TITLE");
|
||||
SetEnum(titleGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Text_Header);
|
||||
|
||||
// 能力格容器(网格)
|
||||
var gridGo = NewUIChild(boxGo.transform, "GridContainer", out var gridRt);
|
||||
SetRect(gridRt, C(0.5f, 0.5f), C(0.5f, 0.5f), C(0.5f, 0.5f), new Vector2(0f, 10f), new Vector2(1056f, 600f));
|
||||
var grid = gridGo.AddComponent<GridLayoutGroup>();
|
||||
// 6 列:21 项 → 4 行,落在框内不溢出(美术可在预制件再调列数/格子尺寸)
|
||||
grid.cellSize = new Vector2(160f, 130f); grid.spacing = new Vector2(16f, 14f);
|
||||
grid.childAlignment = TextAnchor.UpperCenter;
|
||||
grid.constraint = GridLayoutGroup.Constraint.FixedColumnCount; grid.constraintCount = 6;
|
||||
|
||||
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 clblGo = NewUIChild(closeGo.transform, "Label", out var clblRt);
|
||||
Stretch(clblRt);
|
||||
var clbl = clblGo.AddComponent<TextMeshProUGUI>();
|
||||
clbl.alignment = TextAlignmentOptions.Center; clbl.fontSize = 26f;
|
||||
clblGo.AddComponent<LocalizedText>();
|
||||
SetString(clblGo.GetComponent<LocalizedText>(), "_key", "BTN_BACK");
|
||||
SetEnum(clblGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Text_Primary);
|
||||
|
||||
var so = new SerializedObject(panel);
|
||||
so.FindProperty("_meta").objectReferenceValue = meta;
|
||||
so.FindProperty("_container").objectReferenceValue = gridRt;
|
||||
so.FindProperty("_cellPrefab").objectReferenceValue = cell;
|
||||
so.FindProperty("_btnClose").objectReferenceValue = closeBtn;
|
||||
so.FindProperty("_onAbilityUnlocked").objectReferenceValue = FindAsset<AbilityTypeEventChannelSO>("EVT_AbilityUnlocked");
|
||||
so.ApplyModifiedPropertiesWithoutUndo();
|
||||
|
||||
root.SetActive(false);
|
||||
PrefabUtility.SaveAsPrefabAsset(root, $"{PrefabDir}/{PanelName}.prefab");
|
||||
Object.DestroyImmediate(root);
|
||||
report.Add($"{PrefabDir}/{PanelName}.prefab");
|
||||
}
|
||||
|
||||
// ── 助手 ─────────────────────────────────────────────────────────────
|
||||
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 g in AssetDatabase.FindAssets($"{name} t:{typeof(T).Name}")) { var pa = AssetDatabase.GUIDToAssetPath(g); var a = AssetDatabase.LoadAssetAtPath<T>(pa); 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) { var 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; } }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user