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,216 @@
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
using BaseGames.UI;
using BaseGames.Enemies;
using BaseGames.Localization;
using BaseGames.Editor.Localization;
namespace BaseGames.Editor.UI
{
/// <summary>
/// 图鉴条目同步 / 校验工具(策划友好)。从现有敌人数据自动补全 <see cref="BestiaryDatabaseSO"/>
/// <list type="bullet">
/// <item>来源 AAssets/_Game/Data/Enemies/{id}/ 下的 EnemyStatsSO文件夹名即 enemyId读 MaxHP</item>
/// <item>来源 B含 EnemyBase 的预制件(权威 _enemyId + StatsSO.MaxHP + Boss 判定),优先级高于 A</item>
/// </list>
/// 仅"补全缺失":已存在条目的策划字段(名/图标/描述/分级)一律保留,只在 displayMaxHP 为 0 时回填。
/// 新增条目自动生成 ENM_{id}_NAME / ENM_{id}_DESC 占位本地化(已存在不覆盖)。
/// 孤儿条目enemyId 无对应敌人数据,常为手敲错字)仅警告,不删除。
///
/// 菜单BaseGames/UI/控件库/同步图鉴条目(从敌人数据) · 校验图鉴条目
/// </summary>
public static class BestiarySyncTool
{
private const string DbPath = "Assets/_Game/Data/UI/UI_BestiaryDatabase.asset";
private const string EnemiesDir = "Assets/_Game/Data/Enemies";
private struct Src { public int hp; public bool boss; public string from; }
[MenuItem("BaseGames/UI/控件库/同步图鉴条目(从敌人数据)", priority = 300)]
public static void Sync()
{
var db = LoadDb();
if (db == null) return;
var discovered = Discover(out var srcLog);
var so = new SerializedObject(db);
var arr = so.FindProperty("_entries");
// 现有条目 id → 索引
var existing = new Dictionary<string, int>();
for (int i = 0; i < arr.arraySize; i++)
{
string id = arr.GetArrayElementAtIndex(i).FindPropertyRelative("enemyId").stringValue;
if (!string.IsNullOrEmpty(id) && !existing.ContainsKey(id)) existing[id] = i;
}
int added = 0, hpFilled = 0;
var newLocKeys = new List<string>();
foreach (var kv in discovered.OrderBy(k => k.Key))
{
string id = kv.Key;
if (existing.TryGetValue(id, out int idx))
{
// 已存在:仅在 HP 为 0 时回填,其余策划字段保留
var el = arr.GetArrayElementAtIndex(idx);
var hpProp = el.FindPropertyRelative("displayMaxHP");
if (hpProp.intValue == 0 && kv.Value.hp > 0) { hpProp.intValue = kv.Value.hp; hpFilled++; }
continue;
}
// 新增
int n = arr.arraySize;
arr.arraySize = n + 1;
var nel = arr.GetArrayElementAtIndex(n);
nel.FindPropertyRelative("enemyId").stringValue = id;
nel.FindPropertyRelative("displayNameKey").stringValue = $"ENM_{id}_NAME";
nel.FindPropertyRelative("descKey").stringValue = $"ENM_{id}_DESC";
nel.FindPropertyRelative("icon").objectReferenceValue = null;
nel.FindPropertyRelative("tier").enumValueIndex = kv.Value.boss ? 2 : 0; // Boss : Normal
nel.FindPropertyRelative("region").stringValue = "";
nel.FindPropertyRelative("fullLoreKillCount").intValue = 1;
nel.FindPropertyRelative("displayMaxHP").intValue = kv.Value.hp;
newLocKeys.Add(id);
added++;
}
so.ApplyModifiedPropertiesWithoutUndo();
EditorUtility.SetDirty(db);
int locAdded = SeedLocalization(newLocKeys);
// 孤儿DB 有、敌人数据无)
var orphans = existing.Keys.Where(id => !discovered.ContainsKey(id)).OrderBy(s => s).ToList();
AssetDatabase.SaveAssets();
Selection.activeObject = db;
EditorGUIUtility.PingObject(db);
var sb = new System.Text.StringBuilder();
sb.AppendLine($"[图鉴同步] 完成:新增 {added} 条 · 回填 HP {hpFilled} 条 · 本地化新增 {locAdded} 条 · 现有总计 {arr.arraySize} 条。");
sb.AppendLine($"发现敌人来源 {discovered.Count} 个:");
foreach (var l in srcLog) sb.AppendLine(" • " + l);
if (orphans.Count > 0)
sb.AppendLine($"⚠ 孤儿条目 {orphans.Count} 个enemyId 无对应敌人数据,请核对拼写,未自动删除):{string.Join(", ", orphans)}");
Debug.Log(sb.ToString(), db);
EditorUtility.DisplayDialog("图鉴同步",
$"新增 {added} 条,回填 HP {hpFilled} 条。\n现有总计 {arr.arraySize} 条。\n" +
(orphans.Count > 0 ? $"⚠ {orphans.Count} 个孤儿条目(见 Console。" : "无孤儿条目。") +
"\n\n详情见 Console已选中 UI_BestiaryDatabase。", "好");
}
[MenuItem("BaseGames/UI/控件库/校验图鉴条目", priority = 301)]
public static void Validate()
{
var db = LoadDb();
if (db == null) return;
var discovered = Discover(out _);
var so = new SerializedObject(db);
var arr = so.FindProperty("_entries");
var dbIds = new HashSet<string>();
var dupes = new List<string>();
var noKey = new List<string>();
for (int i = 0; i < arr.arraySize; i++)
{
var el = arr.GetArrayElementAtIndex(i);
string id = el.FindPropertyRelative("enemyId").stringValue;
if (string.IsNullOrEmpty(id)) { noKey.Add($"#{i}"); continue; }
if (!dbIds.Add(id)) dupes.Add(id);
if (string.IsNullOrEmpty(el.FindPropertyRelative("displayNameKey").stringValue))
noKey.Add(id);
}
var orphans = dbIds.Where(id => !discovered.ContainsKey(id)).OrderBy(s => s).ToList();
var missing = discovered.Keys.Where(id => !dbIds.Contains(id)).OrderBy(s => s).ToList();
var sb = new System.Text.StringBuilder("[图鉴校验]\n");
sb.AppendLine($"DB 条目 {arr.arraySize} · 敌人数据来源 {discovered.Count} · 已覆盖 {dbIds.Count - orphans.Count}/{discovered.Count}");
sb.AppendLine(missing.Count > 0 ? $"✗ 缺失(有敌人数据无条目){missing.Count}{string.Join(", ", missing)}" : "✓ 无缺失");
sb.AppendLine(orphans.Count > 0 ? $"⚠ 孤儿(条目无敌人数据){orphans.Count}{string.Join(", ", orphans)}" : "✓ 无孤儿");
sb.AppendLine(dupes.Count > 0 ? $"✗ 重复 enemyId {dupes.Count}{string.Join(", ", dupes)}" : "✓ 无重复");
sb.AppendLine(noKey.Count > 0 ? $"⚠ 缺 enemyId/名称 Key{string.Join(", ", noKey)}" : "✓ id/名称 Key 齐全");
Debug.Log(sb.ToString(), db);
EditorUtility.DisplayDialog("图鉴校验",
$"DB {arr.arraySize} 条 / 敌人数据 {discovered.Count} 个\n" +
$"缺失 {missing.Count} · 孤儿 {orphans.Count} · 重复 {dupes.Count}\n\n详情见 Console。",
missing.Count == 0 && orphans.Count == 0 && dupes.Count == 0 ? "全部通过" : "好");
}
// ── 发现敌人来源 ─────────────────────────────────────────────────────
private static Dictionary<string, Src> Discover(out List<string> log)
{
var map = new Dictionary<string, Src>();
log = new List<string>();
// 来源 AData/Enemies/{id}/ 子文件夹 + EnemyStatsSO
if (AssetDatabase.IsValidFolder(EnemiesDir))
{
foreach (var folder in AssetDatabase.GetSubFolders(EnemiesDir))
{
string id = folder.Substring(folder.LastIndexOf('/') + 1);
EnemyStatsSO stats = null;
foreach (var g in AssetDatabase.FindAssets("t:EnemyStatsSO", new[] { folder }))
{
stats = AssetDatabase.LoadAssetAtPath<EnemyStatsSO>(AssetDatabase.GUIDToAssetPath(g));
if (stats != null) break;
}
if (stats == null) continue; // 无 EnemyStatsSO 的文件夹不算敌人(跳过 Stats 等杂项目录)
map[id] = new Src { hp = stats.MaxHP, boss = false, from = "data" };
log.Add($"{id}data, HP={stats.MaxHP}");
}
}
// 来源 B含 EnemyBase 的预制件(权威 id优先覆盖
foreach (var g in AssetDatabase.FindAssets("t:Prefab"))
{
var go = AssetDatabase.LoadAssetAtPath<GameObject>(AssetDatabase.GUIDToAssetPath(g));
if (go == null) continue;
var eb = go.GetComponentInChildren<EnemyBase>(true);
if (eb == null || string.IsNullOrEmpty(eb.EnemyId)) continue;
int hp = eb.StatsSO != null ? eb.StatsSO.MaxHP : 0;
bool boss = eb.GetType().Name.IndexOf("Boss", System.StringComparison.OrdinalIgnoreCase) >= 0;
map[eb.EnemyId] = new Src { hp = hp, boss = boss, from = "prefab" };
log.Add($"{eb.EnemyId}prefab{(boss ? ",BOSS" : "")}, HP={hp}");
}
return map;
}
private static int SeedLocalization(List<string> ids)
{
if (ids == null || ids.Count == 0) return 0;
var zh = LocalizationFileIO.Read(Language.ChineseSimplified, LocalizationTable.UI);
var en = LocalizationFileIO.Read(Language.English, LocalizationTable.UI);
int added = 0;
foreach (var id in ids)
{
string nk = $"ENM_{id}_NAME", dk = $"ENM_{id}_DESC";
if (!zh.ContainsKey(nk)) { zh[nk] = $"敌人 {id}"; added++; }
if (!zh.ContainsKey(dk)) { zh[dk] = "(占位)该敌人的背景与弱点描述,由策划填写。"; added++; }
if (!en.ContainsKey(nk)) { en[nk] = $"Enemy {id}"; added++; }
if (!en.ContainsKey(dk)) { en[dk] = "(Placeholder) Lore and weakness, to be filled by design."; added++; }
}
if (added > 0)
{
LocalizationFileIO.Write(Language.ChineseSimplified, LocalizationTable.UI, zh);
LocalizationFileIO.Write(Language.English, LocalizationTable.UI, en);
}
return added;
}
private static BestiaryDatabaseSO LoadDb()
{
var db = AssetDatabase.LoadAssetAtPath<BestiaryDatabaseSO>(DbPath);
if (db == null)
EditorUtility.DisplayDialog("图鉴同步",
$"未找到 {DbPath}。\n请先运行「BaseGames/UI/控件库/生成图鉴(预制件 + 空表)」。", "好");
return db;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0fc04afec1231b24598e6b7bc81e7778
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -11,17 +11,30 @@ using UnityEngine.UI;
namespace BaseGames.Editor.UI
{
/// <summary>
/// 统一背包菜单脚手架(暂停菜单的 Tab Hub
/// 统一背包菜单脚手架(游戏内查看类面板的 Tab Hub
/// 在当前活动场景的 UIRoot 下生成 InventoryHub Canvas 完整层级,
/// 创建 Tab 头部栏与各 Tab 内容根,自动绑定事件频道并注册到 UIManager 面板栈。
/// 创建 Tab 头部栏与各 Tab 内容根,自动绑定事件频道并注册到 UIManager 面板栈PanelId.Inventory
/// 执行路径BaseGames ▸ Scene ▸ Setup ▸ Scaffold Inventory Hub
///
/// 设计与命名严格对照 <see cref="HUDScaffoldWizard"/>幂等、Undo 友好、报告未尽手动项。
/// 第一期 Tab结合本项目内容"工具"位由"形态技能"承载):
/// 地图(Map) · 形态技能(FormSkills) · 物品(Inventory)
/// 地图 / 形态技能:内嵌既有 standalone 预制件实例(编辑期中和自身 Canvas 链 + 隐藏 chrome
/// 单一面板逻辑两用 —— standalone 打开(全 chrome与 Hub Tab 内嵌(无 chrome。地图 standalone 仍保留 PanelId.Map。
/// 物品Hub 内联构建(与 EquipmentManager/InventoryManager 同源反应式刷新)。
/// 护符 / 能力 / 任务 / 图鉴:二期。
///
/// 设计与命名对照 <see cref="HUDScaffoldWizard"/>幂等重跑前整根清建、Undo 友好、报告未尽手动项。
/// </summary>
public static class InventoryHubScaffoldWizard
{
// Tab 顺序:Map · Inventory · Tools(护符) · Quests · Options
private static readonly string[] kTabNames = { "Map", "Inventory", "Tools", "Quests", "Options" };
// Tab 顺序:地图 / 形态技能 / 能力 / 护符 / 物品 / 任务 / 图鉴
private static readonly string[] kTabNames = { "Map", "FormSkills", "Abilities", "Charm", "Inventory", "Quests", "Bestiary" };
// 各 Tab 的内嵌 standalone 预制件名null = Hub 内联自建,不内嵌)
private static readonly string[] kEmbedPrefab = { "UI_MapPanel", "UI_FormSkillPanel", "UI_AbilityOverviewPanel", "UI_CharmPanel", null, null, "UI_BestiaryPanel" };
// 内嵌实例需隐藏的 chrome 子节点候选名(全屏遮罩 / 关闭按钮等;按名深度查找,命中才隐藏)
private static readonly string[] kChromeNames = { "Overlay", "Dim", "Btn_Close", "CloseButton", "Close" };
[MenuItem("BaseGames/Scene/Setup/Scaffold Inventory Hub", priority = 204)]
public static void ScaffoldInventoryHub()
@@ -33,21 +46,39 @@ namespace BaseGames.Editor.UI
// ── Canvas排序层 5HUD 之上、Pause 同级区间)─────────────────────
GameObject canvasGo = GetOrCreateHubCanvas("InventoryHub Canvas", 5);
// ── 幂等:整根清建(避免旧 Tab 结构残留)────────────────────────────
Transform oldRoot = canvasGo.transform.Find("InventoryHubRoot");
if (oldRoot != null) Object.DestroyImmediate(oldRoot.gameObject);
// ── Hub 根 ─────────────────────────────────────────────────────────
GameObject hubGo = GetOrCreateChild(canvasGo.transform, "InventoryHubRoot").gameObject;
GameObject hubGo = new GameObject("InventoryHubRoot", typeof(RectTransform));
Undo.RegisterCreatedObjectUndo(hubGo, "Create InventoryHubRoot");
hubGo.transform.SetParent(canvasGo.transform, false);
Stretch((RectTransform)hubGo.transform);
InventoryHubPanel hub = GetOrAddComponent<InventoryHubPanel>(hubGo);
CanvasGroup hubGroup = GetOrAddComponent<CanvasGroup>(hubGo);
RectTransform hubRect = GetOrAddComponent<RectTransform>(hubGo);
RectTransform hubRect = (RectTransform)hubGo.transform;
// 打开时冻结游戏 + 切 UI 输入;关闭时恢复(修复"开菜单后 Esc 不暂停")。
var pauseScope = GetOrAddComponent<BaseGames.UI.MenuPauseScope>(hubGo);
AssignAsset(pauseScope, "_inputReader", report, false, "InputReader");
// ── Tab 头部栏 ─────────────────────────────────────────────────────
GameObject tabBarGo = GetOrCreateChild(hubGo.transform, "TabBar").gameObject;
SetRect((RectTransform)tabBarGo.transform, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0.5f, 1f),
new Vector2(0f, -36f), new Vector2(900f, 56f));
var tabBarLayout = GetOrAddComponent<HorizontalLayoutGroup>(tabBarGo);
tabBarLayout.childForceExpandWidth = false;
tabBarLayout.childForceExpandHeight = false;
tabBarLayout.childAlignment = TextAnchor.MiddleCenter;
tabBarLayout.spacing = 8f;
// ── Tab 内容容器 ───────────────────────────────────────────────────
GameObject contentGo = GetOrCreateChild(hubGo.transform, "TabContent").gameObject;
SetRect((RectTransform)contentGo.transform, Vector2.zero, Vector2.one, new Vector2(0.5f, 0.5f),
Vector2.zero, Vector2.zero);
((RectTransform)contentGo.transform).offsetMin = new Vector2(0f, 0f);
((RectTransform)contentGo.transform).offsetMax = new Vector2(0f, -72f); // 留出顶部 TabBar
var headerButtons = new Button[kTabNames.Length];
var headerHighlights = new GameObject[kTabNames.Length];
@@ -57,28 +88,52 @@ namespace BaseGames.Editor.UI
{
// Tab 头按钮
GameObject headerGo = GetOrCreateChild(tabBarGo.transform, $"Tab_{kTabNames[i]}").gameObject;
GetOrAddComponent<Image>(headerGo);
var headerLe = GetOrAddComponent<LayoutElement>(headerGo);
headerLe.preferredWidth = 200f; headerLe.preferredHeight = 48f;
var headerImg = GetOrAddComponent<Image>(headerGo);
headerImg.color = new Color(0.12f, 0.13f, 0.17f, 0.88f); // 深底,保证白字可见
headerButtons[i] = GetOrAddComponent<Button>(headerGo);
GameObject hlGo = GetOrCreateChild(headerGo.transform, "Highlight").gameObject;
GetOrAddComponent<Image>(hlGo);
var hlImg = GetOrAddComponent<Image>(hlGo);
hlImg.color = new Color(0.85f, 0.80f, 0.55f, 0.45f); // 选中高亮(米金,覆盖深底)
Stretch((RectTransform)hlGo.transform);
hlGo.SetActive(false);
headerHighlights[i] = hlGo;
GameObject labelGo = GetOrCreateChild(headerGo.transform, "Label").gameObject;
Stretch((RectTransform)labelGo.transform);
var label = GetOrAddComponent<TextMeshProUGUI>(labelGo);
label.text = kTabNames[i];
label.color = Color.white;
label.alignment = TextAlignmentOptions.Center; label.fontSize = 24f; label.raycastTarget = false;
// Tab 头标签走本地化 key地图/形态技能/物品)
var loc = GetOrAddComponent<BaseGames.Localization.LocalizedText>(labelGo);
SetString(loc, "_key", TabLabelKey(kTabNames[i]));
label.text = kTabNames[i]; // 编辑期占位,运行期由 LocalizedText 覆盖
// Tab 内容根
// Tab 内容根(铺满内容容器)
GameObject tabRoot = GetOrCreateChild(contentGo.transform, $"Content_{kTabNames[i]}").gameObject;
Stretch((RectTransform)tabRoot.transform);
contentRoots[i] = tabRoot;
tabRoot.SetActive(i == 0);
}
// ── 各 Tab 专属内容 ────────────────────────────────────────────────
BuildInventoryTab(contentRoots[1], report); // Inventory
BuildQuestsTab(contentRoots[3], report); // Quests
report.Add("Map TabContent_Map将现有 MapPanel 预制 / 节点作为子物体放入,或在此挂载 MapPanel 组件并配置。");
report.Add("Tools TabContent_Tools将现有 CharmEquipPanel 节点作为子物体放入。");
report.Add("Options TabContent_Options将现有 SettingsPanelController 节点作为子物体放入。");
// ── 各 Tab 内容 ────────────────────────────────────────────────────
for (int i = 0; i < kTabNames.Length; i++)
{
if (kEmbedPrefab[i] != null)
{
EmbedPrefabAsTab(contentRoots[i], kEmbedPrefab[i], report);
}
else if (kTabNames[i] == "Inventory")
{
AddTabBackground(contentRoots[i]); // 内联 Tab 补深底,视觉对齐内嵌 Tab
BuildInventoryTab(contentRoots[i], report);
}
else if (kTabNames[i] == "Quests")
{
AddTabBackground(contentRoots[i]);
BuildQuestsTab(contentRoots[i], report);
}
}
// ── Hub 字段绑定 ───────────────────────────────────────────────────
AssignRef(hub, "_rootGroup", hubGroup);
@@ -94,10 +149,79 @@ namespace BaseGames.Editor.UI
hubGo.SetActive(false); // 面板默认隐藏,由 UIManager.OpenPanel 激活
Undo.CollapseUndoOperations(undoGroup);
MarkDirtyAndLog("Inventory Hub 脚手架", canvasGo, report);
MarkDirtyAndLog("Inventory Hub 脚手架(地图/形态技能/物品)", canvasGo, report);
}
// ── Inventory Tab道具背包────────────────────────────────────────────
private static string TabLabelKey(string tab) => tab switch
{
"Map" => "MENU_MAP",
"FormSkills" => "FORMSKILL_TITLE",
"Abilities" => "ABL_OVERVIEW_TITLE",
"Charm" => "MENU_CHARM",
"Inventory" => "MENU_INVENTORY",
"Quests" => "MENU_QUESTS",
"Bestiary" => "MENU_BESTIARY",
_ => tab,
};
// ── 内嵌 standalone 预制件作 Tab 内容(编辑期中和 Canvas 链 + 隐藏 chrome──
private static void EmbedPrefabAsTab(GameObject contentRoot, string prefabName, List<string> report)
{
GameObject prefab = FindPrefab(prefabName);
if (prefab == null)
{
report.Add($"内嵌预制件 {prefabName} 未找到,{contentRoot.name} 留空(地图需先运行 Scaffold Map UI 并抽出 UI_MapPanel形态技能需先运行「生成形态技能一览」。");
return;
}
var inst = (GameObject)PrefabUtility.InstantiatePrefab(prefab, contentRoot.transform);
Undo.RegisterCreatedObjectUndo(inst, $"Embed {prefabName}");
// 解除预制件连接,便于就地中和 chromeTab 内嵌为快照,刷新重跑脚手架即可)
PrefabUtility.UnpackPrefabInstance(inst, PrefabUnpackMode.Completely, InteractionMode.AutomatedAction);
// 中和自身 Canvas 链 —— 改用 Hub 的单一 Canvas 渲染(必须移除组件,禁用会令子树整体不渲染)
DestroyIfPresent<GraphicRaycaster>(inst);
DestroyIfPresent<CanvasScaler>(inst);
DestroyIfPresent<Canvas>(inst);
// 铺满 Tab 内容区
if (inst.transform is RectTransform rt)
{
rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.one;
rt.offsetMin = Vector2.zero; rt.offsetMax = Vector2.zero;
rt.localScale = Vector3.one;
}
// 隐藏 chrome全屏遮罩 / 关闭按钮)
foreach (string n in kChromeNames)
{
Transform t = FindDeep(inst.transform, n);
if (t != null) t.gameObject.SetActive(false);
}
inst.SetActive(true);
report.Add($"{contentRoot.name} ← 内嵌 {prefabName}(已中和 Canvas 链,隐藏 chrome。");
}
// ── 内联 Tab 背景(对齐内嵌 Tab 的 DialogBox 深底,避免透出下层)──────────
private static void AddTabBackground(GameObject root)
{
if (root.transform.Find("Background") != null) return; // 幂等
GameObject bgGo = new GameObject("Background", typeof(RectTransform));
Undo.RegisterCreatedObjectUndo(bgGo, "Create Tab Background");
bgGo.transform.SetParent(root.transform, false);
bgGo.transform.SetAsFirstSibling();
var rt = (RectTransform)bgGo.transform;
rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.one;
rt.offsetMin = new Vector2(40f, 40f); rt.offsetMax = new Vector2(-40f, -40f);
var img = bgGo.AddComponent<Image>();
img.sprite = AssetDatabase.GetBuiltinExtraResource<Sprite>("UI/Skin/UISprite.psd");
img.type = Image.Type.Sliced;
img.color = new Color(0.10f, 0.11f, 0.15f, 0.98f);
img.raycastTarget = false;
}
// ── Inventory Tab道具背包Hub 内联)──────────────────────────────────
private static void BuildInventoryTab(GameObject root, List<string> report)
{
ItemInventoryPanel panel = GetOrAddComponent<ItemInventoryPanel>(root);
@@ -155,7 +279,7 @@ namespace BaseGames.Editor.UI
AssignAsset(panel, "_onInventoryChanged", report, false, "EVT_InventoryChanged");
}
// ── Quests Tab任务日志)───────────────────────────────────────────────
// ── Quests Tab任务日志Hub 内联)─────────────────────────────────────
private static void BuildQuestsTab(GameObject root, List<string> report)
{
QuestLogPanel panel = GetOrAddComponent<QuestLogPanel>(root);
@@ -218,7 +342,6 @@ namespace BaseGames.Editor.UI
return;
}
// 检查是否已注册 PanelId.Inventory幂等
int inventoryId = (int)PanelId.Inventory;
for (int i = 0; i < panelsProp.arraySize; i++)
{
@@ -227,6 +350,7 @@ namespace BaseGames.Editor.UI
{
el.FindPropertyRelative("root").objectReferenceValue = hubGo;
so.ApplyModifiedPropertiesWithoutUndo();
AssignAsset(uiManager, "_onInventoryOpen", report, false, "EVT_InventoryOpen");
return;
}
}
@@ -259,8 +383,36 @@ namespace BaseGames.Editor.UI
}
// ─────────────────────────────────────────────────────────────────────
// Helpers(对照 HUDScaffoldWizard
// Helpers
// ─────────────────────────────────────────────────────────────────────
private static GameObject FindPrefab(string prefabName)
{
foreach (string guid in AssetDatabase.FindAssets($"{prefabName} t:Prefab"))
{
string path = AssetDatabase.GUIDToAssetPath(guid);
var go = AssetDatabase.LoadAssetAtPath<GameObject>(path);
if (go != null && go.name == prefabName) return go;
}
return null;
}
private static void DestroyIfPresent<T>(GameObject go) where T : Component
{
var c = go.GetComponent<T>();
if (c != null) Object.DestroyImmediate(c);
}
private static Transform FindDeep(Transform root, string name)
{
if (root.name == name) return root;
for (int i = 0; i < root.childCount; i++)
{
Transform found = FindDeep(root.GetChild(i), name);
if (found != null) return found;
}
return null;
}
private static GameObject GetOrCreateHubCanvas(string name, int sortOrder)
{
Scene scene = SceneManager.GetActiveScene();
@@ -301,7 +453,7 @@ namespace BaseGames.Editor.UI
{
Transform child = parent.Find(name);
if (child != null) return child;
GameObject go = new GameObject(name);
GameObject go = new GameObject(name, typeof(RectTransform));
Undo.RegisterCreatedObjectUndo(go, $"Create {name}");
go.transform.SetParent(parent, false);
return go.transform;
@@ -313,6 +465,26 @@ namespace BaseGames.Editor.UI
return c != null ? c : Undo.AddComponent<T>(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 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 propertyName, Object value)
{
var so = new SerializedObject(target);

View 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.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; } }
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: cbcf9174cf741d6499eea7b8443f18ea
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,266 @@
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_BestiaryCell</c> + <c>UI_BestiaryPanel</c> 预制件 + 空 <c>UI_BestiaryDatabase</c> 表 + BESTIARY_* 中英文案。
/// 面板据 BestiaryDatabaseSO 列出全部敌人,经 IBestiaryService 读已发现态(已发现亮/未发现 ??? + 剪影 + 锁),
/// 订阅 EVT_BestiaryUpdated 实时刷新;点击格子在右侧详情区展示。
///
/// 数据:策划在 UI_BestiaryDatabase 增删条目enemyId 须与 EnemyBase._enemyId 一致),或用「同步图鉴条目」工具自动补。
/// 菜单BaseGames/UI/控件库/生成图鉴(预制件 + 空表)
/// </summary>
public static class UIBestiaryScaffold
{
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_BestiaryPanel";
private const string CellName = "UI_BestiaryCell";
private const string DbName = "UI_BestiaryDatabase";
[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 db = EnsureDatabase(report);
SeedLocalization(report);
var cell = BuildCellPrefab(theme, report);
BuildPanelPrefab(theme, db, cell, report);
AssetDatabase.SaveAssets(); AssetDatabase.Refresh();
var sb = new System.Text.StringBuilder("[UIBestiary] 图鉴已生成:\n");
foreach (var r in report) sb.AppendLine(" • " + r);
sb.AppendLine("策划在 UI_BestiaryDatabase 填敌人条目enemyId 同 EnemyBase._enemyId重跑「Scaffold Inventory Hub」收编为图鉴 Tab。");
Debug.Log(sb.ToString());
}
private static BestiaryDatabaseSO EnsureDatabase(List<string> report)
{
string path = $"{ConfigDir}/{DbName}.asset";
var db = AssetDatabase.LoadAssetAtPath<BestiaryDatabaseSO>(path);
if (db == null)
{
db = ScriptableObject.CreateInstance<BestiaryDatabaseSO>();
AssetDatabase.CreateAsset(db, path);
report.Add($"{path}(空表,待策划/同步工具填充)");
}
return db;
}
private static void SeedLocalization(List<string> report)
{
var zh = new Dictionary<string, string>
{
{ "MENU_BESTIARY", "图鉴" }, { "BESTIARY_TITLE", "图鉴" },
{ "BESTIARY_LOCKED", "???" }, { "BESTIARY_LOCKED_DESC", "尚未发现该敌人。" },
{ "BESTIARY_HP", "生命" }, { "BESTIARY_KILLS", "击杀" },
};
var en = new Dictionary<string, string>
{
{ "MENU_BESTIARY", "Bestiary" }, { "BESTIARY_TITLE", "Bestiary" },
{ "BESTIARY_LOCKED", "???" }, { "BESTIARY_LOCKED_DESC", "Not yet discovered." },
{ "BESTIARY_HP", "HP" }, { "BESTIARY_KILLS", "Kills" },
};
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 BestiaryCellView BuildCellPrefab(UIThemeSO theme, List<string> report)
{
var root = new GameObject(CellName, typeof(RectTransform), typeof(CanvasGroup));
((RectTransform)root.transform).sizeDelta = new Vector2(170f, 130f);
var view = root.AddComponent<BestiaryCellView>();
var bg = root.AddComponent<Image>();
bg.sprite = Std(); bg.type = Image.Type.Sliced; bg.color = new Color(1f, 1f, 1f, 0.05f);
var btn = root.AddComponent<Button>(); btn.targetGraphic = bg;
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, -44f), 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, 14f), new Vector2(154f, 40f));
var nameTmp = nameGo.AddComponent<TextMeshProUGUI>();
nameTmp.alignment = TextAlignmentOptions.Center; nameTmp.fontSize = 18f; nameTmp.enableWordWrapping = true;
nameTmp.raycastTarget = false;
var loc = nameGo.AddComponent<LocalizedText>();
SetEnum(nameGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Text_Primary);
var lockGo = NewUIChild(root.transform, "LockedOverlay", out var lockRt);
Stretch(lockRt);
var lockImg = lockGo.AddComponent<Image>(); lockImg.color = new Color(0f, 0f, 0f, 0.5f); 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.FindProperty("_selectButton").objectReferenceValue = btn;
so.ApplyModifiedPropertiesWithoutUndo();
string path = $"{ControlsDir}/{CellName}.prefab";
var asset = PrefabUtility.SaveAsPrefabAsset(root, path);
Object.DestroyImmediate(root);
report.Add(path);
return asset.GetComponent<BestiaryCellView>();
}
// ── 面板预制件 ───────────────────────────────────────────────────────
private static void BuildPanelPrefab(UIThemeSO theme, BestiaryDatabaseSO db, BestiaryCellView 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, 62);
var panel = root.AddComponent<BestiaryPanel>();
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(1280f, 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, -46f), new Vector2(600f, 54f));
var title = titleGo.AddComponent<TextMeshProUGUI>();
title.alignment = TextAlignmentOptions.Center; title.fontSize = 40f; title.raycastTarget = false;
titleGo.AddComponent<LocalizedText>();
SetString(titleGo.GetComponent<LocalizedText>(), "_key", "BESTIARY_TITLE");
SetEnum(titleGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Text_Header);
// 完成度(右上)
var compGo = NewUIChild(boxGo.transform, "Completion", out var compRt);
SetRect(compRt, C(1f, 1f), C(1f, 1f), C(1f, 1f), new Vector2(-40f, -52f), new Vector2(300f, 40f));
var comp = compGo.AddComponent<TextMeshProUGUI>();
comp.alignment = TextAlignmentOptions.Right; comp.fontSize = 26f; comp.raycastTarget = false;
SetEnum(compGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Text_Secondary);
// 敌人格网格(左)
var gridGo = NewUIChild(boxGo.transform, "GridContainer", out var gridRt);
SetRect(gridRt, C(0f, 1f), C(0f, 1f), C(0f, 1f), new Vector2(40f, -120f), new Vector2(760f, 600f));
var grid = gridGo.AddComponent<GridLayoutGroup>();
grid.cellSize = new Vector2(170f, 130f); grid.spacing = new Vector2(14f, 14f);
grid.childAlignment = TextAnchor.UpperLeft;
grid.constraint = GridLayoutGroup.Constraint.FixedColumnCount; grid.constraintCount = 4;
// 详情区(右)
var detailGo = NewUIChild(boxGo.transform, "DetailPane", out var detailRt);
SetRect(detailRt, C(1f, 1f), C(1f, 1f), C(1f, 1f), new Vector2(-40f, -120f), new Vector2(420f, 600f));
var detailBg = detailGo.AddComponent<Image>(); detailBg.color = new Color(1f, 1f, 1f, 0.03f);
detailBg.sprite = Std(); detailBg.type = Image.Type.Sliced;
var dIconGo = NewUIChild(detailGo.transform, "DetailIcon", out var dIconRt);
SetRect(dIconRt, C(0.5f, 1f), C(0.5f, 1f), C(0.5f, 1f), new Vector2(0f, -30f), new Vector2(160f, 160f));
var dIcon = dIconGo.AddComponent<Image>(); dIcon.preserveAspect = true; dIcon.raycastTarget = false;
var dNameGo = NewUIChild(detailGo.transform, "DetailName", out var dNameRt);
SetRect(dNameRt, C(0.5f, 1f), C(0.5f, 1f), C(0.5f, 1f), new Vector2(0f, -210f), new Vector2(384f, 44f));
var dName = dNameGo.AddComponent<TextMeshProUGUI>();
dName.alignment = TextAlignmentOptions.Center; dName.fontSize = 28f; dName.raycastTarget = false;
dNameGo.AddComponent<LocalizedText>();
SetEnum(dNameGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Text_Header);
var dStatsGo = NewUIChild(detailGo.transform, "DetailStats", out var dStatsRt);
SetRect(dStatsRt, C(0.5f, 1f), C(0.5f, 1f), C(0.5f, 1f), new Vector2(0f, -258f), new Vector2(384f, 34f));
var dStats = dStatsGo.AddComponent<TextMeshProUGUI>();
dStats.alignment = TextAlignmentOptions.Center; dStats.fontSize = 20f; dStats.raycastTarget = false;
SetEnum(dStatsGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Text_Secondary);
var dDescGo = NewUIChild(detailGo.transform, "DetailDesc", out var dDescRt);
SetRect(dDescRt, C(0.5f, 1f), C(0.5f, 1f), C(0.5f, 1f), new Vector2(0f, -310f), new Vector2(372f, 260f));
var dDesc = dDescGo.AddComponent<TextMeshProUGUI>();
dDesc.alignment = TextAlignmentOptions.TopLeft; dDesc.fontSize = 18f; dDesc.enableWordWrapping = true; dDesc.raycastTarget = false;
SetEnum(dDescGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Text_Secondary);
// 关闭
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, 32f), 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);
// 绑定 BestiaryPanel
var so = new SerializedObject(panel);
so.FindProperty("_database").objectReferenceValue = db;
so.FindProperty("_container").objectReferenceValue = gridRt;
so.FindProperty("_cellPrefab").objectReferenceValue = cell;
so.FindProperty("_completionText").objectReferenceValue = comp;
so.FindProperty("_btnClose").objectReferenceValue = closeBtn;
so.FindProperty("_detailIcon").objectReferenceValue = dIcon;
so.FindProperty("_detailName").objectReferenceValue = dNameGo.GetComponent<LocalizedText>();
so.FindProperty("_detailDesc").objectReferenceValue = dDesc;
so.FindProperty("_detailStats").objectReferenceValue = dStats;
so.FindProperty("_onBestiaryUpdated").objectReferenceValue = FindAsset<StringEventChannelSO>("EVT_BestiaryUpdated");
so.ApplyModifiedPropertiesWithoutUndo();
root.SetActive(false);
PrefabUtility.SaveAsPrefabAsset(root, $"{PrefabDir}/{PanelName}.prefab");
Object.DestroyImmediate(root);
report.Add($"{PrefabDir}/{PanelName}.prefab");
}
// ── 助手(对照 UIAbilityOverviewScaffold──────────────────────────────
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; } }
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 643450ffe43a0124f9284b664b9f3e28
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

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; }
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: cc9108bb3994a074aa6a534640535f97
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,305 @@
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using BaseGames.UI;
using BaseGames.UI.Theme;
using BaseGames.Skills;
using BaseGames.Core.Events;
using BaseGames.Localization;
using BaseGames.Editor.Localization;
namespace BaseGames.Editor.UI
{
/// <summary>
/// 形态技能一览脚手架:生成 <c>UI_FormSkillPanel</c> 预制件,并为共享数据库 <c>FormSkillDatabase</c>
/// 补形态标签 key + 中英文案。面板只读展示「每形态 3 技能(名称/说明/消耗/冷却)」,按形态翻页。
/// 数据驱动:技能数据读 <see cref="FormSkillDatabaseSO"/>(与玩家 SkillManager 同一权威源)。
///
/// 美术 → 改 UI_FormSkillPanel 预制件样式;策划 → 改 FormSkillDatabase 的技能引用即全生效。
/// 菜单BaseGames/UI/控件库/生成形态技能一览(预制件)
/// </summary>
public static class UIFormSkillScaffold
{
private const string PrefabDir = "Assets/_Game/Prefabs/UI";
private const string ThemePath = "Assets/_Game/Data/UI/Themes/UI_Theme_Default.asset";
private const string DbPath = "Assets/_Game/Data/Skills/FormSkillDatabase.asset";
private const string PrefabName = "UI_FormSkillPanel";
[MenuItem("BaseGames/UI/控件库/生成形态技能一览(预制件)")]
public static void GeneratePrefab()
{
EnsureFolder(PrefabDir);
var report = new List<string>();
var theme = AssetDatabase.LoadAssetAtPath<UIThemeSO>(ThemePath);
var db = EnsureDatabaseLabels(report);
SeedLocalization(report);
BuildPrefab(theme, db, report);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
var sb = new System.Text.StringBuilder("[UIFormSkill] 形态技能一览已生成:\n");
foreach (var r in report) sb.AppendLine(" • " + r);
sb.AppendLine("技能数据读 FormSkillDatabase与玩家 SkillManager 共享);策划在该资产填各形态技能引用即全生效。");
Debug.Log(sb.ToString());
}
// 给数据库 3 条目补 formLabelKey仅空时填不覆盖
private static FormSkillDatabaseSO EnsureDatabaseLabels(List<string> report)
{
var db = AssetDatabase.LoadAssetAtPath<FormSkillDatabaseSO>(DbPath);
if (db == null) { report.Add($"缺少 {DbPath}(请先建 FormSkillDatabase 资产)。"); return null; }
var so = new SerializedObject(db);
var entries = so.FindProperty("_entries");
// formType 枚举序号 0/1/2 = TianHun/DiHun/MingHun
string[] keyByIndex = { "FORM_TIANHUN", "FORM_DIHUN", "FORM_MINGHUN" };
bool changed = false;
for (int i = 0; i < entries.arraySize; i++)
{
var el = entries.GetArrayElementAtIndex(i);
var labelProp = el.FindPropertyRelative("formLabelKey");
int ft = el.FindPropertyRelative("formType").enumValueIndex;
if (labelProp != null && string.IsNullOrEmpty(labelProp.stringValue) && ft >= 0 && ft < keyByIndex.Length)
{
labelProp.stringValue = keyByIndex[ft];
changed = true;
}
}
if (changed) { so.ApplyModifiedPropertiesWithoutUndo(); report.Add($"{DbPath}:已补形态标签 key"); }
return db;
}
private static void SeedLocalization(List<string> report)
{
var zh = new Dictionary<string, string>
{
{ "FORM_TIANHUN", "天魂" }, { "FORM_DIHUN", "地魂" }, { "FORM_MINGHUN", "命魂" },
{ "FORMSKILL_TITLE", "形态技能" },
};
var en = new Dictionary<string, string>
{
{ "FORM_TIANHUN", "Sky Soul" }, { "FORM_DIHUN", "Earth Soul" }, { "FORM_MINGHUN", "Death Soul" },
{ "FORMSKILL_TITLE", "Form Skills" },
};
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, FormSkillDatabaseSO db, List<string> report)
{
var root = new GameObject(PrefabName,
typeof(RectTransform), typeof(Canvas), typeof(CanvasScaler), typeof(GraphicRaycaster), typeof(CanvasGroup));
var rootRt = (RectTransform)root.transform;
Stretch(rootRt);
SetupCanvas(root, 60);
var panel = root.AddComponent<FormSkillPanel>();
var applier = root.AddComponent<UIThemeApplier>();
if (theme != null) AssignRef(applier, "_theme", theme);
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(1040f, 660f));
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, "FormTitle", out var titleRt);
SetRect(titleRt, C(0.5f, 1f), C(0.5f, 1f), C(0.5f, 1f), new Vector2(0f, -54f), new Vector2(560f, 64f));
var title = titleGo.AddComponent<TextMeshProUGUI>();
title.alignment = TextAlignmentOptions.Center; title.fontSize = 40f; title.raycastTarget = false;
SetEnum(titleGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Text_Header);
var dotGo = NewUIChild(titleGo.transform, "ActiveDot", out var dotRt);
SetRect(dotRt, C(0.5f, 0f), C(0.5f, 0f), C(0.5f, 1f), new Vector2(0f, -4f), new Vector2(16f, 16f));
var dot = dotGo.AddComponent<Image>(); dot.color = new Color(0.85f, 0.80f, 0.55f, 1f);
SetEnum(dotGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Graphic_Accent);
dotGo.SetActive(false);
var prevBtn = MakeArrowButton(boxGo.transform, "PrevBtn", "", new Vector2(-480f, 30f));
var nextBtn = MakeArrowButton(boxGo.transform, "NextBtn", "", new Vector2( 480f, 30f));
// 3 技能槽(横排)
var slotsGo = NewUIChild(boxGo.transform, "Slots", out var slotsRt);
SetRect(slotsRt, C(0.5f, 0.5f), C(0.5f, 0.5f), C(0.5f, 0.5f), new Vector2(0f, 10f), new Vector2(960f, 380f));
var hlg = slotsGo.AddComponent<HorizontalLayoutGroup>();
hlg.spacing = 16f; hlg.childAlignment = TextAnchor.MiddleCenter;
hlg.childControlWidth = true; hlg.childControlHeight = true;
hlg.childForceExpandWidth = true; hlg.childForceExpandHeight = true;
var soul = BuildSlot(slotsGo.transform, "SoulSlot");
var spirit1 = BuildSlot(slotsGo.transform, "SpiritSlot1");
var spirit2 = BuildSlot(slotsGo.transform, "SpiritSlot2");
// 关闭按钮
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, 40f), 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);
// 绑定 FormSkillPanel
var so = new SerializedObject(panel);
so.FindProperty("_database").objectReferenceValue = db;
so.FindProperty("_formTitleText").objectReferenceValue = title;
so.FindProperty("_prevFormBtn").objectReferenceValue = prevBtn;
so.FindProperty("_nextFormBtn").objectReferenceValue = nextBtn;
so.FindProperty("_activeFormIndicator").objectReferenceValue = dotGo;
so.FindProperty("_btnClose").objectReferenceValue = closeBtn;
so.FindProperty("_onFormChanged").objectReferenceValue = FindAsset<IntEventChannelSO>("EVT_FormChanged");
BindSlot(so, "_soulSlot", soul);
BindSlot(so, "_spiritSlot1", spirit1);
BindSlot(so, "_spiritSlot2", spirit2);
so.ApplyModifiedPropertiesWithoutUndo();
root.SetActive(false); // 面板由 UIManager/导航激活
PrefabUtility.SaveAsPrefabAsset(root, $"{PrefabDir}/{PrefabName}.prefab");
Object.DestroyImmediate(root);
report.Add($"{PrefabDir}/{PrefabName}.prefab");
}
// 技能槽子树root + icon/name/desc/cost/cooldown
private struct SlotRefs { public GameObject root; public Image icon; public TMP_Text name, desc, cost, cd; }
private static SlotRefs BuildSlot(Transform parent, string name)
{
var r = new SlotRefs();
var slotGo = NewUIChild(parent, name, out var slotRt);
var bg = slotGo.AddComponent<Image>(); bg.color = new Color(1f, 1f, 1f, 0.04f);
bg.sprite = Std(); bg.type = Image.Type.Sliced;
r.root = slotGo;
var iconGo = NewUIChild(slotGo.transform, "Icon", out var iconRt);
SetRect(iconRt, C(0.5f, 1f), C(0.5f, 1f), C(0.5f, 1f), new Vector2(0f, -56f), new Vector2(88f, 88f));
r.icon = iconGo.AddComponent<Image>(); r.icon.preserveAspect = true; r.icon.raycastTarget = false;
r.name = MakeText(slotGo.transform, "Name", new Vector2(0f, -120f), new Vector2(-16f, 36f), 24f,
UIThemeRoleKind.Text_Header, TextAlignmentOptions.Center);
r.cost = MakeText(slotGo.transform, "Cost", new Vector2(0f, -158f), new Vector2(-16f, 28f), 18f,
UIThemeRoleKind.Text_Secondary, TextAlignmentOptions.Center);
r.cd = MakeText(slotGo.transform, "Cooldown", new Vector2(0f, -188f), new Vector2(-16f, 28f), 18f,
UIThemeRoleKind.Text_Secondary, TextAlignmentOptions.Center);
r.desc = MakeText(slotGo.transform, "Desc", new Vector2(0f, -260f), new Vector2(-24f, 110f), 18f,
UIThemeRoleKind.Text_Secondary, TextAlignmentOptions.Top);
r.desc.enableWordWrapping = true;
return r;
}
private static void BindSlot(SerializedObject so, string slotProp, SlotRefs r)
{
var p = so.FindProperty(slotProp);
p.FindPropertyRelative("root").objectReferenceValue = r.root;
p.FindPropertyRelative("icon").objectReferenceValue = r.icon;
p.FindPropertyRelative("nameText").objectReferenceValue = r.name;
p.FindPropertyRelative("descText").objectReferenceValue = r.desc;
p.FindPropertyRelative("costText").objectReferenceValue = r.cost;
p.FindPropertyRelative("cooldownText").objectReferenceValue = r.cd;
}
private static Button MakeArrowButton(Transform parent, string name, string glyph, Vector2 pos)
{
var go = NewUIChild(parent, name, out var rt);
SetRect(rt, C(0.5f, 0.5f), C(0.5f, 0.5f), C(0.5f, 0.5f), pos, new Vector2(64f, 64f));
var img = go.AddComponent<Image>(); img.color = new Color(1f, 1f, 1f, 0.06f);
img.sprite = Std(); img.type = Image.Type.Sliced;
var btn = go.AddComponent<Button>(); btn.targetGraphic = img;
var lblGo = NewUIChild(go.transform, "Label", out var lblRt);
Stretch(lblRt);
var lbl = lblGo.AddComponent<TextMeshProUGUI>();
lbl.text = glyph; lbl.alignment = TextAlignmentOptions.Center; lbl.fontSize = 40f; lbl.raycastTarget = false;
SetEnum(lblGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Text_Primary);
return btn;
}
private static TMP_Text MakeText(Transform parent, string name, Vector2 pos, Vector2 size, float fontSize,
UIThemeRoleKind role, TextAlignmentOptions align)
{
var go = NewUIChild(parent, name, out var rt);
SetRect(rt, C(0.5f, 1f), C(0.5f, 1f), C(0.5f, 1f), pos, size);
var t = go.AddComponent<TextMeshProUGUI>();
t.fontSize = fontSize; t.alignment = align; t.raycastTarget = false;
SetEnum(go.AddComponent<UIThemeRole>(), "_kind", (int)role);
return t;
}
// ── 助手 ─────────────────────────────────────────────────────────────
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; }
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0c6097f5ec7fd51489d7c90b5846043b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -131,12 +131,14 @@ namespace BaseGames.Editor.UI
if (created)
{
var items = new (string key, PauseMenuAction a)[]
// 暂停为薄覆盖层,仅系统级动作;查看/装备类(形态技能 / 能力 / 地图 / 护符 / 物品)
// 一律收敛进统一背包 Tab 屏PanelId.Inventory不再从暂停跳转。
var items = new (string key, PauseMenuAction a, PanelId panel)[]
{
("PAUSE_RESUME", PauseMenuAction.Resume),
("MENU_SETTINGS", PauseMenuAction.OpenSettings),
("PAUSE_MAIN_MENU", PauseMenuAction.ReturnToMainMenu),
("MENU_QUIT", PauseMenuAction.Quit),
("PAUSE_RESUME", PauseMenuAction.Resume, PanelId.Pause),
("MENU_SETTINGS", PauseMenuAction.OpenSettings, PanelId.Pause),
("PAUSE_MAIN_MENU", PauseMenuAction.ReturnToMainMenu, PanelId.Pause),
("MENU_QUIT", PauseMenuAction.Quit, PanelId.Pause),
};
var so = new SerializedObject(cfg);
var prop = so.FindProperty("_items");
@@ -144,9 +146,10 @@ namespace BaseGames.Editor.UI
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("sceneKey").stringValue = "";
el.FindPropertyRelative("labelKey").stringValue = items[i].key;
el.FindPropertyRelative("action").enumValueIndex = (int)items[i].a;
el.FindPropertyRelative("targetPanel").enumValueIndex = (int)items[i].panel;
el.FindPropertyRelative("sceneKey").stringValue = "";
}
so.ApplyModifiedPropertiesWithoutUndo();
report.Add($"{path}(默认 {items.Length} 项)");