Files
zeling_v2/Assets/_Game/Scripts/Editor/UI/HUDScaffoldWizard.cs
2026-06-05 18:41:33 +08:00

443 lines
26 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections.Generic;
using BaseGames.UI;
using BaseGames.UI.HUD;
using TMPro;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
namespace BaseGames.Editor.UI
{
/// <summary>
/// HUD 脚手架工具(架构 10_UIModule §HUD
/// 在当前活动场景中生成完整的 HUD Canvas 层级结构并自动绑定已存在的事件频道资产。
/// 执行路径BaseGames ▸ Scene ▸ Setup ▸ Scaffold HUD Canvas
/// </summary>
public static class HUDScaffoldWizard
{
[MenuItem("BaseGames/Scene/Setup/Scaffold HUD Canvas", priority = 203)]
public static void ScaffoldHUDCanvas()
{
var report = new List<string>();
Undo.SetCurrentGroupName("Scaffold HUD Canvas");
int undoGroup = Undo.GetCurrentGroup();
// ── Canvas ────────────────────────────────────────────────────────
GameObject canvasGo = GetOrCreateRootCanvas("HUD Canvas", 0);
// ── HUDRoot ───────────────────────────────────────────────────────
GameObject hudRootGo = GetOrCreateChild(canvasGo.transform, "HUDRoot").gameObject;
HUDController hudCtrl = GetOrAddComponent<HUDController>(hudRootGo);
// 若场景中已存在 UIManager自动将 _hudRoot 指向本 HUDRoot
UIManager uiManager = Object.FindFirstObjectByType<UIManager>();
if (uiManager != null)
AssignRef(uiManager, "_hudRoot", hudRootGo);
// ── HP 区域 ───────────────────────────────────────────────────────
GameObject hpContainerGo = GetOrCreateChild(hudRootGo.transform, "HP_Container").gameObject;
var hpLayout = GetOrAddComponent<HorizontalLayoutGroup>(hpContainerGo);
hpLayout.childForceExpandWidth = false;
hpLayout.childForceExpandHeight = false;
hpLayout.spacing = 4f;
// ── 魄元Soul量条 ──────────────────────────────────────────────
GameObject soulGaugeGo = GetOrCreateChild(hudRootGo.transform, "Gauge_Soul").gameObject;
Image soulFill = GetOrAddComponent<Image>(soulGaugeGo);
soulFill.type = Image.Type.Filled;
soulFill.fillMethod = Image.FillMethod.Horizontal;
soulFill.fillAmount = 1f;
// ── 灵力Spirit量条 ────────────────────────────────────────────
GameObject spiritGaugeGo = GetOrCreateChild(hudRootGo.transform, "Gauge_Spirit").gameObject;
Image spiritFill = GetOrAddComponent<Image>(spiritGaugeGo);
spiritFill.type = Image.Type.Filled;
spiritFill.fillMethod = Image.FillMethod.Horizontal;
spiritFill.fillAmount = 1f;
// ── 灵铢LingZhu文本 ───────────────────────────────────────────
GameObject lingZhuGo = GetOrCreateChild(hudRootGo.transform, "Text_LingZhu").gameObject;
TMP_Text lingZhuText = GetOrAddComponent<TextMeshProUGUI>(lingZhuGo);
lingZhuText.text = "0";
// ── 回春图标Spring Charges─────────────────────────────────────
GameObject springContainerGo = GetOrCreateChild(hudRootGo.transform, "Spring_Container").gameObject;
var springLayout = GetOrAddComponent<HorizontalLayoutGroup>(springContainerGo);
springLayout.childForceExpandWidth = false;
springLayout.childForceExpandHeight = false;
springLayout.spacing = 4f;
// ── 形态图标Form Icons × 4────────────────────────────────────
const int kFormIconCount = 4;
GameObject formIconsRoot = GetOrCreateChild(hudRootGo.transform, "FormIcons").gameObject;
Image[] formImages = new Image[kFormIconCount];
for (int i = 0; i < kFormIconCount; i++)
{
GameObject iconGo = GetOrCreateChild(formIconsRoot.transform, $"FormIcon_{i}").gameObject;
formImages[i] = GetOrAddComponent<Image>(iconGo);
}
// ── 交互提示InteractPrompt─────────────────────────────────────
GameObject interactPromptGo = GetOrCreateChild(hudRootGo.transform, "InteractPrompt").gameObject;
InteractPromptWidget interactWidget = GetOrAddComponent<InteractPromptWidget>(interactPromptGo);
GameObject promptRootGo = GetOrCreateChild(interactPromptGo.transform, "Root").gameObject;
GameObject promptKeyIconGo = GetOrCreateChild(promptRootGo.transform, "KeyIcon").gameObject;
Image keyIcon = GetOrAddComponent<Image>(promptKeyIconGo);
GameObject promptLabelGo = GetOrCreateChild(promptRootGo.transform, "LabelText").gameObject;
TMP_Text promptLabel = GetOrAddComponent<TextMeshProUGUI>(promptLabelGo);
promptLabel.text = "互动";
promptRootGo.SetActive(false);
AssignRef(interactWidget, "_keyIcon", keyIcon);
AssignRef(interactWidget, "_labelText", promptLabel);
AssignRef(interactWidget, "_root", promptRootGo);
// ── 法术槽SpellSlot───────────────────────────────────────────
GameObject spellSlotGo = GetOrCreateChild(hudRootGo.transform, "SpellSlot").gameObject;
SpellSlotWidget spellWidget = GetOrAddComponent<SpellSlotWidget>(spellSlotGo);
GameObject spellIconGo = GetOrCreateChild(spellSlotGo.transform, "IconImage").gameObject;
Image spellIcon = GetOrAddComponent<Image>(spellIconGo);
GameObject spellCdGo = GetOrCreateChild(spellSlotGo.transform, "CooldownFill").gameObject;
Image spellCd = GetOrAddComponent<Image>(spellCdGo);
spellCd.type = Image.Type.Filled;
spellCd.fillMethod = Image.FillMethod.Radial360;
spellCd.fillAmount = 0f;
GameObject spellEmptyGo = GetOrCreateChild(spellSlotGo.transform, "EmptySlot").gameObject;
AssignRef(spellWidget, "_iconImage", spellIcon);
AssignRef(spellWidget, "_cooldownFill", spellCd);
AssignRef(spellWidget, "_emptySlot", spellEmptyGo);
// ── 工具栏ToolHUD × 2 slots──────────────────────────────────
GameObject toolHUDGo = GetOrCreateChild(hudRootGo.transform, "ToolHUD").gameObject;
ToolHUD toolHUD = GetOrAddComponent<ToolHUD>(toolHUDGo);
const int kToolSlotCount = 2;
var toolSlots = new ToolSlotUI[kToolSlotCount];
for (int i = 0; i < kToolSlotCount; i++)
{
GameObject slotGo = GetOrCreateChild(toolHUDGo.transform, $"ToolSlot_{i}").gameObject;
ToolSlotUI slotUI = GetOrAddComponent<ToolSlotUI>(slotGo);
GameObject iconGo = GetOrCreateChild(slotGo.transform, "Icon").gameObject;
Image slotIcon = GetOrAddComponent<Image>(iconGo);
GameObject usesGo = GetOrCreateChild(slotGo.transform, "UsesText").gameObject;
TMP_Text usesText = GetOrAddComponent<TextMeshProUGUI>(usesGo);
usesText.text = "0";
GameObject cdGo = GetOrCreateChild(slotGo.transform, "CooldownMask").gameObject;
Image cdMask = GetOrAddComponent<Image>(cdGo);
cdMask.type = Image.Type.Filled;
cdMask.fillMethod = Image.FillMethod.Horizontal;
cdMask.fillAmount = 0f;
AssignRef(slotUI, "_icon", slotIcon);
AssignRef(slotUI, "_usesText", usesText);
AssignRef(slotUI, "_cooldownMask", cdMask);
toolSlots[i] = slotUI;
}
AssignArrayRefs(toolHUD, "_slots", toolSlots, report);
// ── Boss 血条BossHPBar────────────────────────────────────────
GameObject bossBarGo = GetOrCreateChild(hudRootGo.transform, "BossHPBar").gameObject;
bossBarGo.SetActive(false);
BossHPBar bossHPBar = GetOrAddComponent<BossHPBar>(bossBarGo);
GameObject bossNameGo = GetOrCreateChild(bossBarGo.transform, "BossName").gameObject;
TMP_Text bossName = GetOrAddComponent<TextMeshProUGUI>(bossNameGo);
bossName.text = "Boss 名称";
GameObject bossHPFillGo = GetOrCreateChild(bossBarGo.transform, "HPFill").gameObject;
Image bossHPFill = GetOrAddComponent<Image>(bossHPFillGo);
bossHPFill.type = Image.Type.Filled;
bossHPFill.fillMethod = Image.FillMethod.Horizontal;
bossHPFill.fillAmount = 1f;
GameObject phaseRootGo = GetOrCreateChild(bossBarGo.transform, "PhaseMarkersRoot").gameObject;
AssignRef(bossHPBar, "_bossNameText", bossName);
AssignRef(bossHPBar, "_hpFill", bossHPFill);
AssignRef(bossHPBar, "_phaseMarkersRoot", phaseRootGo.transform);
// ── 状态效果 HUDStatusEffectHUD──────────────────────────────
GameObject statusHUDGo = GetOrCreateChild(hudRootGo.transform, "StatusEffectHUD").gameObject;
StatusEffectHUD statusHUD = GetOrAddComponent<StatusEffectHUD>(statusHUDGo);
GameObject statusTemplateGo = GetOrCreateChild(statusHUDGo.transform, "SlotTemplate").gameObject;
statusTemplateGo.SetActive(false);
GetOrAddComponent<Image>(statusTemplateGo);
GameObject statusContainerGo = GetOrCreateChild(statusHUDGo.transform, "Container").gameObject;
var statusLayout = GetOrAddComponent<HorizontalLayoutGroup>(statusContainerGo);
statusLayout.childForceExpandWidth = false;
statusLayout.spacing = 4f;
AssignRef(statusHUD, "_slotTemplate", statusTemplateGo);
AssignRef(statusHUD, "_container", statusContainerGo.transform);
// ── 任务追踪QuestTracker──────────────────────────────────────
GameObject questTrackerGo = GetOrCreateChild(hudRootGo.transform, "QuestTracker").gameObject;
QuestTrackerWidget questWidget = GetOrAddComponent<QuestTrackerWidget>(questTrackerGo);
GameObject questTitleGo = GetOrCreateChild(questTrackerGo.transform, "QuestTitle").gameObject;
TMP_Text questTitle = GetOrAddComponent<TextMeshProUGUI>(questTitleGo);
questTitle.text = "任务名称";
GameObject objTemplateGo = GetOrCreateChild(questTrackerGo.transform, "ObjectiveRowTemplate").gameObject;
objTemplateGo.SetActive(false);
GetOrAddComponent<TextMeshProUGUI>(objTemplateGo);
GameObject objContainerGo = GetOrCreateChild(questTrackerGo.transform, "ObjectiveContainer").gameObject;
var objLayout = GetOrAddComponent<VerticalLayoutGroup>(objContainerGo);
objLayout.childForceExpandWidth = false;
objLayout.spacing = 2f;
AssignRef(questWidget, "_questTitleText", questTitle);
AssignRef(questWidget, "_objectiveRowTemplate", objTemplateGo);
AssignRef(questWidget, "_objectiveContainer", objContainerGo.transform);
// ── HUDController 子组件引用 ──────────────────────────────────────
AssignRef(hudCtrl, "_hpContainer", hpContainerGo.transform);
AssignRef(hudCtrl, "_soulGaugeFill", soulFill);
AssignRef(hudCtrl, "_spiritGaugeFill", spiritFill);
AssignRef(hudCtrl, "_lingZhuText", lingZhuText);
AssignRef(hudCtrl, "_springContainer", springContainerGo.transform);
AssignRef(hudCtrl, "_interactPromptWidget", interactWidget);
AssignArrayRefs(hudCtrl, "_formIcons", formImages, report);
// ── HP 格子 / 回春图标 Prefab自动创建并绑定无需手工补──────────
GameObject hpCellPrefab = EnsureHUDIconPrefab("UI_HUD_HPCell",
new Color32(0xD8, 0x3A, 0x3A, 0xFF), new Vector2(40, 40), report); // 面具红
GameObject springPrefab = EnsureHUDIconPrefab("UI_HUD_SpringIcon",
new Color32(0x4A, 0xC8, 0xF0, 0xFF), new Vector2(32, 32), report); // 灵泉青
AssignRef(hudCtrl, "_hpCellPrefab", hpCellPrefab);
AssignRef(hudCtrl, "_springIconPrefab", springPrefab);
// ── 事件频道 ──────────────────────────────────────────────────────
AssignAsset(hudCtrl, "_onHPChanged", report, true, "EVT_HPChanged", "EVT_PlayerHPChanged");
AssignAsset(hudCtrl, "_onMaxHPChanged", report, false, "EVT_MaxHPChanged", "EVT_PlayerMaxHPChanged");
AssignAsset(hudCtrl, "_onSoulPowerChanged", report, false, "EVT_SoulPowerChanged", "EVT_MagicPowerChanged");
AssignAsset(hudCtrl, "_onSpiritPowerChanged", report, false, "EVT_SpiritPowerChanged");
AssignAsset(hudCtrl, "_onLingZhuChanged", report, false, "EVT_LingZhuChanged", "EVT_CurrencyChanged");
AssignAsset(hudCtrl, "_onSpringChargesChanged", report, false, "EVT_SpringChargesChanged", "EVT_SpringCharges");
AssignAsset(hudCtrl, "_onFormChanged", report, false, "EVT_FormChanged");
AssignAsset(interactWidget, "_onShowPrompt", report, false, "EVT_ShowInteractPrompt", "EVT_InteractPromptShow");
AssignAsset(interactWidget, "_onHidePrompt", report, false, "EVT_HideInteractPrompt", "EVT_InteractPromptHide");
AssignAsset(bossHPBar, "_onBossFightToggled", report, false, "EVT_BossFightToggled", "EVT_BossFightStarted");
AssignAsset(bossHPBar, "_onBossHPChanged", report, false, "EVT_BossHPChanged");
AssignAsset(bossHPBar, "_onBossHPMaxSet", report, false, "EVT_BossHPMaxSet");
AssignAsset(bossHPBar, "_onBossNameSet", report, false, "EVT_BossNameSet");
AssignAsset(bossHPBar, "_onBossPhaseThreshold", report, false, "EVT_BossPhaseThreshold");
AssignAsset(statusHUD, "_onStatusEffectApplied", report, false, "EVT_StatusEffectApplied");
AssignAsset(statusHUD, "_onStatusEffectExpired", report, false, "EVT_StatusEffectExpired");
AssignAsset(questWidget, "_onQuestStarted", report, false, "EVT_QuestStarted");
AssignAsset(questWidget, "_onQuestCompleted", report, false, "EVT_QuestCompleted");
AssignAsset(questWidget, "_onQuestFailed", report, false, "EVT_QuestFailed");
AssignAsset(questWidget, "_onQuestAbandoned", report, false, "EVT_QuestAbandoned");
AssignAsset(questWidget, "_onQuestReadyToComplete", report, false, "EVT_QuestReadyToComplete");
AssignAsset(questWidget, "_onObjectiveBatchUpdated", report, false, "EVT_QuestObjectiveBatchUpdated");
AssignAsset(toolHUD, "_onToolUsed", report, false, "EVT_ToolUsed");
// ── 手工步骤说明 ──────────────────────────────────────────────────
// _hpCellPrefab / _springIconPrefab 已自动创建并绑定(占位红/青方块,美术可替换)。
report.Add("BossHPBar._phaseMarkerPrefab请将阶段标记点 Prefab 赋给该字段。");
report.Add("StatusEffectHUD._slotConfigs请在 Inspector 中配置各状态效果的图标映射。");
Undo.CollapseUndoOperations(undoGroup);
MarkDirtyAndLog("HUD Canvas 脚手架", canvasGo, report);
}
// ─────────────────────────────────────────────────────────────────────
// Private helpers
// ─────────────────────────────────────────────────────────────────────
/// <summary>
/// 创建(或复用)一个 HUD 图标 Prefab含 RectTransform + Image + LayoutElement
/// 用作 HUDController._hpCellPrefab / _springIconPrefab。占位纯色方块美术后续可替换 sprite。
/// 路径Assets/_Game/Prefabs/UI/{prefabName}.prefab符合 AssetFolderSpec.md §4 UI 前缀)。
/// </summary>
private static GameObject EnsureHUDIconPrefab(string prefabName, Color color, Vector2 size, List<string> report)
{
const string uiDir = "Assets/_Game/Prefabs/UI";
string path = $"{uiDir}/{prefabName}.prefab";
var existing = AssetDatabase.LoadAssetAtPath<GameObject>(path);
if (existing != null) return existing;
if (!AssetDatabase.IsValidFolder("Assets/_Game/Prefabs"))
AssetDatabase.CreateFolder("Assets/_Game", "Prefabs");
if (!AssetDatabase.IsValidFolder(uiDir))
AssetDatabase.CreateFolder("Assets/_Game/Prefabs", "UI");
var go = new GameObject(prefabName, typeof(RectTransform));
var rt = go.GetComponent<RectTransform>();
rt.sizeDelta = size;
var img = go.AddComponent<UnityEngine.UI.Image>();
img.color = color;
var le = go.AddComponent<UnityEngine.UI.LayoutElement>();
le.preferredWidth = size.x;
le.preferredHeight = size.y;
var prefab = PrefabUtility.SaveAsPrefabAsset(go, path);
Object.DestroyImmediate(go);
report.Add($"已自动创建 HUD 图标 Prefab{path}(占位纯色,美术可替换)。");
return prefab;
}
/// <summary>
/// 在活动场景中查找或创建 HUD Canvas。
/// 查找顺序:
/// 1. 遍历场景根节点中的直接同名节点
/// 2. 遍历各根节点下的 [UI]/UIRoot/{name} 或 UIRoot/{name} 子路径
/// 若均不存在则新建 Canvas并优先挂载至 UIRoot兼容 ScaffoldPersistentScene 层级);
/// 若场景中不存在 UIRoot则回退到场景根层并发出警告。
/// </summary>
private static GameObject GetOrCreateRootCanvas(string name, int sortOrder)
{
Scene scene = SceneManager.GetActiveScene();
// ── Phase 1: 搜索已存在的 Canvas ─────────────────────────────────
foreach (GameObject root in scene.GetRootGameObjects())
{
if (root.name == name)
return root;
// 兼容 ScaffoldPersistentScene 层级:[Persistent] ▸ [UI] ▸ UIRoot ▸ HUD Canvas
foreach (string path in new[] { $"[UI]/UIRoot/{name}", $"UIRoot/{name}", name })
{
Transform found = root.transform.Find(path);
if (found != null) return found.gameObject;
}
}
// ── Phase 2: 定位 UIRootCanvas 的正确父节点)───────────────────
Transform uiRoot = null;
foreach (GameObject root in scene.GetRootGameObjects())
{
uiRoot = root.transform.Find("[UI]/UIRoot")
?? root.transform.Find("UIRoot");
if (uiRoot != null) break;
}
if (uiRoot == null)
Debug.LogWarning(
"[HUDScaffold] 未找到 UIRoot期望路径[Persistent]/[UI]/UIRoot。" +
"将在场景根层创建 HUD Canvas。建议先执行 BaseGames/Scene/Setup/Scaffold Persistent Scene。");
// ── Phase 3: 创建 Canvas 并挂载至正确父节点 ──────────────────────
GameObject canvasGo = new GameObject(name);
Undo.RegisterCreatedObjectUndo(canvasGo, $"Create {name}");
if (uiRoot != null)
canvasGo.transform.SetParent(uiRoot, false);
Canvas canvas = canvasGo.AddComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
canvas.sortingOrder = sortOrder;
CanvasScaler scaler = canvasGo.AddComponent<CanvasScaler>();
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
scaler.referenceResolution = new Vector2(1920, 1080);
canvasGo.AddComponent<GraphicRaycaster>();
return canvasGo;
}
private static Transform GetOrCreateChild(Transform parent, string name)
{
Transform child = parent.Find(name);
if (child != null) return child;
GameObject go = new GameObject(name);
Undo.RegisterCreatedObjectUndo(go, $"Create {name}");
go.transform.SetParent(parent, false);
return go.transform;
}
private static T GetOrAddComponent<T>(GameObject go) where T : Component
{
T c = go.GetComponent<T>();
return c != null ? c : Undo.AddComponent<T>(go);
}
private static void AssignRef(Object target, string propertyName, Object value)
{
var so = new SerializedObject(target);
var prop = so.FindProperty(propertyName);
if (prop == null)
{
Debug.LogWarning($"[HUDScaffold] 未找到属性 {target.GetType().Name}.{propertyName}", target);
return;
}
prop.objectReferenceValue = value;
so.ApplyModifiedPropertiesWithoutUndo();
}
private static void AssignArrayRefs<T>(Object target, string propertyName, T[] values, List<string> report)
where T : Object
{
var so = new SerializedObject(target);
var prop = so.FindProperty(propertyName);
if (prop == null || !prop.isArray)
{
report.Add($"{target.GetType().Name}.{propertyName} 不是可写数组字段。");
return;
}
prop.arraySize = values.Length;
for (int i = 0; i < values.Length; i++)
prop.GetArrayElementAtIndex(i).objectReferenceValue = values[i];
so.ApplyModifiedPropertiesWithoutUndo();
}
private static void AssignAsset(Object target, string propertyName, List<string> report,
bool required, params string[] candidates)
{
Object asset = FindFirstAsset(candidates);
if (asset == null)
{
if (required)
report.Add($"未找到 {target.GetType().Name}.{propertyName} 所需资产: {string.Join(" / ", candidates)}");
return;
}
AssignRef(target, propertyName, asset);
}
private static Object FindFirstAsset(params string[] candidates)
{
foreach (string candidate in candidates)
{
if (string.IsNullOrWhiteSpace(candidate)) continue;
string[] guids = AssetDatabase.FindAssets(candidate);
foreach (string guid in guids)
{
string path = AssetDatabase.GUIDToAssetPath(guid);
Object asset = AssetDatabase.LoadMainAssetAtPath(path);
if (asset != null && asset.name == candidate)
return asset;
}
}
return null;
}
private static void MarkDirtyAndLog(string scaffoldName, GameObject root, List<string> report)
{
EditorSceneManager.MarkSceneDirty(SceneManager.GetActiveScene());
Selection.activeGameObject = root;
if (report.Count == 0)
{
Debug.Log($"[HUDScaffold] {scaffoldName} 完成。", root);
return;
}
Debug.LogWarning(
$"[HUDScaffold] {scaffoldName} 完成,以下 {report.Count} 项需手动确认:\n- {string.Join("\n- ", report)}",
root);
}
}
}