地图系统

This commit is contained in:
2026-06-05 18:41:33 +08:00
parent 613f2a4d13
commit fe4fd60083
234 changed files with 33090 additions and 4899 deletions

View File

@@ -0,0 +1,656 @@
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);
}
}
}