Files
zeling_v2/Assets/_Game/Scripts/Editor/UI/HUDScaffoldWizard.cs
Joywayer 613f2a4d13 feat: Enhance scene transition management and HUD scaffolding
- Added RequestTransition method to ISceneService for direct scene transition requests without needing Inspector SO references.
- Updated DoorTransition and RoomTransition to utilize the new RequestTransition method via ServiceLocator.
- Introduced SceneFadeController to manage scene fade effects during transitions, with event channel integration for fade requests.
- Created HUDScaffoldWizard to automate HUD Canvas setup, including various UI elements and event channel bindings.
- Updated assembly definitions to include necessary dependencies for new UI components.
- Added Streaming assets for budget configuration to optimize scene loading and memory management.
2026-06-03 08:08:27 +08:00

403 lines
23 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);
// ── 事件频道 ──────────────────────────────────────────────────────
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");
// ── 手工步骤说明 ──────────────────────────────────────────────────
report.Add("HUDController._hpCellPrefab请将 HP 格子 Prefab 赋给该字段。");
report.Add("HUDController._springIconPrefab请将回春图标 Prefab 赋给该字段。");
report.Add("BossHPBar._phaseMarkerPrefab请将阶段标记点 Prefab 赋给该字段。");
report.Add("StatusEffectHUD._slotConfigs请在 Inspector 中配置各状态效果的图标映射。");
Undo.CollapseUndoOperations(undoGroup);
MarkDirtyAndLog("HUD Canvas 脚手架", canvasGo, report);
}
// ─────────────────────────────────────────────────────────────────────
// Private helpers
// ─────────────────────────────────────────────────────────────────────
/// <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);
}
}
}