@@ -0,0 +1,402 @@
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 ) ;
// ── 状态效果 HUD( StatusEffectHUD)──────────────────────────────
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: 定位 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 > ( ) ;
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 ) ;
}
}
}