Files
zeling_v2/Assets/_Game/Scripts/Editor/UI/UIAbilityOverviewScaffold.cs
2026-06-08 11:26:17 +08:00

251 lines
16 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using BaseGames.UI;
using BaseGames.UI.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.Groupstype, 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; } }
}
}