Files
zeling_v2/Assets/_Game/Scripts/Editor/UI/HUDScaffoldWizard.cs
Joywayer a1f54b68e6 Refactor interaction prompt system to use world space prompts
- Removed the InteractPromptWidget from HUD and its references in HUDController.
- Introduced IInteractPromptView interface for world space interaction prompts.
- Implemented WorldInteractPrompt class to manage display of interaction prompts in world space.
- Updated InteractableDetector to handle showing/hiding of world space prompts based on player proximity to interactable objects.
- Created a new prefab for UI_WorldInteractPrompt to facilitate the new interaction prompt system.
2026-06-10 14:14:08 +08:00

486 lines
28 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;
StretchFull(hudRootGo.transform); // 铺满 Canvas使子元件的四角锚定映射到真实屏幕角
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;
var formLayout = GetOrAddComponent<HorizontalLayoutGroup>(formIconsRoot);
formLayout.childForceExpandWidth = false;
formLayout.childForceExpandHeight = false;
formLayout.spacing = 6f;
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);
var le = GetOrAddComponent<LayoutElement>(iconGo);
le.preferredWidth = 40f;
le.preferredHeight = 40f;
}
// 交互提示已改为「每个交互物自带的世界空间提示」WorldInteractPrompt
// 不再放在屏幕固定 HUD 上。由交互物脚手架在各自预制体/场景物体上创建。
// ── 法术槽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);
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(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");
// ── 布局摆位(点锚定到屏幕四角,避免元件堆叠在中心;尺寸/留白为占位,美术可再精修)──
// 左上角:战斗信息簇(血条 / 魄元 / 灵力 / 灵泉 / 形态 / 灵铢 / 状态效果,自上而下)
SetRect(hpContainerGo.transform, TopLeft, TopLeft, new Vector2(40f, -40f), new Vector2(500f, 50f));
SetRect(soulGaugeGo.transform, TopLeft, TopLeft, new Vector2(40f, -100f), new Vector2(240f, 22f));
SetRect(spiritGaugeGo.transform, TopLeft, TopLeft, new Vector2(40f, -128f), new Vector2(240f, 22f));
SetRect(springContainerGo.transform, TopLeft, TopLeft, new Vector2(40f, -160f), new Vector2(240f, 32f));
SetRect(formIconsRoot.transform, TopLeft, TopLeft, new Vector2(40f, -200f), new Vector2(240f, 44f));
SetRect(lingZhuGo.transform, TopLeft, TopLeft, new Vector2(40f, -252f), new Vector2(200f, 40f));
SetRect(statusHUDGo.transform, TopLeft, TopLeft, new Vector2(40f, -300f), new Vector2(320f, 44f));
// 顶部居中Boss 血条(默认隐藏)
SetRect(bossBarGo.transform, TopCenter, TopCenter, new Vector2(0f, -40f), new Vector2(720f, 60f));
// 右上角:任务追踪
SetRect(questTrackerGo.transform, TopRight, TopRight, new Vector2(-40f, -40f), new Vector2(320f, 220f));
// 右下角:法术槽 + 工具栏
SetRect(spellSlotGo.transform, BottomRight, BottomRight, new Vector2(-40f, 40f), new Vector2(90f, 90f));
SetRect(toolHUDGo.transform, BottomRight, BottomRight, new Vector2(-150f, 40f), new Vector2(180f, 90f));
// ── 手工步骤说明 ──────────────────────────────────────────────────
// _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;
// UI 节点必须是 RectTransformnew GameObject 默认创建普通 Transform
// 仅当随后挂上 Graphic/LayoutGroup 时才会被隐式转换——纯逻辑容器FormIcons / SpellSlot 等)
// 会停留为普通 Transform 而无法锚定/定位。显式带 RectTransform 创建,杜绝该缺陷。
GameObject go = new GameObject(name, typeof(RectTransform));
Undo.RegisterCreatedObjectUndo(go, $"Create {name}");
go.transform.SetParent(parent, false);
return go.transform;
}
// ── 锚定 / 布局 ───────────────────────────────────────────────────────
/// <summary>设置 RectTransform 的锚点 / 轴心 / 锚定坐标 / 尺寸(统一布局入口)。</summary>
private static void SetRect(Transform t, Vector2 anchor, Vector2 pivot, Vector2 anchoredPos, Vector2 size)
{
var rt = t as RectTransform;
if (rt == null) return;
rt.anchorMin = anchor;
rt.anchorMax = anchor;
rt.pivot = pivot;
rt.sizeDelta = size;
rt.anchoredPosition = anchoredPos;
}
/// <summary>将 RectTransform 设为全屏拉伸铺满父节点offset 全 0。用于 HUDRoot 等容器根。</summary>
private static void StretchFull(Transform t)
{
var rt = t as RectTransform;
if (rt == null) return;
rt.anchorMin = Vector2.zero;
rt.anchorMax = Vector2.one;
rt.pivot = new Vector2(0.5f, 0.5f);
rt.offsetMin = Vector2.zero;
rt.offsetMax = Vector2.zero;
}
// 常用锚/轴心常量(点锚定,非拉伸)
private static readonly Vector2 TopLeft = new(0f, 1f);
private static readonly Vector2 TopCenter = new(0.5f, 1f);
private static readonly Vector2 TopRight = new(1f, 1f);
private static readonly Vector2 BottomRight = new(1f, 0f);
private static readonly Vector2 BottomCenter= new(0.5f, 0f);
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);
}
}
}