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

657 lines
36 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 TMPro;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
using BaseGames.UI;
using BaseGames.UI.Menus;
using BaseGames.World.Map;
using BaseGames.Localization;
namespace BaseGames.Editor.UI
{
/// <summary>
/// 地图 UI 脚手架(对照 <see cref="HUDScaffoldWizard"/>)。
/// 在当前活动场景生成完整的地图 UI 层级与预制,并按规范绑定引用:
/// <list type="bullet">
/// <item>Cell / Pin / ExitConnector 预制 + MapPinConfig 占位资产</item>
/// <item>HUD 下小地图MinimapHUD + MinimapInputHandler</item>
/// <item>Map Canvas 下全屏地图MapPanel + MapInputHandlerPanelStack 管理)</item>
/// <item>传送确认框ConfirmDialogController+ MapTeleportConfirmController接 MapPanel</item>
/// <item>登记 UIManager._panels[Map],绑定 EVT_MapOpen</item>
/// </list>
/// 执行路径BaseGames ▸ Scene ▸ Setup ▸ Scaffold Map UI
/// <para>占位为纯色块/空 Sprite美术后续替换运行依赖 [GameManagers] 下的
/// MapManager / MapPlayerTracker / MapPinManager / TeleportService 已存在(另由 Persistent 脚手架搭建)。</para>
/// </summary>
public static class MapUIScaffoldWizard
{
private const string UiPrefabDir = "Assets/_Game/Prefabs/UI";
private const string MapDataDir = "Assets/_Game/Data/Map";
[MenuItem("BaseGames/Scene/Setup/Scaffold Map UI", priority = 204)]
public static void ScaffoldMapUI()
{
var report = new List<string>();
Undo.SetCurrentGroupName("Scaffold Map UI");
int undoGroup = Undo.GetCurrentGroup();
// ── 共享资产Cell / Pin / Exit 预制 + PinConfig ──────────────────
MapPinConfigSO pinConfig = EnsurePinConfig(report);
GameObject cellPrefab = EnsureCellPrefab(report);
GameObject pinPrefab = EnsureSimpleImagePrefab("UI_Map_Pin",
new Color32(0xF0, 0xC0, 0x40, 0xFF), new Vector2(14, 14), report);
GameObject exitPrefab = EnsureSimpleImagePrefab("UI_Map_ExitConnector",
new Color32(0xC0, 0xC0, 0xC0, 0xCC), new Vector2(8, 16), report);
Sprite playerDotSprite = null; // 占位用纯色,留空即可
// ── 小地图HUD Canvas 下)────────────────────────────────────────
GameObject hudCanvas = FindHudCanvas();
if (hudCanvas == null)
report.Add("未找到 HUD Canvas请先执行 BaseGames/Scene/Setup/Scaffold HUD Canvas本次跳过小地图/区域名搭建。");
else
{
// 按需求不搭建右上角小地图,只保留全屏大地图;保留进入区域时的区域名横幅。
BuildRegionBanner(hudCanvas, report);
report.Add("已跳过小地图MinimapHUD搭建按需求只保留全屏大地图。");
}
// ── 全屏地图(独立 Map CanvasPanelStack 管理)───────────────────
GameObject mapPanelRoot = BuildFullMap(cellPrefab, exitPrefab, pinPrefab, pinConfig, report);
// ── 传送确认框 + 控制器 ───────────────────────────────────────────
BuildTeleportConfirm(mapPanelRoot, report);
// ── 登记 UIManager._panels[Map] + 绑定 EVT_MapOpen ────────────────
RegisterMapPanelWithUIManager(mapPanelRoot, report);
Undo.CollapseUndoOperations(undoGroup);
AssetDatabase.SaveAssets();
MarkDirtyAndLog("Map UI 脚手架", mapPanelRoot != null ? mapPanelRoot : hudCanvas, report);
}
// ─────────────────────────────────────────────────────────────────────
// 预制 / 资产
// ─────────────────────────────────────────────────────────────────────
/// <summary>创建或复用MapRoomCellUI 预制_bg 为 Raycast Target可点击传送其余子图不挡射线。</summary>
private static GameObject EnsureCellPrefab(List<string> report)
{
string path = $"{UiPrefabDir}/UI_Map_RoomCell.prefab";
var existing = AssetDatabase.LoadAssetAtPath<GameObject>(path);
if (existing != null) return existing;
EnsureFolder(UiPrefabDir);
var root = new GameObject("UI_Map_RoomCell", typeof(RectTransform));
((RectTransform)root.transform).sizeDelta = new Vector2(32, 32);
// 背景(可见 + 可点击)
var bg = MakeImage(root.transform, "BG", new Color(1f, 1f, 1f, 1f), new Vector2(32, 32), raycast: true);
// 轮廓RawImage默认禁用
var outlineGo = MakeChild(root.transform, "Outline", new Vector2(32, 32));
var outline = outlineGo.AddComponent<RawImage>();
outline.raycastTarget = false; outline.enabled = false;
// 图标 / 高亮 / 雾 / 传送标记(均不挡射线,运行时按需启用)
var icon = MakeImage(root.transform, "Icon", new Color(1, 1, 1, 1), new Vector2(20, 20), raycast: false); icon.enabled = false;
var highlight = MakeImage(root.transform, "Highlight", new Color(1f, 0.9f, 0.2f, 1f),new Vector2(34, 34), raycast: false); highlight.enabled = false;
var fog = MakeImage(root.transform, "Fog", new Color(0, 0, 0, 0.85f), new Vector2(32, 32), raycast: false); fog.enabled = false;
var teleport = MakeImage(root.transform, "TeleportMarker", new Color(0.3f, 0.8f, 1f, 1f), new Vector2(12, 12), raycast: false); teleport.enabled = false;
var cell = root.AddComponent<MapRoomCellUI>();
AssignRef(cell, "_bg", bg);
AssignRef(cell, "_icon", icon);
AssignRef(cell, "_outlineImage", outline);
AssignRef(cell, "_highlight", highlight);
AssignRef(cell, "_fogOverlay", fog);
AssignRef(cell, "_teleportMarker",teleport);
var prefab = PrefabUtility.SaveAsPrefabAsset(root, path);
Object.DestroyImmediate(root);
report.Add($"已创建 Cell 预制:{path}(占位纯色,美术可替换)。");
return prefab;
}
private static GameObject EnsureSimpleImagePrefab(string name, Color color, Vector2 size, List<string> report)
{
string path = $"{UiPrefabDir}/{name}.prefab";
var existing = AssetDatabase.LoadAssetAtPath<GameObject>(path);
if (existing != null) return existing;
EnsureFolder(UiPrefabDir);
var go = new GameObject(name, typeof(RectTransform));
((RectTransform)go.transform).sizeDelta = size;
var img = go.AddComponent<Image>();
img.color = color;
img.raycastTarget = false;
var prefab = PrefabUtility.SaveAsPrefabAsset(go, path);
Object.DestroyImmediate(go);
report.Add($"已创建预制:{path}(占位纯色,美术可替换)。");
return prefab;
}
private static MapPinConfigSO EnsurePinConfig(List<string> report)
{
string path = $"{MapDataDir}/MapPinConfig.asset";
var existing = AssetDatabase.LoadAssetAtPath<MapPinConfigSO>(path);
if (existing != null) return existing;
EnsureFolder(MapDataDir);
var so = ScriptableObject.CreateInstance<MapPinConfigSO>();
AssetDatabase.CreateAsset(so, path);
report.Add($"已创建占位 MapPinConfig{path}_entries 为空,请配置 PinType→Sprite 映射)。");
return so;
}
// ─────────────────────────────────────────────────────────────────────
// 小地图HUD
// ─────────────────────────────────────────────────────────────────────
private static void BuildMinimap(GameObject hudCanvas, GameObject cellPrefab,
GameObject pinPrefab, MapPinConfigSO pinConfig, List<string> report)
{
Transform hudRoot = hudCanvas.transform.Find("HUDRoot") ?? hudCanvas.transform;
var minimapGo = GetOrCreateChild(hudRoot, "Minimap").gameObject;
var mmRect = minimapGo.GetComponent<RectTransform>() ?? minimapGo.AddComponent<RectTransform>();
AnchorTopRight(mmRect, new Vector2(180, 180), new Vector2(-16, -16));
// 边框底图(可见,便于定位;美术可替换/移除)
var frame = minimapGo.GetComponent<Image>() ?? minimapGo.AddComponent<Image>();
frame.color = new Color(0f, 0f, 0f, 0.35f);
frame.raycastTarget = false;
// 带 RectMask2D 的内容容器cell 在此平移)
var viewportGo = GetOrCreateChild(minimapGo.transform, "Viewport").gameObject;
var vpRect = viewportGo.GetComponent<RectTransform>() ?? viewportGo.AddComponent<RectTransform>();
StretchFill(vpRect, 6f);
if (viewportGo.GetComponent<RectMask2D>() == null) viewportGo.AddComponent<RectMask2D>();
// 玩家圆点(在容器内)
var playerDot = MakeImage(viewportGo.transform, "PlayerDot", new Color(1f, 0.25f, 0.25f, 1f), new Vector2(8, 8), raycast: false);
var hud = GetOrAddComponent<MinimapHUD>(minimapGo);
var input = GetOrAddComponent<MinimapInputHandler>(minimapGo);
AssignRef(hud, "_cellPrefab", cellPrefab.GetComponent<MapRoomCellUI>());
AssignRef(hud, "_cellContainer",vpRect);
AssignRef(hud, "_playerDot", playerDot);
AssignRef(hud, "_pinPrefab", pinPrefab.GetComponent<Image>());
AssignRef(hud, "_pinConfig", pinConfig);
AssignMapIcons(hud, report);
AssignAsset(input, "_inputReader", report, true, "InputReader");
report.Add("Minimap 已搭建于 HUDRoot右上角占位框。美术可调整位置/尺寸/边框。");
}
/// <summary>进入新区域时屏幕中央渐显区域名横幅RegionNameDisplay挂 HUDRoot。</summary>
private static void BuildRegionBanner(GameObject hudCanvas, List<string> report)
{
Transform hudRoot = hudCanvas.transform.Find("HUDRoot") ?? hudCanvas.transform;
var bannerGo = GetOrCreateChild(hudRoot, "RegionNameBanner").gameObject;
var br = bannerGo.GetComponent<RectTransform>() ?? bannerGo.AddComponent<RectTransform>();
br.anchorMin = br.anchorMax = new Vector2(0.5f, 0.72f);
br.pivot = new Vector2(0.5f, 0.5f);
br.sizeDelta = new Vector2(640f, 90f);
br.anchoredPosition = Vector2.zero;
if (bannerGo.GetComponent<CanvasGroup>() == null) bannerGo.AddComponent<CanvasGroup>();
var txt = MakeText(bannerGo.transform, "RegionText", "区域名");
txt.fontSize = 48;
StretchFill((RectTransform)txt.transform, 0f);
var rnd = GetOrAddComponent<RegionNameDisplay>(bannerGo);
AssignRef(rnd, "_regionText", txt);
AssignAsset(rnd, "_onRegionChanged", report, false, "EVT_RegionChanged");
report.Add("RegionNameDisplay进入区域时渐显区域名横幅已搭建于 HUDRoot。");
}
// ─────────────────────────────────────────────────────────────────────
// 全屏地图(独立 Canvas
// ─────────────────────────────────────────────────────────────────────
private static GameObject BuildFullMap(GameObject cellPrefab, GameObject exitPrefab,
GameObject pinPrefab, MapPinConfigSO pinConfig, List<string> report)
{
GameObject canvasGo = GetOrCreateMapCanvas("Map Canvas", 25);
var panelGo = GetOrCreateChild(canvasGo.transform, "MapPanel").gameObject;
StretchFill(panelGo.GetComponent<RectTransform>() ?? panelGo.AddComponent<RectTransform>(), 0f);
// 半透明全屏底
var panelBg = panelGo.GetComponent<Image>() ?? panelGo.AddComponent<Image>();
panelBg.color = new Color(0.05f, 0.05f, 0.07f, 0.92f);
// ScrollView → Viewport(RectMask2D) → RoomContainer(content)
var scrollGo = GetOrCreateChild(panelGo.transform, "ScrollView").gameObject;
StretchFill(scrollGo.GetComponent<RectTransform>() ?? scrollGo.AddComponent<RectTransform>(), 40f);
var scrollRect = GetOrAddComponent<ScrollRect>(scrollGo);
scrollRect.horizontal = true; scrollRect.vertical = true;
scrollRect.movementType = ScrollRect.MovementType.Clamped;
scrollRect.scrollSensitivity = 0f; // 缩放/平移由 MapInputHandler 处理
var viewportGo = GetOrCreateChild(scrollGo.transform, "Viewport").gameObject;
var vpRect = viewportGo.GetComponent<RectTransform>() ?? viewportGo.AddComponent<RectTransform>();
StretchFill(vpRect, 0f);
if (viewportGo.GetComponent<RectMask2D>() == null) viewportGo.AddComponent<RectMask2D>();
var vpImg = viewportGo.GetComponent<Image>() ?? viewportGo.AddComponent<Image>();
vpImg.color = new Color(0, 0, 0, 0.01f); // 近透明,作为 ScrollRect viewport 的图形
var contentGo = GetOrCreateChild(viewportGo.transform, "RoomContainer").gameObject;
var contentRect = contentGo.GetComponent<RectTransform>() ?? contentGo.AddComponent<RectTransform>();
contentRect.anchorMin = contentRect.anchorMax = new Vector2(0.5f, 0.5f);
contentRect.pivot = new Vector2(0.5f, 0.5f);
contentRect.sizeDelta = new Vector2(4000, 4000);
scrollRect.content = contentRect;
scrollRect.viewport = vpRect;
// 玩家图标content 内)
var playerIcon = MakeImage(contentGo.transform, "PlayerIcon", new Color(1f, 0.3f, 0.3f, 1f), new Vector2(16, 16), raycast: false);
// Tooltip默认隐藏
var tooltipGo = GetOrCreateChild(panelGo.transform, "Tooltip").gameObject;
var ttRect = tooltipGo.GetComponent<RectTransform>() ?? tooltipGo.AddComponent<RectTransform>();
AnchorTopRight(ttRect, new Vector2(240, 60), new Vector2(-20, -20));
var ttImg = tooltipGo.GetComponent<Image>() ?? tooltipGo.AddComponent<Image>();
ttImg.color = new Color(0, 0, 0, 0.8f); ttImg.raycastTarget = false;
var ttText = MakeText(tooltipGo.transform, "Text", "房间");
tooltipGo.SetActive(false);
// 组件
var mapPanel = GetOrAddComponent<MapPanel>(panelGo);
var mapInput = GetOrAddComponent<MapInputHandler>(panelGo);
AssignRef(mapPanel, "_roomContainer", contentRect);
AssignRef(mapPanel, "_cellPrefab", cellPrefab.GetComponent<MapRoomCellUI>());
AssignRef(mapPanel, "_exitConnectorPrefab", exitPrefab.GetComponent<Image>());
AssignRef(mapPanel, "_scrollRect", scrollRect);
AssignRef(mapPanel, "_playerIconImg", playerIcon);
AssignRef(mapPanel, "_pinPrefab", pinPrefab.GetComponent<Image>());
AssignRef(mapPanel, "_pinConfig", pinConfig);
AssignRef(mapPanel, "_tooltipPanel", tooltipGo);
AssignRef(mapPanel, "_tooltipText", ttText);
AssignMapIcons(mapPanel, report);
AssignRefObj(mapPanel, "_iconPlayerPos", null); // 占位玩家图标用纯色,留空
AssignAsset(mapInput, "_inputReader", report, true, "InputReader");
AssignRef(mapInput, "_scrollRect", scrollRect);
AssignRef(mapInput, "_zoomTarget", contentRect);
// ── 探索进度(左上角,全局% + 当前区域%;格式串走本地化 Key──────
var progGo = GetOrCreateChild(panelGo.transform, "ProgressDisplay").gameObject;
var progRect = progGo.GetComponent<RectTransform>() ?? progGo.AddComponent<RectTransform>();
progRect.anchorMin = progRect.anchorMax = new Vector2(0f, 1f);
progRect.pivot = new Vector2(0f, 1f);
progRect.anchoredPosition = new Vector2(28f, -24f);
progRect.sizeDelta = new Vector2(380f, 96f);
var globalTxt = MakeText(progGo.transform, "GlobalProgress", "0%");
var gRt = (RectTransform)globalTxt.transform; gRt.anchorMin = gRt.anchorMax = new Vector2(0f, 1f); gRt.pivot = new Vector2(0f, 1f); gRt.anchoredPosition = Vector2.zero;
globalTxt.alignment = TextAlignmentOptions.TopLeft;
var regionTxt = MakeText(progGo.transform, "RegionProgress", "0%");
var rRt = (RectTransform)regionTxt.transform; rRt.anchorMin = rRt.anchorMax = new Vector2(0f, 1f); rRt.pivot = new Vector2(0f, 1f); rRt.anchoredPosition = new Vector2(0f, -44f);
regionTxt.alignment = TextAlignmentOptions.TopLeft; regionTxt.fontSize = 24;
var prog = GetOrAddComponent<MapProgressDisplay>(progGo);
AssignRef(prog, "_globalProgressText", globalTxt);
AssignRef(prog, "_regionProgressText", regionTxt);
AssignString(prog, "_globalFormat", "MAP_PROGRESS_GLOBAL"); // 本地化 KeyMapProgressDisplay 运行时解析为格式串
AssignString(prog, "_regionFormat", "MAP_PROGRESS_REGION");
AssignAsset(prog, "_onRegionChanged", report, false, "EVT_RegionChanged");
// ── 关闭提示(底部居中):输入图标(随设备自适应) + 本地化标签 ──────
var hintRow = GetOrCreateChild(panelGo.transform, "CloseHint").gameObject;
var hintRowRt = hintRow.GetComponent<RectTransform>() ?? hintRow.AddComponent<RectTransform>();
hintRowRt.anchorMin = hintRowRt.anchorMax = new Vector2(0.5f, 0f);
hintRowRt.pivot = new Vector2(0.5f, 0f);
hintRowRt.anchoredPosition = new Vector2(0f, 24f);
hintRowRt.sizeDelta = new Vector2(360f, 40f);
var hintLayout = GetOrAddComponent<HorizontalLayoutGroup>(hintRow);
hintLayout.childAlignment = TextAnchor.MiddleCenter; hintLayout.spacing = 8f;
hintLayout.childForceExpandWidth = false; hintLayout.childForceExpandHeight = false;
MakeInputIcon(hintRow.transform, "CloseIcon", "Cancel"); // 复用 InputIconImage键鼠/手柄自动显示对应按键图标
var hintLabel = MakeLocalizedText(hintRow.transform, "CloseLabel", "MAP_CLOSE_HINT");
hintLabel.fontSize = 22;
report.Add("关闭提示InputIconImage(动作 'Cancel') + LocalizedText('MAP_CLOSE_HINT')。" +
"动作名需与 InputActions 一致;按键图标需在 InputDeviceIconSetSO 中配置(用 Input Icon Studio。");
panelGo.SetActive(false); // 由 PanelStack 控制显隐
report.Add("MapPanel 已搭建(含探索进度 + 输入图标关闭提示;默认隐藏,由 UIManager PanelStack 管理)。");
return panelGo;
}
// ─────────────────────────────────────────────────────────────────────
// 传送确认框 + 控制器
// ─────────────────────────────────────────────────────────────────────
private static void BuildTeleportConfirm(GameObject mapPanelRoot, List<string> report)
{
if (mapPanelRoot == null) { report.Add("MapPanel 缺失,跳过传送确认框搭建。"); return; }
Transform canvas = mapPanelRoot.transform.parent ?? mapPanelRoot.transform;
// 确认框自包含SetActive 显隐)
var dialogGo = GetOrCreateChild(canvas, "TeleportConfirmDialog").gameObject;
StretchFill(dialogGo.GetComponent<RectTransform>() ?? dialogGo.AddComponent<RectTransform>(), 0f);
var dimImg = dialogGo.GetComponent<Image>() ?? dialogGo.AddComponent<Image>();
dimImg.color = new Color(0, 0, 0, 0.6f);
var boxGo = GetOrCreateChild(dialogGo.transform, "Box").gameObject;
var boxRect = boxGo.GetComponent<RectTransform>() ?? boxGo.AddComponent<RectTransform>();
boxRect.anchorMin = boxRect.anchorMax = new Vector2(0.5f, 0.5f);
boxRect.pivot = new Vector2(0.5f, 0.5f);
boxRect.sizeDelta = new Vector2(520, 260);
var boxImg = boxGo.GetComponent<Image>() ?? boxGo.AddComponent<Image>();
boxImg.color = new Color(0.12f, 0.12f, 0.15f, 1f);
var title = MakeText(boxGo.transform, "Title", "快速传送");
((RectTransform)title.transform).anchoredPosition = new Vector2(0, 90);
var body = MakeText(boxGo.transform, "Body", "传送到该地点?");
var confirmBtn = MakeButton(boxGo.transform, "ConfirmButton", "确认", new Vector2(-110, -90), out TMP_Text confirmLabel);
var cancelBtn = MakeButton(boxGo.transform, "CancelButton", "取消", new Vector2( 110, -90), out TMP_Text cancelLabel);
var dialog = GetOrAddComponent<ConfirmDialogController>(dialogGo);
AssignRef(dialog, "_root", dialogGo);
AssignRef(dialog, "_titleText", title);
AssignRef(dialog, "_bodyText", body);
AssignRef(dialog, "_confirmLabel", confirmLabel);
AssignRef(dialog, "_cancelLabel", cancelLabel);
AssignRef(dialog, "_btnConfirm", confirmBtn);
AssignRef(dialog, "_btnCancel", cancelBtn);
dialogGo.SetActive(false);
// 控制器:接 MapPanel 的 OnTeleportStationSelected
var ctrlGo = GetOrCreateChild(canvas, "MapTeleportConfirmController").gameObject;
var ctrl = GetOrAddComponent<MapTeleportConfirmController>(ctrlGo);
AssignRef(ctrl, "_mapPanel", mapPanelRoot.GetComponent<MapPanel>());
AssignRef(ctrl, "_confirmDialog", dialog);
report.Add("传送确认框 + MapTeleportConfirmController 已搭建并绑定 MapPanel。");
}
// ─────────────────────────────────────────────────────────────────────
// UIManager 登记
// ─────────────────────────────────────────────────────────────────────
private static void RegisterMapPanelWithUIManager(GameObject mapPanelRoot, List<string> report)
{
if (mapPanelRoot == null) return;
var uiManager = Object.FindFirstObjectByType<UIManager>();
if (uiManager == null)
{
report.Add("场景中无 UIManager未登记 PanelId.Map请在 UIManager._panels 手动添加 {Map, MapPanel}。");
return;
}
var so = new SerializedObject(uiManager);
var panels = so.FindProperty("_panels");
if (panels == null || !panels.isArray)
{
report.Add("UIManager._panels 不可写,请手动登记 PanelId.Map。");
}
else
{
// 已登记则跳过
bool exists = false;
for (int i = 0; i < panels.arraySize; i++)
{
var el = panels.GetArrayElementAtIndex(i);
if (el.FindPropertyRelative("id").enumValueIndex == (int)PanelId.Map)
{
el.FindPropertyRelative("root").objectReferenceValue = mapPanelRoot;
exists = true; break;
}
}
if (!exists)
{
int idx = panels.arraySize;
panels.arraySize = idx + 1;
var el = panels.GetArrayElementAtIndex(idx);
el.FindPropertyRelative("id").enumValueIndex = (int)PanelId.Map;
el.FindPropertyRelative("root").objectReferenceValue = mapPanelRoot;
}
so.ApplyModifiedPropertiesWithoutUndo();
report.Add("已登记 UIManager._panels[Map] → MapPanel。");
}
AssignAsset(uiManager, "_onMapOpen", report, false, "EVT_MapOpen", "EVT_OpenMap");
}
// ─────────────────────────────────────────────────────────────────────
// 通用辅助
// ─────────────────────────────────────────────────────────────────────
private static void AssignMapIcons(Object target, List<string> report)
{
AssignAsset(target, "_iconSavePoint", report, false, "ICN_Map_SavePoint", "ICN_SavePoint");
AssignAsset(target, "_iconBossRoom", report, false, "ICN_Map_Boss", "ICN_Boss");
AssignAsset(target, "_iconShop", report, false, "ICN_Map_Shop", "ICN_Shop");
AssignAsset(target, "_iconTeleport", report, false, "ICN_Map_Teleport", "ICN_Teleport");
}
private static GameObject FindHudCanvas()
{
Scene scene = SceneManager.GetActiveScene();
foreach (GameObject root in scene.GetRootGameObjects())
{
if (root.name == "HUD Canvas") return root;
foreach (string path in new[] { "[UI]/UIRoot/HUD Canvas", "UIRoot/HUD Canvas", "HUD Canvas" })
{
var found = root.transform.Find(path);
if (found != null) return found.gameObject;
}
}
return null;
}
private static GameObject GetOrCreateMapCanvas(string name, int sortOrder)
{
Scene scene = SceneManager.GetActiveScene();
Transform uiRoot = null;
foreach (GameObject root in scene.GetRootGameObjects())
{
if (root.name == name) return root;
foreach (string path in new[] { $"[UI]/UIRoot/{name}", $"UIRoot/{name}" })
{
var found = root.transform.Find(path);
if (found != null) return found.gameObject;
}
uiRoot ??= root.transform.Find("[UI]/UIRoot") ?? root.transform.Find("UIRoot");
}
var canvasGo = new GameObject(name);
Undo.RegisterCreatedObjectUndo(canvasGo, $"Create {name}");
if (uiRoot != null) canvasGo.transform.SetParent(uiRoot, false);
var canvas = canvasGo.AddComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
canvas.sortingOrder = sortOrder;
var scaler = canvasGo.AddComponent<CanvasScaler>();
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
scaler.referenceResolution = new Vector2(1920, 1080);
canvasGo.AddComponent<GraphicRaycaster>();
return canvasGo;
}
// ── UI 元素构造 ──────────────────────────────────────────────────────
private static GameObject MakeChild(Transform parent, string name, Vector2 size)
{
var go = GetOrCreateChild(parent, name).gameObject;
var rt = go.GetComponent<RectTransform>() ?? go.AddComponent<RectTransform>();
rt.sizeDelta = size;
return go;
}
private static Image MakeImage(Transform parent, string name, Color color, Vector2 size, bool raycast)
{
var go = MakeChild(parent, name, size);
var img = go.GetComponent<Image>() ?? go.AddComponent<Image>();
img.color = color;
img.raycastTarget = raycast;
return img;
}
private static TMP_Text MakeText(Transform parent, string name, string text)
{
var go = MakeChild(parent, name, new Vector2(240, 40));
var t = go.GetComponent<TextMeshProUGUI>() ?? go.AddComponent<TextMeshProUGUI>();
t.text = text;
t.alignment = TextAlignmentOptions.Center;
t.fontSize = 28;
t.raycastTarget = false;
return t;
}
private static Button MakeButton(Transform parent, string name, string label, Vector2 anchoredPos, out TMP_Text labelText)
{
var go = MakeChild(parent, name, new Vector2(160, 56));
((RectTransform)go.transform).anchorMin = ((RectTransform)go.transform).anchorMax = new Vector2(0.5f, 0.5f);
((RectTransform)go.transform).anchoredPosition = anchoredPos;
var img = go.GetComponent<Image>() ?? go.AddComponent<Image>();
img.color = new Color(0.25f, 0.25f, 0.3f, 1f);
var btn = GetOrAddComponent<Button>(go);
btn.targetGraphic = img;
labelText = MakeText(go.transform, "Label", label);
return btn;
}
/// <summary>创建带 LocalizedText 的 TMP 文本(文案随语言切换自动刷新)。返回 TMP_Text 供调整字号/对齐。</summary>
private static TMP_Text MakeLocalizedText(Transform parent, string name, string locKey)
{
var go = MakeChild(parent, name, new Vector2(180f, 36f));
var t = go.GetComponent<TextMeshProUGUI>() ?? go.AddComponent<TextMeshProUGUI>();
t.alignment = TextAlignmentOptions.Center;
t.fontSize = 22;
t.raycastTarget = false;
var lt = GetOrAddComponent<LocalizedText>(go); // RequireComponent<TMP_Text> 已满足
AssignString(lt, "_key", locKey);
return t;
}
/// <summary>创建带 InputIconImage(ByActionName) 的按键图标 Image随当前设备自适应显示。</summary>
private static InputIconImage MakeInputIcon(Transform parent, string name, string actionName)
{
var go = MakeChild(parent, name, new Vector2(36f, 36f));
var img = go.GetComponent<Image>() ?? go.AddComponent<Image>();
img.raycastTarget = false;
var icon = GetOrAddComponent<InputIconImage>(go); // 默认 ByActionName 模式
AssignString(icon, "_actionName", actionName);
return icon;
}
// ── 布局 ──────────────────────────────────────────────────────────────
private static void StretchFill(RectTransform rt, float padding)
{
rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.one;
rt.pivot = new Vector2(0.5f, 0.5f);
rt.offsetMin = new Vector2(padding, padding);
rt.offsetMax = new Vector2(-padding, -padding);
}
private static void AnchorTopRight(RectTransform rt, Vector2 size, Vector2 offset)
{
rt.anchorMin = rt.anchorMax = new Vector2(1f, 1f);
rt.pivot = new Vector2(1f, 1f);
rt.sizeDelta = size;
rt.anchoredPosition = offset;
}
// ── 引用 / 资产绑定(对照 HUDScaffoldWizard─────────────────────────
private static Transform GetOrCreateChild(Transform parent, string name)
{
var child = parent.Find(name);
if (child != null) return child;
var go = new GameObject(name);
Undo.RegisterCreatedObjectUndo(go, $"Create {name}");
go.transform.SetParent(parent, false);
go.AddComponent<RectTransform>();
return go.transform;
}
private static T GetOrAddComponent<T>(GameObject go) where T : Component
{
var 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($"[MapUIScaffold] 未找到属性 {target.GetType().Name}.{propertyName}", target);
return;
}
prop.objectReferenceValue = value;
so.ApplyModifiedPropertiesWithoutUndo();
}
private static void AssignRefObj(Object target, string propertyName, Object value) => AssignRef(target, propertyName, value);
private static void AssignString(Object target, string propertyName, string value)
{
var so = new SerializedObject(target);
var prop = so.FindProperty(propertyName);
if (prop == null)
{
Debug.LogWarning($"[MapUIScaffold] 未找到字符串属性 {target.GetType().Name}.{propertyName}", target);
return;
}
prop.stringValue = value;
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;
foreach (string guid in AssetDatabase.FindAssets(candidate))
{
string path = AssetDatabase.GUIDToAssetPath(guid);
Object asset = AssetDatabase.LoadMainAssetAtPath(path);
if (asset != null && asset.name == candidate) return asset;
}
}
return null;
}
private static void EnsureFolder(string folder)
{
if (string.IsNullOrEmpty(folder) || AssetDatabase.IsValidFolder(folder)) return;
var parts = folder.Split('/');
string cur = parts[0];
for (int i = 1; i < parts.Length; i++)
{
string next = $"{cur}/{parts[i]}";
if (!AssetDatabase.IsValidFolder(next)) AssetDatabase.CreateFolder(cur, parts[i]);
cur = next;
}
}
private static void MarkDirtyAndLog(string scaffoldName, GameObject root, List<string> report)
{
EditorSceneManager.MarkSceneDirty(SceneManager.GetActiveScene());
if (root != null) Selection.activeGameObject = root;
if (report.Count == 0) { Debug.Log($"[MapUIScaffold] {scaffoldName} 完成。", root); return; }
Debug.LogWarning($"[MapUIScaffold] {scaffoldName} 完成,以下 {report.Count} 项需手动确认:\n- {string.Join("\n- ", report)}", root);
}
}
}