@@ -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" , repo rt, 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 . FindPrope rty ( "_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 ;
G etOrAddComponent < VerticalLayoutGroup > ( menuPanelGo ) ;
// ── 主按钮区域(底部居中竖排,带 CanvasGroup 供入场动画) ─────────────
var menuPanelRt = GetOrCreateUI Child ( canvasGo . transform , "MenuPanel" ) ;
S etRect ( 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 = GetOrCreateUI Child ( 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 = GetOrCreateUI Child ( 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 = GetOrCreateUI Child ( 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 = GetOrCreateUI Child ( 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 ) ;