UI 系统
This commit is contained in:
@@ -88,6 +88,7 @@ namespace BaseGames.Editor
|
||||
CreateAsset<IntEventChannelSO> ("UI/Inventory", "EVT_InventoryTabChanged"); // 当前激活 Tab 索引变化
|
||||
CreateAsset<VoidEventChannelSO> ("UI/Inventory", "EVT_InventoryTabNext"); // L/R 肩键:切换到下一 Tab
|
||||
CreateAsset<VoidEventChannelSO> ("UI/Inventory", "EVT_InventoryTabPrev"); // L/R 肩键:切换到上一 Tab
|
||||
CreateAsset<VoidEventChannelSO> ("UI/Inventory", "EVT_QuickOpenMap"); // 快速直达:打开统一屏并定位地图 Tab(默认 M)
|
||||
CreateAsset<StringEventChannelSO> ("UI/Inventory", "EVT_ItemAcquired"); // 道具首次获得(itemId)
|
||||
CreateAsset<VoidEventChannelSO> ("UI/Inventory", "EVT_InventoryChanged"); // 背包内容变化(无负载)
|
||||
|
||||
@@ -154,6 +155,7 @@ namespace BaseGames.Editor
|
||||
CreateAsset<CharmEventChannelSO> ("Progression", "EVT_CharmEquipped");
|
||||
CreateAsset<CharmEventChannelSO> ("Progression", "EVT_CharmUnequipped");
|
||||
CreateAsset<VoidEventChannelSO> ("Progression", "EVT_EquipmentChanged");
|
||||
CreateAsset<StringEventChannelSO> ("Progression", "EVT_BestiaryUpdated");
|
||||
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
|
||||
@@ -145,10 +145,17 @@ namespace BaseGames.Editor
|
||||
GameObject settingsRootGo = GetOrCreateChild(uiRootGo.transform, "SettingsRoot").gameObject;
|
||||
GameObject mapRootGo = GetOrCreateChild(uiRootGo.transform, "MapRoot").gameObject;
|
||||
GameObject shopRootGo = GetOrCreateChild(uiRootGo.transform, "ShopRoot").gameObject;
|
||||
// 形态技能一览 / 能力解锁总览(实例化预制件,注册为 PanelId.FormSkills / Abilities,供暂停菜单打开)
|
||||
GameObject formSkillRootGo = EnsureUIPanelInstance(uiRootGo.transform, "FormSkillRoot",
|
||||
"Assets/_Game/Prefabs/UI/UI_FormSkillPanel.prefab", report);
|
||||
GameObject abilityRootGo = EnsureUIPanelInstance(uiRootGo.transform, "AbilityRoot",
|
||||
"Assets/_Game/Prefabs/UI/UI_AbilityOverviewPanel.prefab", report);
|
||||
pauseRootGo.SetActive(false);
|
||||
settingsRootGo.SetActive(false);
|
||||
mapRootGo.SetActive(false);
|
||||
shopRootGo.SetActive(false);
|
||||
formSkillRootGo.SetActive(false);
|
||||
abilityRootGo.SetActive(false);
|
||||
|
||||
GameObject deathCanvasGo = GetOrCreateCanvas(uiRootGo.transform, "DeathScreen Canvas", 10);
|
||||
GameObject deathRootGo = GetOrCreateChild(deathCanvasGo.transform, "DeathScreenRoot").gameObject;
|
||||
@@ -268,7 +275,7 @@ namespace BaseGames.Editor
|
||||
// UIManager uses _panels (PanelRegistration[]) — NOT individual _pauseMenuRoot/_settingsRoot etc.
|
||||
var so = new SerializedObject(uiManager);
|
||||
var panelsProp = so.FindProperty("_panels");
|
||||
panelsProp.arraySize = 4;
|
||||
panelsProp.arraySize = 6;
|
||||
var p0 = panelsProp.GetArrayElementAtIndex(0);
|
||||
p0.FindPropertyRelative("id").intValue = (int)PanelId.Pause;
|
||||
p0.FindPropertyRelative("root").objectReferenceValue = pauseRootGo;
|
||||
@@ -281,6 +288,12 @@ namespace BaseGames.Editor
|
||||
var p3 = panelsProp.GetArrayElementAtIndex(3);
|
||||
p3.FindPropertyRelative("id").intValue = (int)PanelId.Shop;
|
||||
p3.FindPropertyRelative("root").objectReferenceValue = shopRootGo;
|
||||
var p4 = panelsProp.GetArrayElementAtIndex(4);
|
||||
p4.FindPropertyRelative("id").intValue = (int)PanelId.FormSkills;
|
||||
p4.FindPropertyRelative("root").objectReferenceValue = formSkillRootGo;
|
||||
var p5 = panelsProp.GetArrayElementAtIndex(5);
|
||||
p5.FindPropertyRelative("id").intValue = (int)PanelId.Abilities;
|
||||
p5.FindPropertyRelative("root").objectReferenceValue = abilityRootGo;
|
||||
so.ApplyModifiedPropertiesWithoutUndo();
|
||||
}
|
||||
AssignAsset(uiManager, "_onGameStateChanged", report, true, "EVT_GameStateChanged", "EVT_GameState");
|
||||
@@ -1435,6 +1448,29 @@ namespace BaseGames.Editor
|
||||
/// 历史的非预制件裸物体会被替换为预制件实例;绑定 <c>_config</c> 与三个加载事件频道。
|
||||
/// 预制件缺失时仅在 report 提示(先跑「生成加载界面」菜单),不报错。
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// 通用:实例化 / 复用一个自足 UI 面板预制件为指定命名根(自身已绑好引用,不再额外绑定)。
|
||||
/// 历史裸物体会被替换为预制件实例。预制件缺失时仅 report 提示,返回占位根(不报错)。
|
||||
/// </summary>
|
||||
private static GameObject EnsureUIPanelInstance(Transform uiParent, string instName, string prefabPath, List<string> report)
|
||||
{
|
||||
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
|
||||
Transform existing = uiParent.Find(instName);
|
||||
if (prefab == null)
|
||||
{
|
||||
report.Add($"缺少预制件 {prefabPath}(请先运行对应「BaseGames/UI/控件库」生成菜单)。");
|
||||
return existing != null ? existing.gameObject : GetOrCreateChild(uiParent, instName).gameObject;
|
||||
}
|
||||
if (existing != null && PrefabUtility.GetCorrespondingObjectFromSource(existing.gameObject) != null)
|
||||
return existing.gameObject;
|
||||
if (existing != null) Undo.DestroyObjectImmediate(existing.gameObject);
|
||||
var go = (GameObject)PrefabUtility.InstantiatePrefab(prefab, uiParent);
|
||||
go.name = instName;
|
||||
Undo.RegisterCreatedObjectUndo(go, $"Instantiate {instName}");
|
||||
report.Add($"{instName}:由 {System.IO.Path.GetFileName(prefabPath)} 实例化。");
|
||||
return go;
|
||||
}
|
||||
|
||||
private static void EnsureLoadingScreenInstance(Transform uiParent, List<string> report)
|
||||
{
|
||||
const string instName = "Canvas_Loading";
|
||||
|
||||
216
Assets/_Game/Scripts/Editor/UI/BestiarySyncTool.cs
Normal file
216
Assets/_Game/Scripts/Editor/UI/BestiarySyncTool.cs
Normal 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>来源 A:Assets/_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>();
|
||||
|
||||
// 来源 A:Data/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;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Editor/UI/BestiarySyncTool.cs.meta
Normal file
11
Assets/_Game/Scripts/Editor/UI/BestiarySyncTool.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0fc04afec1231b24598e6b7bc81e7778
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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(排序层 5:HUD 之上、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 Tab(Content_Map):将现有 MapPanel 预制 / 节点作为子物体放入,或在此挂载 MapPanel 组件并配置。");
|
||||
report.Add("Tools Tab(Content_Tools):将现有 CharmEquipPanel 节点作为子物体放入。");
|
||||
report.Add("Options Tab(Content_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}");
|
||||
// 解除预制件连接,便于就地中和 chrome(Tab 内嵌为快照,刷新重跑脚手架即可)
|
||||
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);
|
||||
|
||||
250
Assets/_Game/Scripts/Editor/UI/UIAbilityOverviewScaffold.cs
Normal file
250
Assets/_Game/Scripts/Editor/UI/UIAbilityOverviewScaffold.cs
Normal file
@@ -0,0 +1,250 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
using BaseGames.UI;
|
||||
using BaseGames.UI.Theme;
|
||||
using BaseGames.Player;
|
||||
using BaseGames.Core.Events;
|
||||
using BaseGames.Localization;
|
||||
using BaseGames.Editor.Localization;
|
||||
|
||||
namespace BaseGames.Editor.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 能力解锁总览脚手架:生成 <c>UI_AbilityCell</c> + <c>UI_AbilityOverviewPanel</c> 预制件
|
||||
/// + 默认 <c>UI_AbilityMeta</c> 表(参照 AbilityTypeDrawer 分组)+ ABL_* 中英文案。
|
||||
/// 面板据 AbilityMetaSO 列出全部能力 + 已解锁(亮)/未解锁(暗+锁)态,订阅 EVT_AbilityUnlocked 实时刷新。
|
||||
///
|
||||
/// 菜单:BaseGames/UI/控件库/生成能力解锁总览(预制件 + 默认表)
|
||||
/// </summary>
|
||||
public static class UIAbilityOverviewScaffold
|
||||
{
|
||||
private const string PrefabDir = "Assets/_Game/Prefabs/UI";
|
||||
private const string ControlsDir = "Assets/_Game/Prefabs/UI/Controls";
|
||||
private const string ConfigDir = "Assets/_Game/Data/UI";
|
||||
private const string ThemePath = "Assets/_Game/Data/UI/Themes/UI_Theme_Default.asset";
|
||||
private const string PanelName = "UI_AbilityOverviewPanel";
|
||||
private const string CellName = "UI_AbilityCell";
|
||||
private const string MetaName = "UI_AbilityMeta";
|
||||
|
||||
// 默认能力项(参照 Editor/Equipment/AbilityTypeDrawer.Groups):type, labelKey, 中文, 英文
|
||||
private static readonly (AbilityType t, string key, string zh, string en)[] Defaults =
|
||||
{
|
||||
(AbilityType.WallCling, "ABL_WALLCLING", "贴墙悬挂", "Wall Cling"),
|
||||
(AbilityType.WallJump, "ABL_WALLJUMP", "墙跳", "Wall Jump"),
|
||||
(AbilityType.Dash, "ABL_DASH", "冲刺", "Dash"),
|
||||
(AbilityType.DoubleJump, "ABL_DOUBLEJUMP", "二段跳", "Double Jump"),
|
||||
(AbilityType.SuperJump, "ABL_SUPERJUMP", "超级跳", "Super Jump"),
|
||||
(AbilityType.Swim, "ABL_SWIM", "游泳", "Swim"),
|
||||
(AbilityType.DownDash, "ABL_DOWNDASH", "下冲刺", "Down Dash"),
|
||||
(AbilityType.InvincibleDash,"ABL_INVINCIBLEDASH","无敌冲刺","Invincible Dash"),
|
||||
(AbilityType.Spell1, "ABL_SPELL1", "法术槽 1", "Spell Slot 1"),
|
||||
(AbilityType.Spell2, "ABL_SPELL2", "法术槽 2", "Spell Slot 2"),
|
||||
(AbilityType.Spell3, "ABL_SPELL3", "法术槽 3", "Spell Slot 3"),
|
||||
(AbilityType.SpiritForm, "ABL_SPIRITFORM", "灵魄形态", "Spirit Form"),
|
||||
(AbilityType.SpiritDash, "ABL_SPIRITDASH", "灵魄冲刺", "Spirit Dash"),
|
||||
(AbilityType.FormTianHun, "ABL_FORM_TIANHUN", "天魂", "Sky Soul"),
|
||||
(AbilityType.FormDiHun, "ABL_FORM_DIHUN", "地魂", "Earth Soul"),
|
||||
(AbilityType.FormMingHun, "ABL_FORM_MINGHUN", "命魂", "Death Soul"),
|
||||
(AbilityType.Parry, "ABL_PARRY", "弹反", "Parry"),
|
||||
(AbilityType.ChargeAttack, "ABL_CHARGEATTACK", "蓄力攻击", "Charge Attack"),
|
||||
(AbilityType.DownSlash, "ABL_DOWNSLASH", "下斩", "Down Slash"),
|
||||
(AbilityType.Interact, "ABL_INTERACT", "互动", "Interact"),
|
||||
(AbilityType.FastTravel, "ABL_FASTTRAVEL", "快速旅行", "Fast Travel"),
|
||||
};
|
||||
|
||||
[MenuItem("BaseGames/UI/控件库/生成能力解锁总览(预制件 + 默认表)")]
|
||||
public static void GeneratePrefab()
|
||||
{
|
||||
EnsureFolder(PrefabDir); EnsureFolder(ControlsDir); EnsureFolder(ConfigDir);
|
||||
var report = new List<string>();
|
||||
var theme = AssetDatabase.LoadAssetAtPath<UIThemeSO>(ThemePath);
|
||||
var meta = EnsureMeta(report);
|
||||
SeedLocalization(report);
|
||||
var cell = BuildCellPrefab(theme, report);
|
||||
BuildPanelPrefab(theme, meta, cell, report);
|
||||
|
||||
AssetDatabase.SaveAssets(); AssetDatabase.Refresh();
|
||||
var sb = new System.Text.StringBuilder("[UIAbilityOverview] 能力总览已生成:\n");
|
||||
foreach (var r in report) sb.AppendLine(" • " + r);
|
||||
sb.AppendLine("策划改 UI_AbilityMeta 增删/重排能力;美术改 UI_AbilityCell / 面板预制件改样式。");
|
||||
Debug.Log(sb.ToString());
|
||||
}
|
||||
|
||||
private static AbilityMetaSO EnsureMeta(List<string> report)
|
||||
{
|
||||
string path = $"{ConfigDir}/{MetaName}.asset";
|
||||
var meta = AssetDatabase.LoadAssetAtPath<AbilityMetaSO>(path);
|
||||
bool created = meta == null;
|
||||
if (created) { meta = ScriptableObject.CreateInstance<AbilityMetaSO>(); AssetDatabase.CreateAsset(meta, path); }
|
||||
if (created)
|
||||
{
|
||||
var so = new SerializedObject(meta);
|
||||
var p = so.FindProperty("_items");
|
||||
p.arraySize = Defaults.Length;
|
||||
for (int i = 0; i < Defaults.Length; i++)
|
||||
{
|
||||
var el = p.GetArrayElementAtIndex(i);
|
||||
el.FindPropertyRelative("type").enumValueFlag = (int)(uint)Defaults[i].t;
|
||||
el.FindPropertyRelative("labelKey").stringValue = Defaults[i].key;
|
||||
}
|
||||
so.ApplyModifiedPropertiesWithoutUndo();
|
||||
report.Add($"{path}(默认 {Defaults.Length} 项能力)");
|
||||
}
|
||||
return meta;
|
||||
}
|
||||
|
||||
private static void SeedLocalization(List<string> report)
|
||||
{
|
||||
var zh = new Dictionary<string, string> { { "ABL_OVERVIEW_TITLE", "能力" } };
|
||||
var en = new Dictionary<string, string> { { "ABL_OVERVIEW_TITLE", "Abilities" } };
|
||||
foreach (var d in Defaults) { zh[d.key] = d.zh; en[d.key] = d.en; }
|
||||
int added = MergeWriteUI(Language.ChineseSimplified, zh) + MergeWriteUI(Language.English, en);
|
||||
report.Add($"本地化新增 {added} 条(已存在不覆盖)。");
|
||||
}
|
||||
|
||||
private static int MergeWriteUI(Language lang, Dictionary<string, string> kv)
|
||||
{
|
||||
var dict = LocalizationFileIO.Read(lang, LocalizationTable.UI);
|
||||
int added = 0;
|
||||
foreach (var p in kv) if (!dict.ContainsKey(p.Key)) { dict[p.Key] = p.Value; added++; }
|
||||
if (added > 0) LocalizationFileIO.Write(lang, LocalizationTable.UI, dict);
|
||||
return added;
|
||||
}
|
||||
|
||||
// ── 单元格预制件 ─────────────────────────────────────────────────────
|
||||
private static AbilityCellView BuildCellPrefab(UIThemeSO theme, List<string> report)
|
||||
{
|
||||
var root = new GameObject(CellName, typeof(RectTransform), typeof(CanvasGroup));
|
||||
((RectTransform)root.transform).sizeDelta = new Vector2(180f, 150f);
|
||||
var view = root.AddComponent<AbilityCellView>();
|
||||
|
||||
var bg = root.AddComponent<Image>();
|
||||
bg.sprite = Std(); bg.type = Image.Type.Sliced; bg.color = new Color(1f, 1f, 1f, 0.05f);
|
||||
|
||||
var iconGo = NewUIChild(root.transform, "Icon", out var iconRt);
|
||||
SetRect(iconRt, C(0.5f, 1f), C(0.5f, 1f), C(0.5f, 1f), new Vector2(0f, -50f), new Vector2(72f, 72f));
|
||||
var icon = iconGo.AddComponent<Image>(); icon.preserveAspect = true; icon.raycastTarget = false;
|
||||
|
||||
var nameGo = NewUIChild(root.transform, "Name", out var nameRt);
|
||||
SetRect(nameRt, C(0.5f, 0f), C(0.5f, 0f), C(0.5f, 0f), new Vector2(0f, 18f), new Vector2(-12f, 44f));
|
||||
var nameTmp = nameGo.AddComponent<TextMeshProUGUI>();
|
||||
nameTmp.alignment = TextAlignmentOptions.Center; nameTmp.fontSize = 20f; nameTmp.enableWordWrapping = true;
|
||||
nameTmp.raycastTarget = false;
|
||||
var loc = nameGo.AddComponent<LocalizedText>();
|
||||
SetEnum(nameGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Text_Primary);
|
||||
|
||||
// 锁定遮罩(未解锁时显示):半透明暗罩(美术可替换为锁图标 sprite)
|
||||
var lockGo = NewUIChild(root.transform, "LockedOverlay", out var lockRt);
|
||||
Stretch(lockRt);
|
||||
var lockImg = lockGo.AddComponent<Image>(); lockImg.color = new Color(0f, 0f, 0f, 0.55f); lockImg.raycastTarget = false;
|
||||
lockGo.SetActive(false);
|
||||
|
||||
var so = new SerializedObject(view);
|
||||
so.FindProperty("_icon").objectReferenceValue = icon;
|
||||
so.FindProperty("_label").objectReferenceValue = loc;
|
||||
so.FindProperty("_lockedOverlay").objectReferenceValue = lockGo;
|
||||
so.FindProperty("_group").objectReferenceValue = root.GetComponent<CanvasGroup>();
|
||||
so.ApplyModifiedPropertiesWithoutUndo();
|
||||
|
||||
string path = $"{ControlsDir}/{CellName}.prefab";
|
||||
var asset = PrefabUtility.SaveAsPrefabAsset(root, path);
|
||||
Object.DestroyImmediate(root);
|
||||
report.Add(path);
|
||||
return asset.GetComponent<AbilityCellView>();
|
||||
}
|
||||
|
||||
// ── 面板预制件 ───────────────────────────────────────────────────────
|
||||
private static void BuildPanelPrefab(UIThemeSO theme, AbilityMetaSO meta, AbilityCellView cell, List<string> report)
|
||||
{
|
||||
var root = new GameObject(PanelName,
|
||||
typeof(RectTransform), typeof(Canvas), typeof(CanvasScaler), typeof(GraphicRaycaster), typeof(CanvasGroup));
|
||||
Stretch((RectTransform)root.transform);
|
||||
SetupCanvas(root, 60);
|
||||
var panel = root.AddComponent<DataDrivenAbilityPanel>();
|
||||
var applier = root.AddComponent<UIThemeApplier>();
|
||||
if (theme != null) AssignRef(applier, "_theme", theme);
|
||||
|
||||
var overlayGo = NewUIChild(root.transform, "Overlay", out var ovRt);
|
||||
Stretch(ovRt);
|
||||
overlayGo.AddComponent<Image>().color = new Color(0f, 0f, 0f, 0.6f);
|
||||
|
||||
var boxGo = NewUIChild(root.transform, "DialogBox", out var boxRt);
|
||||
SetRect(boxRt, C(0.5f), C(0.5f), C(0.5f), Vector2.zero, new Vector2(1120f, 760f));
|
||||
var box = boxGo.AddComponent<Image>(); box.sprite = Std(); box.type = Image.Type.Sliced;
|
||||
box.color = new Color(0.10f, 0.11f, 0.15f, 0.98f);
|
||||
SetEnum(boxGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Graphic_Background);
|
||||
|
||||
var titleGo = NewUIChild(boxGo.transform, "Title", out var titleRt);
|
||||
SetRect(titleRt, C(0.5f, 1f), C(0.5f, 1f), C(0.5f, 1f), new Vector2(0f, -48f), new Vector2(-60f, 56f));
|
||||
var title = titleGo.AddComponent<TextMeshProUGUI>();
|
||||
title.alignment = TextAlignmentOptions.Center; title.fontSize = 40f; title.raycastTarget = false;
|
||||
var titleLoc = titleGo.AddComponent<LocalizedText>();
|
||||
SetString(titleLoc, "_key", "ABL_OVERVIEW_TITLE");
|
||||
SetEnum(titleGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Text_Header);
|
||||
|
||||
// 能力格容器(网格)
|
||||
var gridGo = NewUIChild(boxGo.transform, "GridContainer", out var gridRt);
|
||||
SetRect(gridRt, C(0.5f, 0.5f), C(0.5f, 0.5f), C(0.5f, 0.5f), new Vector2(0f, 10f), new Vector2(1056f, 600f));
|
||||
var grid = gridGo.AddComponent<GridLayoutGroup>();
|
||||
// 6 列:21 项 → 4 行,落在框内不溢出(美术可在预制件再调列数/格子尺寸)
|
||||
grid.cellSize = new Vector2(160f, 130f); grid.spacing = new Vector2(16f, 14f);
|
||||
grid.childAlignment = TextAnchor.UpperCenter;
|
||||
grid.constraint = GridLayoutGroup.Constraint.FixedColumnCount; grid.constraintCount = 6;
|
||||
|
||||
var closeGo = NewUIChild(boxGo.transform, "Btn_Close", out var closeRt);
|
||||
SetRect(closeRt, C(0.5f, 0f), C(0.5f, 0f), C(0.5f, 0f), new Vector2(0f, 36f), new Vector2(240f, 56f));
|
||||
var closeImg = closeGo.AddComponent<Image>(); closeImg.sprite = Std(); closeImg.type = Image.Type.Sliced;
|
||||
closeImg.color = new Color(1f, 1f, 1f, 0.06f);
|
||||
var closeBtn = closeGo.AddComponent<Button>(); closeBtn.targetGraphic = closeImg;
|
||||
var clblGo = NewUIChild(closeGo.transform, "Label", out var clblRt);
|
||||
Stretch(clblRt);
|
||||
var clbl = clblGo.AddComponent<TextMeshProUGUI>();
|
||||
clbl.alignment = TextAlignmentOptions.Center; clbl.fontSize = 26f;
|
||||
clblGo.AddComponent<LocalizedText>();
|
||||
SetString(clblGo.GetComponent<LocalizedText>(), "_key", "BTN_BACK");
|
||||
SetEnum(clblGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Text_Primary);
|
||||
|
||||
var so = new SerializedObject(panel);
|
||||
so.FindProperty("_meta").objectReferenceValue = meta;
|
||||
so.FindProperty("_container").objectReferenceValue = gridRt;
|
||||
so.FindProperty("_cellPrefab").objectReferenceValue = cell;
|
||||
so.FindProperty("_btnClose").objectReferenceValue = closeBtn;
|
||||
so.FindProperty("_onAbilityUnlocked").objectReferenceValue = FindAsset<AbilityTypeEventChannelSO>("EVT_AbilityUnlocked");
|
||||
so.ApplyModifiedPropertiesWithoutUndo();
|
||||
|
||||
root.SetActive(false);
|
||||
PrefabUtility.SaveAsPrefabAsset(root, $"{PrefabDir}/{PanelName}.prefab");
|
||||
Object.DestroyImmediate(root);
|
||||
report.Add($"{PrefabDir}/{PanelName}.prefab");
|
||||
}
|
||||
|
||||
// ── 助手 ─────────────────────────────────────────────────────────────
|
||||
private static Vector2 C(float x = 0.5f, float y = 0.5f) => new Vector2(x, y);
|
||||
private static GameObject NewUIChild(Transform parent, string name, out RectTransform rt)
|
||||
{ var go = new GameObject(name, typeof(RectTransform)); rt = (RectTransform)go.transform; rt.SetParent(parent, false); return go; }
|
||||
private static void Stretch(RectTransform rt) { rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.one; rt.offsetMin = Vector2.zero; rt.offsetMax = Vector2.zero; }
|
||||
private static void SetRect(RectTransform rt, Vector2 aMin, Vector2 aMax, Vector2 pivot, Vector2 pos, Vector2 size)
|
||||
{ rt.anchorMin = aMin; rt.anchorMax = aMax; rt.pivot = pivot; rt.anchoredPosition = pos; rt.sizeDelta = size; }
|
||||
private static void SetEnum(Component c, string prop, int value) { var so = new SerializedObject(c); var p = so.FindProperty(prop); if (p != null) { p.enumValueIndex = value; so.ApplyModifiedPropertiesWithoutUndo(); } }
|
||||
private static void SetString(Component c, string prop, string value) { if (c == null) return; var so = new SerializedObject(c); var p = so.FindProperty(prop); if (p != null) { p.stringValue = value; so.ApplyModifiedPropertiesWithoutUndo(); } }
|
||||
private static void AssignRef(Object target, string prop, Object value) { var so = new SerializedObject(target); var p = so.FindProperty(prop); if (p != null) { p.objectReferenceValue = value; so.ApplyModifiedPropertiesWithoutUndo(); } }
|
||||
private static T FindAsset<T>(string name) where T : Object
|
||||
{ foreach (var g in AssetDatabase.FindAssets($"{name} t:{typeof(T).Name}")) { var pa = AssetDatabase.GUIDToAssetPath(g); var a = AssetDatabase.LoadAssetAtPath<T>(pa); if (a != null && a.name == name) return a; } return null; }
|
||||
private static void SetupCanvas(GameObject go, int sortOrder)
|
||||
{
|
||||
var canvas = go.GetComponent<Canvas>();
|
||||
canvas.renderMode = RenderMode.ScreenSpaceOverlay; canvas.sortingOrder = sortOrder;
|
||||
var scaler = go.GetComponent<CanvasScaler>();
|
||||
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
||||
scaler.referenceResolution = new Vector2(1920f, 1080f);
|
||||
scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight;
|
||||
scaler.matchWidthOrHeight = 0.5f;
|
||||
}
|
||||
|
||||
private static Sprite Std() => AssetDatabase.GetBuiltinExtraResource<Sprite>("UI/Skin/UISprite.psd");
|
||||
private static void EnsureFolder(string dir) { var parts = dir.Split('/'); string cur = parts[0]; for (int i = 1; i < parts.Length; i++) { string n = $"{cur}/{parts[i]}"; if (!AssetDatabase.IsValidFolder(n)) AssetDatabase.CreateFolder(cur, parts[i]); cur = n; } }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cbcf9174cf741d6499eea7b8443f18ea
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
266
Assets/_Game/Scripts/Editor/UI/UIBestiaryScaffold.cs
Normal file
266
Assets/_Game/Scripts/Editor/UI/UIBestiaryScaffold.cs
Normal 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; } }
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Editor/UI/UIBestiaryScaffold.cs.meta
Normal file
11
Assets/_Game/Scripts/Editor/UI/UIBestiaryScaffold.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 643450ffe43a0124f9284b664b9f3e28
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
309
Assets/_Game/Scripts/Editor/UI/UICharmScaffold.cs
Normal file
309
Assets/_Game/Scripts/Editor/UI/UICharmScaffold.cs
Normal 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 → IEquipmentService(EquipmentManager);变更经 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("运行期读 IEquipmentService(EquipmentManager);重跑「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);
|
||||
|
||||
// 卡片模板(CharmCardView,inactive)
|
||||
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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Editor/UI/UICharmScaffold.cs.meta
Normal file
11
Assets/_Game/Scripts/Editor/UI/UICharmScaffold.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cc9108bb3994a074aa6a534640535f97
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
305
Assets/_Game/Scripts/Editor/UI/UIFormSkillScaffold.cs
Normal file
305
Assets/_Game/Scripts/Editor/UI/UIFormSkillScaffold.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Editor/UI/UIFormSkillScaffold.cs.meta
Normal file
11
Assets/_Game/Scripts/Editor/UI/UIFormSkillScaffold.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0c6097f5ec7fd51489d7c90b5846043b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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} 项)");
|
||||
|
||||
Reference in New Issue
Block a user