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 { /// /// HUD 脚手架工具(架构 10_UIModule §HUD)。 /// 在当前活动场景中生成完整的 HUD Canvas 层级结构并自动绑定已存在的事件频道资产。 /// 执行路径:BaseGames ▸ Scene ▸ Setup ▸ Scaffold HUD Canvas /// public static class HUDScaffoldWizard { [MenuItem("BaseGames/Scene/Setup/Scaffold HUD Canvas", priority = 203)] public static void ScaffoldHUDCanvas() { var report = new List(); 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(hudRootGo); // 若场景中已存在 UIManager,自动将 _hudRoot 指向本 HUDRoot UIManager uiManager = Object.FindFirstObjectByType(); if (uiManager != null) AssignRef(uiManager, "_hudRoot", hudRootGo); // ── HP 区域 ─────────────────────────────────────────────────────── GameObject hpContainerGo = GetOrCreateChild(hudRootGo.transform, "HP_Container").gameObject; var hpLayout = GetOrAddComponent(hpContainerGo); hpLayout.childForceExpandWidth = false; hpLayout.childForceExpandHeight = false; hpLayout.spacing = 4f; // ── 魄元(Soul)量条 ────────────────────────────────────────────── GameObject soulGaugeGo = GetOrCreateChild(hudRootGo.transform, "Gauge_Soul").gameObject; Image soulFill = GetOrAddComponent(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(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(lingZhuGo); lingZhuText.text = "0"; // ── 回春图标(Spring Charges)───────────────────────────────────── GameObject springContainerGo = GetOrCreateChild(hudRootGo.transform, "Spring_Container").gameObject; var springLayout = GetOrAddComponent(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(iconGo); } // ── 交互提示(InteractPrompt)───────────────────────────────────── GameObject interactPromptGo = GetOrCreateChild(hudRootGo.transform, "InteractPrompt").gameObject; InteractPromptWidget interactWidget = GetOrAddComponent(interactPromptGo); GameObject promptRootGo = GetOrCreateChild(interactPromptGo.transform, "Root").gameObject; GameObject promptKeyIconGo = GetOrCreateChild(promptRootGo.transform, "KeyIcon").gameObject; Image keyIcon = GetOrAddComponent(promptKeyIconGo); GameObject promptLabelGo = GetOrCreateChild(promptRootGo.transform, "LabelText").gameObject; TMP_Text promptLabel = GetOrAddComponent(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(spellSlotGo); GameObject spellIconGo = GetOrCreateChild(spellSlotGo.transform, "IconImage").gameObject; Image spellIcon = GetOrAddComponent(spellIconGo); GameObject spellCdGo = GetOrCreateChild(spellSlotGo.transform, "CooldownFill").gameObject; Image spellCd = GetOrAddComponent(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(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(slotGo); GameObject iconGo = GetOrCreateChild(slotGo.transform, "Icon").gameObject; Image slotIcon = GetOrAddComponent(iconGo); GameObject usesGo = GetOrCreateChild(slotGo.transform, "UsesText").gameObject; TMP_Text usesText = GetOrAddComponent(usesGo); usesText.text = "0"; GameObject cdGo = GetOrCreateChild(slotGo.transform, "CooldownMask").gameObject; Image cdMask = GetOrAddComponent(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(bossBarGo); GameObject bossNameGo = GetOrCreateChild(bossBarGo.transform, "BossName").gameObject; TMP_Text bossName = GetOrAddComponent(bossNameGo); bossName.text = "Boss 名称"; GameObject bossHPFillGo = GetOrCreateChild(bossBarGo.transform, "HPFill").gameObject; Image bossHPFill = GetOrAddComponent(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); // ── 状态效果 HUD(StatusEffectHUD)────────────────────────────── GameObject statusHUDGo = GetOrCreateChild(hudRootGo.transform, "StatusEffectHUD").gameObject; StatusEffectHUD statusHUD = GetOrAddComponent(statusHUDGo); GameObject statusTemplateGo = GetOrCreateChild(statusHUDGo.transform, "SlotTemplate").gameObject; statusTemplateGo.SetActive(false); GetOrAddComponent(statusTemplateGo); GameObject statusContainerGo = GetOrCreateChild(statusHUDGo.transform, "Container").gameObject; var statusLayout = GetOrAddComponent(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(questTrackerGo); GameObject questTitleGo = GetOrCreateChild(questTrackerGo.transform, "QuestTitle").gameObject; TMP_Text questTitle = GetOrAddComponent(questTitleGo); questTitle.text = "任务名称"; GameObject objTemplateGo = GetOrCreateChild(questTrackerGo.transform, "ObjectiveRowTemplate").gameObject; objTemplateGo.SetActive(false); GetOrAddComponent(objTemplateGo); GameObject objContainerGo = GetOrCreateChild(questTrackerGo.transform, "ObjectiveContainer").gameObject; var objLayout = GetOrAddComponent(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 // ───────────────────────────────────────────────────────────────────── /// /// 创建(或复用)一个 HUD 图标 Prefab(含 RectTransform + Image + LayoutElement), /// 用作 HUDController._hpCellPrefab / _springIconPrefab。占位纯色方块,美术后续可替换 sprite。 /// 路径:Assets/_Game/Prefabs/UI/{prefabName}.prefab(符合 AssetFolderSpec.md §4 UI 前缀)。 /// private static GameObject EnsureHUDIconPrefab(string prefabName, Color color, Vector2 size, List report) { const string uiDir = "Assets/_Game/Prefabs/UI"; string path = $"{uiDir}/{prefabName}.prefab"; var existing = AssetDatabase.LoadAssetAtPath(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(); rt.sizeDelta = size; var img = go.AddComponent(); img.color = color; var le = go.AddComponent(); le.preferredWidth = size.x; le.preferredHeight = size.y; var prefab = PrefabUtility.SaveAsPrefabAsset(go, path); Object.DestroyImmediate(go); report.Add($"已自动创建 HUD 图标 Prefab:{path}(占位纯色,美术可替换)。"); return prefab; } /// /// 在活动场景中查找或创建 HUD Canvas。 /// 查找顺序: /// 1. 遍历场景根节点中的直接同名节点 /// 2. 遍历各根节点下的 [UI]/UIRoot/{name} 或 UIRoot/{name} 子路径 /// 若均不存在则新建 Canvas,并优先挂载至 UIRoot(兼容 ScaffoldPersistentScene 层级); /// 若场景中不存在 UIRoot,则回退到场景根层并发出警告。 /// 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: 定位 UIRoot(Canvas 的正确父节点)─────────────────── 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.renderMode = RenderMode.ScreenSpaceOverlay; canvas.sortingOrder = sortOrder; CanvasScaler scaler = canvasGo.AddComponent(); scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize; scaler.referenceResolution = new Vector2(1920, 1080); canvasGo.AddComponent(); 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(GameObject go) where T : Component { T c = go.GetComponent(); return c != null ? c : Undo.AddComponent(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(Object target, string propertyName, T[] values, List 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 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 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); } } }