地图系统

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

@@ -22,6 +22,9 @@ using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.Tilemaps;
using UnityEngine.UI;
using BaseGames.Feedback;
using MoreMountains.Feedbacks;
using TMPro;
namespace BaseGames.Editor
{
@@ -72,6 +75,8 @@ namespace BaseGames.Editor
InputReaderBootstrap inputBootstrap = GetOrAddComponent<InputReaderBootstrap>(inputHolderGo);
AssignReference(inputBootstrap, "_inputReader", inputReaderAsset, report);
// 输入模式由游戏状态驱动Gameplay/BossFight→游戏输入其余→UI 输入):绑定状态变化频道
AssignAsset(inputBootstrap, "_onGameStateChanged", report, true, "EVT_GameStateChanged", "EVT_GameState");
if (inputReaderAsset != null)
{
AssignReference(inputReaderAsset, "_onPauseRequested", FindFirstAssetByType<VoidEventChannelSO>("EVT_PauseRequested"), report);
@@ -84,6 +89,9 @@ namespace BaseGames.Editor
UnityEngine.Camera mainCamera = GetOrAddComponent<UnityEngine.Camera>(mainCameraGo);
mainCamera.orthographic = false;
mainCamera.fieldOfView = 60f;
// 2D 游戏使用纯色清除(非 Skybox避免背景层缝隙处露出 skybox/黑色;深蓝灰与场景雾色协调
mainCamera.clearFlags = UnityEngine.CameraClearFlags.SolidColor;
mainCamera.backgroundColor = new Color(0.192f, 0.302f, 0.475f, 1f);
mainCameraGo.tag = "MainCamera";
AudioListener mainCameraAudioListener = GetOrAddComponent<AudioListener>(mainCameraGo);
CinemachineBrain brain = GetOrAddComponent<CinemachineBrain>(mainCameraGo);
@@ -123,6 +131,11 @@ namespace BaseGames.Editor
HUDController hudController = GetOrAddComponent<HUDController>(hudRootGo);
GameObject pauseRootGo = GetOrCreateChild(uiRootGo.transform, "PauseMenuRoot").gameObject;
PauseMenuController pauseMenuCtrl = GetOrAddComponent<PauseMenuController>(pauseRootGo);
Button pauseBtnResume = GetOrAddComponent<Button>(GetOrCreateChild(pauseRootGo.transform, "Btn_Resume").gameObject);
Button pauseBtnSettings = GetOrAddComponent<Button>(GetOrCreateChild(pauseRootGo.transform, "Btn_Settings").gameObject);
Button pauseBtnMainMenu = GetOrAddComponent<Button>(GetOrCreateChild(pauseRootGo.transform, "Btn_MainMenu").gameObject);
Button pauseBtnQuit = GetOrAddComponent<Button>(GetOrCreateChild(pauseRootGo.transform, "Btn_Quit").gameObject);
GameObject settingsRootGo = GetOrCreateChild(uiRootGo.transform, "SettingsRoot").gameObject;
GameObject mapRootGo = GetOrCreateChild(uiRootGo.transform, "MapRoot").gameObject;
GameObject shopRootGo = GetOrCreateChild(uiRootGo.transform, "ShopRoot").gameObject;
@@ -138,6 +151,8 @@ namespace BaseGames.Editor
GameObject respawnButtonGo = GetOrCreateChild(deathRootGo.transform, "RespawnButton").gameObject;
GetOrAddComponent<Image>(respawnButtonGo);
Button respawnButton = GetOrAddComponent<Button>(respawnButtonGo);
GameObject deathMessageGo = GetOrCreateChild(deathRootGo.transform, "DeathMessage").gameObject;
TextMeshProUGUI deathMessage = GetOrAddComponent<TextMeshProUGUI>(deathMessageGo);
// ── BootSequencer启动流程──────────────────────────────────────
GameObject bootSequencerGo = GetOrCreateChild(services, "BootSequencer").gameObject;
@@ -157,12 +172,30 @@ namespace BaseGames.Editor
// ── Canvas_Splash启动演出──────────────────────────────────────
GameObject splashCanvasGo = GetOrCreateCanvas(ui.transform, "Canvas_Splash", 100);
SplashScreenController splashCtrl = GetOrAddComponent<SplashScreenController>(splashCanvasGo);
CanvasGroup splashRootGroup = GetOrAddComponent<CanvasGroup>(splashCanvasGo);
AssignReference(splashCtrl, "_splashRoot", splashRootGroup);
GameObject studioLogoGo = GetOrCreateChild(splashCanvasGo.transform, "StudioLogo").gameObject;
CanvasGroup studioLogoGroup = GetOrAddComponent<CanvasGroup>(studioLogoGo);
AssignReference(splashCtrl, "_studioLogoGroup", studioLogoGroup);
GameObject gameTitleGo = GetOrCreateChild(splashCanvasGo.transform, "GameTitle").gameObject;
CanvasGroup gameTitleGroup = GetOrAddComponent<CanvasGroup>(gameTitleGo);
AssignReference(splashCtrl, "_gameTitleGroup", gameTitleGroup);
AssignAsset(splashCtrl, "_onSplashStartRequest", report, false, "EVT_SplashStartRequest");
AssignAsset(splashCtrl, "_onSplashComplete", report, false, "EVT_SplashComplete");
// ── LoadingScreenManager加载遮罩──────────────────────────────
GameObject loadingCanvasGo = GetOrCreateCanvas(ui.transform, "Canvas_Loading", 99);
LoadingScreenManager loadingMgr = GetOrAddComponent<LoadingScreenManager>(loadingCanvasGo);
GameObject loadingRootGo = GetOrCreateChild(loadingCanvasGo.transform, "LoadingRoot").gameObject;
AssignReference(loadingMgr, "_loadingRoot", loadingRootGo);
GameObject progressFillGo = GetOrCreateChild(loadingRootGo.transform, "ProgressBarFill").gameObject;
Image progressFillImg = GetOrAddComponent<Image>(progressFillGo);
progressFillImg.type = Image.Type.Filled;
progressFillImg.fillMethod = Image.FillMethod.Horizontal;
AssignReference(loadingMgr, "_progressFill", progressFillImg);
GameObject tipTextGo = GetOrCreateChild(loadingRootGo.transform, "TipText").gameObject;
TextMeshProUGUI tipText = GetOrAddComponent<TextMeshProUGUI>(tipTextGo);
AssignReference(loadingMgr, "_tipText", tipText);
AssignAsset(loadingMgr, "_onLoadingStarted", report, false, "EVT_LoadingStarted");
AssignAsset(loadingMgr, "_onLoadingComplete", report, false, "EVT_LoadingComplete");
AssignAsset(loadingMgr, "_onLoadingProgressUpdated", report, false, "EVT_LoadingProgressUpdated");
@@ -173,13 +206,20 @@ namespace BaseGames.Editor
// 实际 UI 效果完全由 SceneFeedback 内部的 MMF_Player 负责。
GameObject fadeCtrGo = GetOrCreateChild(ui.transform, "SYS_SceneFade").gameObject;
SceneFadeController fadeCtr = GetOrAddComponent<SceneFadeController>(fadeCtrGo);
GameObject fadeOutGo = GetOrCreateChild(fadeCtrGo.transform, "FeedbackFadeOut").gameObject;
MMF_Player fadeOutPlayer = GetOrAddComponent<MMF_Player>(fadeOutGo);
SceneFeedback fadeOutFeedback = GetOrAddComponent<SceneFeedback>(fadeOutGo);
AssignReference(fadeOutFeedback, "_player", fadeOutPlayer);
AssignReference(fadeCtr, "_fadeOut", fadeOutFeedback);
GameObject fadeInGo = GetOrCreateChild(fadeCtrGo.transform, "FeedbackFadeIn").gameObject;
MMF_Player fadeInPlayer = GetOrAddComponent<MMF_Player>(fadeInGo);
SceneFeedback fadeInFeedback = GetOrAddComponent<SceneFeedback>(fadeInGo);
AssignReference(fadeInFeedback, "_player", fadeInPlayer);
AssignReference(fadeCtr, "_fadeIn", fadeInFeedback);
AssignAsset(fadeCtr, "_onFadeOutRequest", report, false, "EVT_FadeOutRequest");
AssignAsset(fadeCtr, "_onFadeInRequest", report, false, "EVT_FadeInRequest");
report.Add("Canvas_Splash请将工作室 Logo CanvasGroup 赋给 _studioLogoGroup游戏标题 CanvasGroup 赋给 _gameTitleGroup。");
report.Add("Canvas_Loading请为 LoadingScreenManager 绑定 _progressBarSlider和 _loadingPanelGameObject。");
report.Add("SYS_SceneFade请创建两个带 MMF_Player 的 SceneFeedback淡出/淡入)," +
"配置完毕后分别拖入 SceneFadeController._fadeOut / _fadeIn。" +
report.Add("SYS_SceneFadeSceneFeedback 子节点已创建并绑定。请在 FeedbackFadeOut / FeedbackFadeIn 的 MMF_Player 中配置所需效果(如全屏黑幕淡入淡出)。" +
"MMF_Player 总时长应 ≤ SceneService._sceneFadeDuration默认 0.4 s。");
EnsureAudioSources(audioManagerGo, audioManager, report);
@@ -204,6 +244,8 @@ namespace BaseGames.Editor
AssignAsset(sceneService, "_onSceneLoadRequest", report, false, "EVT_SceneLoadRequest");
AssignAsset(sceneService, "_onFadeInRequest", report, false, "EVT_FadeInRequest");
AssignAsset(sceneService, "_onFadeOutRequest", report, false, "EVT_FadeOutRequest");
// 场景加载完毕、世界状态恢复后触发;场景物体据此应用存档状态,淡入前保证画面正确
AssignAsset(sceneService, "_onSceneWorldStateRestored", report, true, "EVT_SceneWorldStateRestored");
AssignReference(sceneService, "_sceneLoader", sceneLoader);
AssignAsset(sceneLoader, "_onSceneLoaded", report, false, "EVT_SceneLoaded");
@@ -226,21 +268,46 @@ namespace BaseGames.Editor
AssignAsset(cameraStateController, "_lensConfig", report, false, "CAM_LensConfig", "LensConfig", "CameraLensConfig");
AssignReference(uiManager, "_hudRoot", hudRootGo);
AssignReference(uiManager, "_pauseMenuRoot", pauseRootGo);
AssignReference(uiManager, "_deathScreenRoot", deathRootGo);
AssignReference(uiManager, "_settingsRoot", settingsRootGo);
AssignReference(uiManager, "_mapRoot", mapRootGo);
AssignReference(uiManager, "_shopRoot", shopRootGo);
AssignAsset(uiManager, "_onGameStateChanged", report, true, "EVT_GameStateChanged", "EVT_GameState");
AssignAsset(uiManager, "_onPauseRequested", report, false, "EVT_PauseRequested");
AssignAsset(uiManager, "_onFastTravelOpen", report, false, "EVT_FastTravelOpen");
AssignAsset(uiManager, "_onShopOpen", report, false, "EVT_ShopOpen");
AssignAsset(uiManager, "_onMapOpen", report, false, "EVT_MapOpen");
AssignReference(uiManager, "_addressablePanelParent", uiRootGo.transform);
{
// UIManager uses _panels (PanelRegistration[]) — NOT individual _pauseMenuRoot/_settingsRoot etc.
var so = new SerializedObject(uiManager);
var panelsProp = so.FindProperty("_panels");
panelsProp.arraySize = 4;
var p0 = panelsProp.GetArrayElementAtIndex(0);
p0.FindPropertyRelative("id").intValue = (int)PanelId.Pause;
p0.FindPropertyRelative("root").objectReferenceValue = pauseRootGo;
var p1 = panelsProp.GetArrayElementAtIndex(1);
p1.FindPropertyRelative("id").intValue = (int)PanelId.Settings;
p1.FindPropertyRelative("root").objectReferenceValue = settingsRootGo;
var p2 = panelsProp.GetArrayElementAtIndex(2);
p2.FindPropertyRelative("id").intValue = (int)PanelId.Map;
p2.FindPropertyRelative("root").objectReferenceValue = mapRootGo;
var p3 = panelsProp.GetArrayElementAtIndex(3);
p3.FindPropertyRelative("id").intValue = (int)PanelId.Shop;
p3.FindPropertyRelative("root").objectReferenceValue = shopRootGo;
so.ApplyModifiedPropertiesWithoutUndo();
}
AssignAsset(uiManager, "_onGameStateChanged", report, true, "EVT_GameStateChanged", "EVT_GameState");
AssignAsset(uiManager, "_onPauseRequested", report, false, "EVT_PauseRequested");
AssignAsset(uiManager, "_onFastTravelOpen", report, false, "EVT_FastTravelOpen");
AssignAsset(uiManager, "_onShopOpen", report, false, "EVT_ShopOpen");
AssignAsset(uiManager, "_onMapOpen", report, false, "EVT_MapOpen");
AssignAsset(uiManager, "_onCharmPanelOpen", report, false, "EVT_CharmPanelOpen");
AssignAsset(uiManager, "_onSpellSelectOpen", report, false, "EVT_SpellSelectOpen");
AssignReference(deathScreenController, "_btnRespawn", respawnButton);
AssignAsset(deathScreenController, "_onPlayerDied", report, true, "EVT_PlayerDied");
AssignReference(deathScreenController, "_deathMessage", deathMessage);
AssignAsset(deathScreenController, "_onDeathScreenConfirmed", report, true, "EVT_DeathScreenConfirmed");
AssignReference(pauseMenuCtrl, "_btnResume", pauseBtnResume);
AssignReference(pauseMenuCtrl, "_btnSettings", pauseBtnSettings);
AssignReference(pauseMenuCtrl, "_btnMainMenu", pauseBtnMainMenu);
AssignReference(pauseMenuCtrl, "_btnQuit", pauseBtnQuit);
AssignAsset(pauseMenuCtrl, "_onResumeRequested", report, false, "EVT_ResumeRequested");
AssignAsset(pauseMenuCtrl, "_onSceneLoadRequest", report, false, "EVT_SceneLoadRequest");
AddScaffoldNote(hudRootGo, "HUDController 已挂载。其内部图片/文本/图标 Prefab 依赖较多,需后续手工补 UI 资源与事件频道。", report);
// ── 流式加载系统 ──────────────────────────────────────────────────
@@ -271,54 +338,315 @@ namespace BaseGames.Editor
// ── Canvas_MainMenu排序层 10显示在 HUD 之上)────────────────
GameObject canvasGo = GetOrCreateCanvas(root.transform, "Canvas_MainMenu", 10);
// ── 全屏暗色背景(幽暗基调)────────────────────────────────
GetOrCreateImage(canvasGo.transform, "Background", new Color(0.05f, 0.06f, 0.09f, 1f), false)
.transform.SetAsFirstSibling();
// ── 标题 ──────────────────────────────────────────────────────────
var titleRt = GetOrCreateUIChild(canvasGo.transform, "TitleText");
SetRect(titleRt, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0.5f, 1f),
new Vector2(0f, -150f), new Vector2(1400f, 180f));
var titleTmp = GetOrAddComponent<TextMeshProUGUI>(titleRt.gameObject);
titleTmp.text = "ZELING"; titleTmp.fontSize = 130f; titleTmp.fontStyle = FontStyles.Bold;
titleTmp.alignment = TextAlignmentOptions.Center; titleTmp.color = GoldText; titleTmp.raycastTarget = false;
titleTmp.characterSpacing = 14f;
var subtitleRt = GetOrCreateUIChild(canvasGo.transform, "SubtitleText");
SetRect(subtitleRt, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0.5f, 1f),
new Vector2(0f, -300f), new Vector2(1000f, 60f));
var subTmp = GetOrAddComponent<TextMeshProUGUI>(subtitleRt.gameObject);
subTmp.text = "A 2D Action Adventure"; subTmp.fontSize = 40f; subTmp.alignment = TextAlignmentOptions.Center;
subTmp.color = new Color(0.7f, 0.66f, 0.55f, 0.9f); subTmp.raycastTarget = false; subTmp.characterSpacing = 8f;
// ── 主菜单控制器 ──────────────────────────────────────────────────
MainMenuController menuCtrl = GetOrAddComponent<MainMenuController>(canvasGo);
AssignAsset(menuCtrl, "_onGameStateChanged", report, false, "EVT_GameStateChanged", "EVT_GameState");
AssignAsset(menuCtrl, "_onSceneLoadRequest", report, false, "EVT_SceneLoadRequest");
AssignAsset(menuCtrl, "_onSlotConfirmed", report, false, "EVT_SlotConfirmed");
// ── 主按钮区域 ────────────────────────────────────────────────────
GameObject menuPanelGo = GetOrCreateChild(canvasGo.transform, "MenuPanel").gameObject;
GetOrAddComponent<VerticalLayoutGroup>(menuPanelGo);
// ── 主按钮区域(底部居中竖排,带 CanvasGroup 供入场动画)─────────────
var menuPanelRt = GetOrCreateUIChild(canvasGo.transform, "MenuPanel");
SetRect(menuPanelRt, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0.5f, 0f),
new Vector2(0f, 170f), new Vector2(560f, 470f));
GameObject menuPanelGo = menuPanelRt.gameObject;
var menuGroup = GetOrAddComponent<CanvasGroup>(menuPanelGo);
var menuVlg = GetOrAddComponent<VerticalLayoutGroup>(menuPanelGo);
menuVlg.spacing = 12f; menuVlg.childAlignment = TextAnchor.MiddleCenter;
menuVlg.childControlWidth = true; menuVlg.childControlHeight = true;
menuVlg.childForceExpandWidth = true; menuVlg.childForceExpandHeight = false;
GameObject btnNewGameGo = GetOrCreateButtonChild(menuPanelGo.transform, "Btn_NewGame", "新游戏");
GameObject btnContinueGo = GetOrCreateButtonChild(menuPanelGo.transform, "Btn_Continue", "继续");
GameObject btnSettingsGo = GetOrCreateButtonChild(menuPanelGo.transform, "Btn_Settings", "设置");
GameObject btnCreditsGo = GetOrCreateButtonChild(menuPanelGo.transform, "Btn_Credits", "制作团队");
GameObject btnQuitGo = GetOrCreateButtonChild(menuPanelGo.transform, "Btn_Quit", "退出");
GameObject btnNewGameGo = GetOrCreateButtonChild(menuPanelGo.transform, "Btn_NewGame", "New Game");
GameObject btnContinueGo = GetOrCreateButtonChild(menuPanelGo.transform, "Btn_Continue", "Continue");
GameObject btnSettingsGo = GetOrCreateButtonChild(menuPanelGo.transform, "Btn_Settings", "Settings");
GameObject btnCreditsGo = GetOrCreateButtonChild(menuPanelGo.transform, "Btn_Credits", "Credits");
GameObject btnQuitGo = GetOrCreateButtonChild(menuPanelGo.transform, "Btn_Quit", "Quit");
foreach (var b in new[] { btnNewGameGo, btnContinueGo, btnSettingsGo, btnCreditsGo, btnQuitGo })
{
StyleAsTextButton(b);
var le = GetOrAddComponent<LayoutElement>(b);
le.preferredHeight = 64f; le.minHeight = 56f;
}
AssignReference(menuCtrl, "_btnNewGame", btnNewGameGo.GetComponent<Button>());
AssignReference(menuCtrl, "_btnContinue", btnContinueGo.GetComponent<Button>());
AssignReference(menuCtrl, "_btnSettings", btnSettingsGo.GetComponent<Button>());
AssignReference(menuCtrl, "_btnCredits", btnCreditsGo.GetComponent<Button>());
AssignReference(menuCtrl, "_btnQuit", btnQuitGo.GetComponent<Button>());
AssignReference(menuCtrl, "_menuPanel", menuPanelGo);
AssignReference(menuCtrl, "_mainButtonsGroup", menuGroup);
AssignReference(menuCtrl, "_mainButtonsRect", menuPanelRt);
// ── SaveSlotPanel ─────────────────────────────────────────────────
GameObject saveSlotPanelGo = GetOrCreateChild(canvasGo.transform, "SaveSlotPanel").gameObject;
// ── SaveSlotPanel(全屏模态:半透明遮罩 + 竖排 3 卡片)──────────────
var saveSlotPanelRt = GetOrCreateUIChild(canvasGo.transform, "SaveSlotPanel");
StretchFull(saveSlotPanelRt);
GameObject saveSlotPanelGo = saveSlotPanelRt.gameObject;
saveSlotPanelGo.SetActive(false);
SaveSlotController saveSlotCtrl = GetOrAddComponent<SaveSlotController>(saveSlotPanelGo);
AssignAsset(saveSlotCtrl, "_onSlotConfirmed", report, false, "EVT_SlotConfirmed");
AssignReference(menuCtrl, "_saveSlotPanel", saveSlotPanelGo);
AssignReference(menuCtrl, "_saveSlotController", saveSlotCtrl);
// 近乎不透明的遮罩(拦截背后点击,并遮住主菜单避免文字透出)
GetOrCreateImage(saveSlotPanelGo.transform, "Overlay", new Color(0.04f, 0.05f, 0.08f, 0.97f), true)
.transform.SetAsFirstSibling();
// 面板标题
var slotTitleRt = GetOrCreateUIChild(saveSlotPanelGo.transform, "PanelTitle");
SetRect(slotTitleRt, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0.5f, 1f),
new Vector2(0f, -70f), new Vector2(900f, 80f));
var slotTitleTmp = GetOrAddComponent<TextMeshProUGUI>(slotTitleRt.gameObject);
slotTitleTmp.text = "Select Save"; slotTitleTmp.fontSize = 56f; slotTitleTmp.fontStyle = FontStyles.Bold;
slotTitleTmp.alignment = TextAlignmentOptions.Center; slotTitleTmp.color = GoldText; slotTitleTmp.raycastTarget = false;
// 卡片容器(居中竖排)
var slotsContainerRt = GetOrCreateUIChild(saveSlotPanelGo.transform, "SlotsContainer");
SetRect(slotsContainerRt, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f),
new Vector2(0f, -10f), new Vector2(960f, 660f));
var slotsVlg = GetOrAddComponent<VerticalLayoutGroup>(slotsContainerRt.gameObject);
slotsVlg.spacing = 22f; slotsVlg.childAlignment = TextAnchor.MiddleCenter;
slotsVlg.childControlWidth = true; slotsVlg.childControlHeight = true;
slotsVlg.childForceExpandWidth = true; slotsVlg.childForceExpandHeight = false;
// ── 存档槽卡片 Slot_0/1/2挂 SaveSlotUI绑定到 _slotUIs─────────────
var regionRegistry = FindFirstAssetByType<BaseGames.World.Map.RegionRegistrySO>("RegionRegistry");
var slotUIs = new SaveSlotUI[3];
for (int i = 0; i < 3; i++)
slotUIs[i] = BuildSaveSlotCard(slotsContainerRt, i, regionRegistry);
// _slotUIs 数组与默认聚焦按钮
var saveSlotSO = new UnityEditor.SerializedObject(saveSlotCtrl);
var slotUIsProp = saveSlotSO.FindProperty("_slotUIs");
slotUIsProp.arraySize = 3;
for (int i = 0; i < 3; i++)
slotUIsProp.GetArrayElementAtIndex(i).objectReferenceValue = slotUIs[i];
saveSlotSO.ApplyModifiedProperties();
AssignReference(saveSlotCtrl, "_defaultFocusButton",
slotUIs[0].transform.Find("SelectButton")?.GetComponent<Button>());
if (regionRegistry == null)
report.Add("未找到 RegionRegistry 资产SaveSlotUI._regionRegistry 未绑定(存档槽背景图失效)。先运行 BaseGames/Setup/Create Project Assets。");
// 返回按钮(关闭存档槽面板 → 绑定 MainMenuController._btnCloseSaveSlot
GameObject slotBackGo = GetOrCreateButtonChild(saveSlotPanelGo.transform, "BackButton", "Back");
var slotBackRt = (RectTransform)slotBackGo.transform;
SetRect(slotBackRt, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0.5f, 0f),
new Vector2(0f, 70f), new Vector2(260f, 64f));
StyleAsTextButton(slotBackGo, 30f);
AssignReference(menuCtrl, "_btnCloseSaveSlot", slotBackGo.GetComponent<Button>());
// ── ConfirmDialog覆盖 / 删除确认)─────────────────────
ConfirmDialogController confirmCtrl = BuildConfirmDialog(saveSlotPanelGo.transform);
AssignReference(saveSlotCtrl, "_confirmDialog", confirmCtrl);
// ── NewGameMode新游戏模式选择普通 / 钢铁之魂)────────────────────
NewGameModeController modeCtrl = BuildNewGameMode(saveSlotPanelGo.transform);
AssignReference(saveSlotCtrl, "_modeSelect", modeCtrl);
// ── SettingsPanel ─────────────────────────────────────────────────
GameObject settingsPanelGo = GetOrCreateChild(canvasGo.transform, "SettingsPanel").gameObject;
settingsPanelGo.SetActive(false);
AssignReference(menuCtrl, "_settingsPanel", settingsPanelGo);
var settingsPanelRt = GetOrCreateUIChild(canvasGo.transform, "SettingsPanel");
StretchFull(settingsPanelRt);
settingsPanelRt.gameObject.SetActive(false);
AssignReference(menuCtrl, "_settingsPanel", settingsPanelRt.gameObject);
// ── CreditsPanel ──────────────────────────────────────────────────
GameObject creditsPanelGo = GetOrCreateChild(canvasGo.transform, "CreditsPanel").gameObject;
creditsPanelGo.SetActive(false);
AssignReference(menuCtrl, "_creditsPanel", creditsPanelGo);
var creditsPanelRt = GetOrCreateUIChild(canvasGo.transform, "CreditsPanel");
StretchFull(creditsPanelRt);
creditsPanelRt.gameObject.SetActive(false);
AssignReference(menuCtrl, "_creditsPanel", creditsPanelRt.gameObject);
report.Add("设置 MainMenuController._firstGameSceneKey 为第一个游戏场景的 Addressable Key字符串。");
report.Add("SaveSlotPanel 需要补充 3 个存档槽 Button 引用_slot0Btn / _slot1Btn / _slot2Btn。");
report.Add("建议为 MenuPanel 添加 RectTransform 入场动画所需的锚点配置,参考 MainMenuController._menuPanel 的偏移量。");
report.Add("存档槽卡片已含完整布局与文本(区域 / 时长 / 时间 / 灵珠 / 生命 / 钢魂徽章),空槽显示\"开始新游戏\"提示。");
report.Add("ConfirmDialog / NewGameMode 已作为 SaveSlotPanel 子节点生成并接线;需补本地化键:"
+ "CONFIRM_OVERWRITE_TITLE / CONFIRM_OVERWRITE_BODY / CONFIRM_DELETE_TITLE / CONFIRM_DELETE_BODY / MODE_STEELSOUL_DESCUI 表)。");
MarkDirtyAndLog("Main Menu 场景脚手架", root, report);
}
// ─────────────────────────────────────────────────────────────────────
// Main Menu — 子结构构建器
// ─────────────────────────────────────────────────────────────────────
/// <summary>
/// 构建单张存档槽卡片(含背景框 / 全覆盖选择按钮 / 空槽提示 / 有档信息区 / 删除按钮),
/// 并完成 SaveSlotUI 字段绑定。卡片由父容器的 VerticalLayoutGroup 排版,高度经 LayoutElement 固定。
/// </summary>
private static SaveSlotUI BuildSaveSlotCard(Transform parent, int index, Object regionRegistry)
{
var cardRt = GetOrCreateUIChild(parent, $"Slot_{index}");
GameObject slotGo = cardRt.gameObject;
var cardLe = GetOrAddComponent<LayoutElement>(slotGo);
cardLe.preferredHeight = 180f; cardLe.minHeight = 160f;
SaveSlotUI slotUI = GetOrAddComponent<SaveSlotUI>(slotGo);
// 卡片框底(半透明深色,作为按钮 targetGraphic 的视觉基底)
var frameImg = GetOrCreateImage(slotGo.transform, "Frame", new Color(0.12f, 0.13f, 0.18f, 0.92f), false);
frameImg.transform.SetAsFirstSibling();
// 区域背景图(默认隐藏,由 SaveSlotUI.RefreshBackground 控制)
var bgImg = GetOrCreateImage(slotGo.transform, "Background", Color.white, false);
bgImg.type = Image.Type.Simple; bgImg.preserveAspect = true; bgImg.enabled = false;
bgImg.transform.SetSiblingIndex(1);
// 全覆盖选择按钮(透明,金色高亮;位于信息层之下,靠 raycast 接收点击)
GameObject selectGo = GetOrCreateButtonChild(slotGo.transform, "SelectButton", "");
StretchFull((RectTransform)selectGo.transform);
var selImg = selectGo.GetComponent<Image>();
if (selImg != null) selImg.color = new Color(1f, 1f, 1f, 0f);
var selLabel = GetButtonLabel(selectGo);
if (selLabel != null) selLabel.gameObject.SetActive(false);
// 空槽提示
var emptyRt = GetOrCreateUIChild(slotGo.transform, "EmptyIndicator");
StretchFull(emptyRt);
GameObject emptyGo = emptyRt.gameObject;
GetOrCreateText(emptyGo.transform, "EmptyText", "Empty Slot · New Game", 34f,
new Color(0.7f, 0.66f, 0.55f, 0.85f), TextAlignmentOptions.Center);
// 有档信息区(左侧竖排:区域 / 时长 / 时间)+ 右侧(灵珠 / 生命 / 钢魂)
var dataRt = GetOrCreateUIChild(slotGo.transform, "DataIndicator");
StretchFull(dataRt, 28f);
GameObject dataGo = dataRt.gameObject;
var regionText = GetOrCreateText(dataGo.transform, "RegionText", "Region", 38f, GoldText, TextAlignmentOptions.TopLeft);
SetRect((RectTransform)regionText.transform, new Vector2(0f, 1f), new Vector2(0.7f, 1f), new Vector2(0f, 1f), new Vector2(0f, -4f), new Vector2(0f, 48f));
var playtimeText = GetOrCreateText(dataGo.transform, "PlaytimeText", "00:00:00", 26f, new Color(0.8f,0.78f,0.7f,1f), TextAlignmentOptions.TopLeft);
SetRect((RectTransform)playtimeText.transform, new Vector2(0f, 1f), new Vector2(0.7f, 1f), new Vector2(0f, 1f), new Vector2(0f, -58f), new Vector2(0f, 34f));
var lastSavedText= GetOrCreateText(dataGo.transform, "LastSavedText", "—", 22f, new Color(0.6f,0.58f,0.52f,1f), TextAlignmentOptions.TopLeft);
SetRect((RectTransform)lastSavedText.transform, new Vector2(0f, 1f), new Vector2(0.7f, 1f), new Vector2(0f, 1f), new Vector2(0f, -98f), new Vector2(0f, 30f));
var lingZhuText = GetOrCreateText(dataGo.transform, "LingZhuText", "0", 28f, new Color(0.85f,0.8f,0.55f,1f), TextAlignmentOptions.TopRight);
SetRect((RectTransform)lingZhuText.transform, new Vector2(0.7f, 1f), new Vector2(1f, 1f), new Vector2(1f, 1f), new Vector2(0f, -4f), new Vector2(0f, 40f));
var hpText = GetOrCreateText(dataGo.transform, "HPText", "0", 28f, new Color(0.85f,0.5f,0.5f,1f), TextAlignmentOptions.TopRight);
SetRect((RectTransform)hpText.transform, new Vector2(0.7f, 1f), new Vector2(1f, 1f), new Vector2(1f, 1f), new Vector2(0f, -48f), new Vector2(0f, 40f));
var badgeRt = GetOrCreateUIChild(dataGo.transform, "SteelSoulBadge");
SetRect(badgeRt, new Vector2(1f, 0f), new Vector2(1f, 0f), new Vector2(1f, 0f), new Vector2(0f, 4f), new Vector2(120f, 40f));
GetOrAddComponent<Image>(badgeRt.gameObject).color = new Color(0.5f, 0.55f, 0.6f, 0.5f);
GetOrCreateText(badgeRt.transform, "BadgeText", "STEEL", 22f, new Color(0.85f,0.9f,1f,1f), TextAlignmentOptions.Center);
GameObject badgeGo = badgeRt.gameObject;
// 删除按钮(右上角小 ×
GameObject deleteGo = GetOrCreateButtonChild(slotGo.transform, "DeleteButton", "×");
SetRect((RectTransform)deleteGo.transform, new Vector2(1f, 1f), new Vector2(1f, 1f), new Vector2(1f, 1f), new Vector2(-10f, -10f), new Vector2(48f, 48f));
var delImg = deleteGo.GetComponent<Image>();
if (delImg != null) delImg.color = new Color(0.4f, 0.12f, 0.12f, 0.7f);
var delLabel = GetButtonLabel(deleteGo);
if (delLabel != null) { delLabel.fontSize = 32f; delLabel.color = new Color(1f, 0.8f, 0.8f, 1f); }
// 绑定 SaveSlotUI 字段
AssignReference(slotUI, "_emptyIndicator", emptyGo);
AssignReference(slotUI, "_dataIndicator", dataGo);
AssignReference(slotUI, "_selectButton", selectGo.GetComponent<Button>());
AssignReference(slotUI, "_deleteButton", deleteGo.GetComponent<Button>());
AssignReference(slotUI, "_backgroundImage", bgImg);
AssignReference(slotUI, "_regionText", regionText);
AssignReference(slotUI, "_playtimeText", playtimeText);
AssignReference(slotUI, "_lastSavedText", lastSavedText);
AssignReference(slotUI, "_lingZhuText", lingZhuText);
AssignReference(slotUI, "_hpText", hpText);
AssignReference(slotUI, "_steelSoulBadge", badgeGo);
if (regionRegistry != null)
AssignReference(slotUI, "_regionRegistry", regionRegistry);
// 初始隐藏数据层(运行时由 Refresh 控制;编辑器下让空槽提示可见)
emptyGo.SetActive(true);
dataGo.SetActive(false);
return slotUI;
}
/// <summary>构建通用确认对话框(居中模态:遮罩 + 对话框 + 标题 / 正文 / 确认 / 取消),返回控制器。</summary>
private static ConfirmDialogController BuildConfirmDialog(Transform parent)
{
var rootRt = GetOrCreateUIChild(parent, "ConfirmDialog");
StretchFull(rootRt);
GameObject confirmGo = rootRt.gameObject;
confirmGo.SetActive(false);
ConfirmDialogController confirmCtrl = GetOrAddComponent<ConfirmDialogController>(confirmGo);
GetOrCreateImage(confirmGo.transform, "Overlay", new Color(0f, 0f, 0f, 0.6f), true).transform.SetAsFirstSibling();
var boxRt = GetOrCreateUIChild(confirmGo.transform, "DialogBox");
SetRect(boxRt, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(720f, 380f));
GetOrAddComponent<Image>(boxRt.gameObject).color = new Color(0.10f, 0.11f, 0.15f, 0.98f);
var titleTmp = GetOrCreateText(boxRt.transform, "TitleText", "Confirm", 40f, GoldText, TextAlignmentOptions.Center);
SetRect((RectTransform)titleTmp.transform, new Vector2(0f,1f), new Vector2(1f,1f), new Vector2(0.5f,1f), new Vector2(0f,-50f), new Vector2(-60f,60f));
var bodyTmp = GetOrCreateText(boxRt.transform, "BodyText", "Are you sure?", 28f, new Color(0.82f,0.8f,0.74f,1f), TextAlignmentOptions.Center);
SetRect((RectTransform)bodyTmp.transform, new Vector2(0f,0.5f), new Vector2(1f,0.5f), new Vector2(0.5f,0.5f), new Vector2(0f,10f), new Vector2(-80f,120f));
GameObject yesGo = GetOrCreateButtonChild(boxRt.transform, "Btn_Confirm", "Confirm");
SetRect((RectTransform)yesGo.transform, new Vector2(0.5f,0f), new Vector2(0.5f,0f), new Vector2(0.5f,0f), new Vector2(-150f,40f), new Vector2(220f,64f));
yesGo.GetComponent<Image>().color = new Color(0.45f, 0.12f, 0.12f, 0.85f);
GameObject noGo = GetOrCreateButtonChild(boxRt.transform, "Btn_Cancel", "Cancel");
SetRect((RectTransform)noGo.transform, new Vector2(0.5f,0f), new Vector2(0.5f,0f), new Vector2(0.5f,0f), new Vector2(150f,40f), new Vector2(220f,64f));
AssignReference(confirmCtrl, "_root", confirmGo);
AssignReference(confirmCtrl, "_titleText", titleTmp);
AssignReference(confirmCtrl, "_bodyText", bodyTmp);
AssignReference(confirmCtrl, "_confirmLabel", GetButtonLabel(yesGo));
AssignReference(confirmCtrl, "_cancelLabel", GetButtonLabel(noGo));
AssignReference(confirmCtrl, "_btnConfirm", yesGo.GetComponent<Button>());
AssignReference(confirmCtrl, "_btnCancel", noGo.GetComponent<Button>());
return confirmCtrl;
}
/// <summary>构建新游戏模式选择面板(居中模态:普通 / 钢铁之魂 / 返回 + 钢魂说明),返回控制器。</summary>
private static NewGameModeController BuildNewGameMode(Transform parent)
{
var rootRt = GetOrCreateUIChild(parent, "NewGameMode");
StretchFull(rootRt);
GameObject modeGo = rootRt.gameObject;
modeGo.SetActive(false);
NewGameModeController modeCtrl = GetOrAddComponent<NewGameModeController>(modeGo);
GetOrCreateImage(modeGo.transform, "Overlay", new Color(0f, 0f, 0f, 0.6f), true).transform.SetAsFirstSibling();
var boxRt = GetOrCreateUIChild(modeGo.transform, "DialogBox");
SetRect(boxRt, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(760f, 460f));
GetOrAddComponent<Image>(boxRt.gameObject).color = new Color(0.10f, 0.11f, 0.15f, 0.98f);
var modeTitle = GetOrCreateText(boxRt.transform, "TitleText", "Select Mode", 40f, GoldText, TextAlignmentOptions.Center);
SetRect((RectTransform)modeTitle.transform, new Vector2(0f,1f), new Vector2(1f,1f), new Vector2(0.5f,1f), new Vector2(0f,-46f), new Vector2(-60f,56f));
GameObject normalGo = GetOrCreateButtonChild(boxRt.transform, "Btn_Normal", "Normal");
SetRect((RectTransform)normalGo.transform, new Vector2(0.5f,1f), new Vector2(0.5f,1f), new Vector2(0.5f,1f), new Vector2(0f,-130f), new Vector2(420f,66f));
GameObject steelGo = GetOrCreateButtonChild(boxRt.transform, "Btn_SteelSoul", "Steel Soul");
SetRect((RectTransform)steelGo.transform, new Vector2(0.5f,1f), new Vector2(0.5f,1f), new Vector2(0.5f,1f), new Vector2(0f,-206f), new Vector2(420f,66f));
steelGo.GetComponent<Image>().color = new Color(0.30f, 0.33f, 0.40f, 0.85f);
var steelDesc = GetOrCreateText(boxRt.transform, "SteelSoulDesc", "Steel Soul: one life. Death wipes the save.", 22f, new Color(0.8f,0.55f,0.55f,1f), TextAlignmentOptions.Center);
SetRect((RectTransform)steelDesc.transform, new Vector2(0f,0f), new Vector2(1f,0f), new Vector2(0.5f,0f), new Vector2(0f,130f), new Vector2(-80f,60f));
GameObject backGo = GetOrCreateButtonChild(boxRt.transform, "Btn_Back", "Back");
SetRect((RectTransform)backGo.transform, new Vector2(0.5f,0f), new Vector2(0.5f,0f), new Vector2(0.5f,0f), new Vector2(0f,46f), new Vector2(260f,60f));
StyleAsTextButton(backGo, 28f);
AssignReference(modeCtrl, "_root", modeGo);
AssignReference(modeCtrl, "_btnNormal", normalGo.GetComponent<Button>());
AssignReference(modeCtrl, "_btnSteelSoul", steelGo.GetComponent<Button>());
AssignReference(modeCtrl, "_btnBack", backGo.GetComponent<Button>());
AssignReference(modeCtrl, "_steelSoulDescText", steelDesc);
return modeCtrl;
}
// ─────────────────────────────────────────────────────────────────────
// Scaffold Game Room
// ─────────────────────────────────────────────────────────────────────
@@ -578,7 +906,10 @@ namespace BaseGames.Editor
AssignReference(audioManager, "_bgmSourceA", bgmA);
AssignReference(audioManager, "_bgmSourceB", bgmB);
AssignArrayReferences(audioManager, "_sfxSources", sfxSources, report);
report.Add("AudioManager 已生成 2 个 BGM Source 和 6 个 SFX SourceAudioMixer 仍需手工指定。");
// 尝试自动绑定 AudioMixer 与 AudioConfig缺失时报告需音频资产补齐
AssignReference(audioManager, "_mixer", FindFirstAssetWithExtension(".mixer", "MainAudioMixer", "GameAudioMixer", "AudioMixer"), report);
AssignAsset(audioManager, "_audioConfig", report, false, "AUD_AudioConfig", "AudioConfig");
report.Add("AudioManager 已生成 2 个 BGM Source 和 6 个 SFX Source_mixer/_audioConfig 若缺失需补齐音频资产。");
}
private static GameObject GetOrCreateRoot(string name)
@@ -622,21 +953,140 @@ namespace BaseGames.Editor
Canvas canvas = GetOrAddComponent<Canvas>(canvasGo);
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
canvas.sortingOrder = sortOrder;
GetOrAddComponent<CanvasScaler>(canvasGo);
var scaler = GetOrAddComponent<CanvasScaler>(canvasGo);
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
scaler.referenceResolution = new Vector2(1920f, 1080f);
scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight;
scaler.matchWidthOrHeight = 0.5f;
GetOrAddComponent<GraphicRaycaster>(canvasGo);
return canvasGo;
}
/// <summary>在指定父节点下创建一个带 Button 的菜单按钮子节点(幂等)。文本由美术后续补充。</summary>
// ─────────────────────────────────────────────────────────────────────
// UI 布局辅助RectTransform 感知)
// ─────────────────────────────────────────────────────────────────────
/// <summary>
/// 创建/获取 UI 子节点并保证其 transform 为 <see cref="RectTransform"/>。
/// 旧的普通 <see cref="Transform"/> 节点无法原地转换,会被销毁并以 RectTransform 重建(含其子树),
/// 以支持脚手架对历史场景的"重建"修复。
/// </summary>
private static RectTransform GetOrCreateUIChild(Transform parent, string name)
{
Transform child = parent.Find(name);
if (child is RectTransform existing) return existing;
if (child != null) Undo.DestroyObjectImmediate(child.gameObject);
GameObject go = new GameObject(name, typeof(RectTransform));
Undo.RegisterCreatedObjectUndo(go, $"Create {name}");
go.transform.SetParent(parent, false);
return (RectTransform)go.transform;
}
/// <summary>将 RectTransform 设为四向拉伸(铺满父节点,可带统一内边距)。</summary>
private static void StretchFull(RectTransform rt, float padding = 0f)
{
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);
}
/// <summary>按锚点 + 锚定位置 + 尺寸配置 RectTransform。</summary>
private static void SetRect(RectTransform rt, Vector2 anchorMin, Vector2 anchorMax,
Vector2 pivot, Vector2 anchoredPos, Vector2 size)
{
rt.anchorMin = anchorMin;
rt.anchorMax = anchorMax;
rt.pivot = pivot;
rt.anchoredPosition = anchoredPos;
rt.sizeDelta = size;
}
/// <summary>创建/获取一个 <see cref="TextMeshProUGUI"/> 文本子节点(默认铺满父节点、不拦截射线)。</summary>
private static TextMeshProUGUI GetOrCreateText(Transform parent, string name, string text,
float fontSize, Color color, TextAlignmentOptions align = TextAlignmentOptions.Center)
{
RectTransform rt = GetOrCreateUIChild(parent, name);
StretchFull(rt);
var tmp = GetOrAddComponent<TextMeshProUGUI>(rt.gameObject);
tmp.text = text;
tmp.fontSize = fontSize;
tmp.color = color;
tmp.alignment = align;
tmp.enableWordWrapping = true;
tmp.raycastTarget = false;
return tmp;
}
/// <summary>创建/获取一个铺满父节点的 <see cref="Image"/>(纯色块,可作背景 / 遮罩)。</summary>
private static Image GetOrCreateImage(Transform parent, string name, Color color, bool raycastTarget)
{
RectTransform rt = GetOrCreateUIChild(parent, name);
StretchFull(rt);
var img = GetOrAddComponent<Image>(rt.gameObject);
img.color = color;
img.raycastTarget = raycastTarget;
return img;
}
// 金色高亮配色(文字按钮 / 卡片选择)
private static readonly Color GoldText = new Color(0.92f, 0.86f, 0.66f, 1f);
private static readonly Color GoldHighlight = new Color(1f, 0.84f, 0.40f, 0.20f);
private static readonly Color GoldPressed = new Color(1f, 0.84f, 0.40f, 0.34f);
/// <summary>
/// 在父节点下创建/获取一个带 <see cref="Image"/> + <see cref="Button"/> + TMP 文本标签的按钮幂等RectTransform 化)。
/// 默认带低调的深色底 + 金色悬停 / 选中高亮;文本标签位于子节点 "Label"。
/// </summary>
private static GameObject GetOrCreateButtonChild(Transform parent, string name, string label)
{
GameObject go = GetOrCreateChild(parent, name).gameObject;
GetOrAddComponent<Image>(go);
GetOrAddComponent<Button>(go);
_ = label; // 占位:文本内容由美术在 Prefab/Scene 中设置
RectTransform rt = GetOrCreateUIChild(parent, name);
GameObject go = rt.gameObject;
var img = GetOrAddComponent<Image>(go);
img.color = new Color(0.10f, 0.11f, 0.14f, 0.55f);
img.raycastTarget = true;
var btn = GetOrAddComponent<Button>(go);
btn.targetGraphic = img;
var colors = btn.colors;
colors.normalColor = Color.white;
colors.highlightedColor = new Color(1f, 0.95f, 0.80f, 1f);
colors.pressedColor = new Color(1f, 0.84f, 0.40f, 1f);
colors.selectedColor = new Color(1f, 0.95f, 0.80f, 1f);
colors.disabledColor = new Color(0.5f, 0.5f, 0.5f, 0.4f);
colors.fadeDuration = 0.08f;
btn.colors = colors;
// 文本标签(铺满按钮,居中)
GetOrCreateText(go.transform, "Label", label ?? string.Empty, 30f, GoldText, TextAlignmentOptions.Center);
return go;
}
/// <summary>取按钮的 TMP 文本标签GetOrCreateButtonChild 生成的 "Label" 子节点)。</summary>
private static TextMeshProUGUI GetButtonLabel(GameObject buttonGo)
{
var t = buttonGo.transform.Find("Label");
return t != null ? t.GetComponent<TextMeshProUGUI>() : null;
}
/// <summary>将按钮改造为"纯文字"风格(透明底,仅金色高亮),用于主菜单主按钮列表。</summary>
private static void StyleAsTextButton(GameObject buttonGo, float fontSize = 34f)
{
var img = buttonGo.GetComponent<Image>();
if (img != null) img.color = new Color(1f, 1f, 1f, 0f); // 透明底,仍可作 raycast target
var label = GetButtonLabel(buttonGo);
if (label != null)
{
label.fontSize = fontSize;
label.fontStyle = FontStyles.Normal;
label.color = GoldText;
}
}
private static void AssignReference(Object target, string propertyName, Object value)
{
AssignReference(target, propertyName, value, null);