地图系统
This commit is contained in:
@@ -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 绑定 _progressBar(Slider)和 _loadingPanel(GameObject)。");
|
||||
report.Add("SYS_SceneFade:请创建两个带 MMF_Player 的 SceneFeedback(淡出/淡入)," +
|
||||
"配置完毕后分别拖入 SceneFadeController._fadeOut / _fadeIn。" +
|
||||
report.Add("SYS_SceneFade:SceneFeedback 子节点已创建并绑定。请在 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_DESC(UI 表)。");
|
||||
|
||||
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 Source,AudioMixer 仍需手工指定。");
|
||||
// 尝试自动绑定 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);
|
||||
|
||||
Reference in New Issue
Block a user