UI系统
This commit is contained in:
@@ -10,6 +10,7 @@ using BaseGames.UI;
|
||||
using BaseGames.UI.HUD;
|
||||
using BaseGames.UI.MainMenu;
|
||||
using BaseGames.UI.Menus;
|
||||
using BaseGames.UI.Settings;
|
||||
using BaseGames.UI.Splash;
|
||||
using BaseGames.World;
|
||||
using BaseGames.World.Map;
|
||||
@@ -106,36 +107,41 @@ namespace BaseGames.Editor
|
||||
|
||||
GameObject vcamAGo = GetOrCreateChild(camera, "VCamA").gameObject;
|
||||
CinemachineCamera vcamA = GetOrAddComponent<CinemachineCamera>(vcamAGo);
|
||||
GetOrAddComponent<CinemachineConfiner3D>(vcamAGo);
|
||||
GetOrAddComponent<CameraAxisLockExtension>(vcamAGo);
|
||||
// 扩展组件顺序:偏置扩展(AsymmetricDamping/AdaptiveLookahead)必须在 CinemachineConfiner3D 之前、
|
||||
// AxisLock 必须在之后,否则偏置会把相机推出限位区域(见 CameraStateController.ValidateVCamExtensionOrder)。
|
||||
GetOrAddComponent<CameraAsymmetricDampingExtension>(vcamAGo);
|
||||
GetOrAddComponent<CameraAdaptiveLookaheadExtension>(vcamAGo);
|
||||
GetOrAddComponent<CinemachineConfiner3D>(vcamAGo);
|
||||
GetOrAddComponent<CameraAxisLockExtension>(vcamAGo);
|
||||
// CinemachinePositionComposer:Body 阶段组件,必须存在;ConfigureSlot 依赖它写入所有相机跟随参数
|
||||
var composerA = GetOrAddComponent<CinemachinePositionComposer>(vcamAGo);
|
||||
ApplyComposerDefaults(composerA);
|
||||
// 幂等纠正:重跑脚手架时若既有 VCam 组件顺序不对,一并修正
|
||||
CameraAreaEditor.FixVCamExtensionOrder(vcamA);
|
||||
|
||||
GameObject vcamBGo = GetOrCreateChild(camera, "VCamB").gameObject;
|
||||
CinemachineCamera vcamB = GetOrAddComponent<CinemachineCamera>(vcamBGo);
|
||||
GetOrAddComponent<CinemachineConfiner3D>(vcamBGo);
|
||||
GetOrAddComponent<CameraAxisLockExtension>(vcamBGo);
|
||||
GetOrAddComponent<CameraAsymmetricDampingExtension>(vcamBGo);
|
||||
GetOrAddComponent<CameraAdaptiveLookaheadExtension>(vcamBGo);
|
||||
GetOrAddComponent<CinemachineConfiner3D>(vcamBGo);
|
||||
GetOrAddComponent<CameraAxisLockExtension>(vcamBGo);
|
||||
var composerB = GetOrAddComponent<CinemachinePositionComposer>(vcamBGo);
|
||||
ApplyComposerDefaults(composerB);
|
||||
CameraAreaEditor.FixVCamExtensionOrder(vcamB);
|
||||
|
||||
GameObject uiRootGo = GetOrCreateChild(ui, "UIRoot").gameObject;
|
||||
UIManager uiManager = GetOrAddComponent<UIManager>(uiRootGo);
|
||||
|
||||
// 统一 UI 导航栈:常驻此处,经 ServiceLocator 暴露,主菜单与游戏内共用。
|
||||
UINavigator uiNavigator = GetOrAddComponent<UINavigator>(uiRootGo);
|
||||
AssignAsset(uiNavigator, "_onUICancelPressed", report, false, "EVT_UICancelPressed");
|
||||
|
||||
GameObject hudCanvasGo = GetOrCreateCanvas(uiRootGo.transform, "HUD Canvas", 0);
|
||||
GameObject hudRootGo = GetOrCreateChild(hudCanvasGo.transform, "HUDRoot").gameObject;
|
||||
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);
|
||||
// 暂停菜单:实例化 UI_PauseScreen 预制件(样式在预制件、菜单项在 UI_PauseMenuConfig,数据驱动)
|
||||
GameObject pauseRootGo = EnsurePauseScreenInstance(uiRootGo.transform, report);
|
||||
GameObject settingsRootGo = GetOrCreateChild(uiRootGo.transform, "SettingsRoot").gameObject;
|
||||
GameObject mapRootGo = GetOrCreateChild(uiRootGo.transform, "MapRoot").gameObject;
|
||||
GameObject shopRootGo = GetOrCreateChild(uiRootGo.transform, "ShopRoot").gameObject;
|
||||
@@ -165,8 +171,7 @@ namespace BaseGames.Editor
|
||||
AssignAsset(gameManager, "_onSceneLoadRequest", report, false, "EVT_SceneLoadRequest");
|
||||
AssignAsset(gameManager, "_onSceneLoaded", report, false, "EVT_SceneLoaded");
|
||||
|
||||
AssignAsset(sceneLoader, "_onLoadingStarted", report, false, "EVT_LoadingStarted");
|
||||
AssignAsset(sceneLoader, "_onLoadingComplete", report, false, "EVT_LoadingComplete");
|
||||
// 加载进度由 SceneLoader 发布;加载画面显隐已归口到 SceneService(见下方 sceneService 绑定)。
|
||||
AssignAsset(sceneLoader, "_onLoadingProgressUpdated", report, false, "EVT_LoadingProgressUpdated");
|
||||
|
||||
// ── Canvas_Splash(启动演出)──────────────────────────────────────
|
||||
@@ -183,23 +188,9 @@ namespace BaseGames.Editor
|
||||
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");
|
||||
loadingCanvasGo.SetActive(false);
|
||||
// ── 加载界面(实例化 UI_LoadingScreen 预制件)──────────────────────
|
||||
// 样式/美术在预制件里由美术编辑、内容/时长在 UI_LoadingConfig 由策划编辑(数据驱动,对齐 MainMenu 范式)。
|
||||
EnsureLoadingScreenInstance(ui.transform, report);
|
||||
|
||||
// ── SceneFadeController(场景切换黑幕)───────────────────────────
|
||||
// 纯逻辑节点:监听淡出/淡入事件后调用 SceneFeedback.Play()。
|
||||
@@ -246,6 +237,9 @@ namespace BaseGames.Editor
|
||||
AssignAsset(sceneService, "_onFadeOutRequest", report, false, "EVT_FadeOutRequest");
|
||||
// 场景加载完毕、世界状态恢复后触发;场景物体据此应用存档状态,淡入前保证画面正确
|
||||
AssignAsset(sceneService, "_onSceneWorldStateRestored", report, true, "EVT_SceneWorldStateRestored");
|
||||
// 加载画面显隐归口到 SceneService(包裹 SceneLoader 与流式 coordinator 两条加载路径)
|
||||
AssignAsset(sceneService, "_onLoadingStarted", report, false, "EVT_LoadingStarted");
|
||||
AssignAsset(sceneService, "_onLoadingComplete", report, false, "EVT_LoadingComplete");
|
||||
AssignReference(sceneService, "_sceneLoader", sceneLoader);
|
||||
|
||||
AssignAsset(sceneLoader, "_onSceneLoaded", report, false, "EVT_SceneLoaded");
|
||||
@@ -301,12 +295,7 @@ namespace BaseGames.Editor
|
||||
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");
|
||||
// 暂停菜单引用(_config / 事件频道)由 EnsurePauseScreenInstance 在实例化时绑定。
|
||||
|
||||
AddScaffoldNote(hudRootGo, "HUDController 已挂载。其内部图片/文本/图标 Prefab 依赖较多,需后续手工补 UI 资源与事件频道。", report);
|
||||
|
||||
@@ -375,13 +364,15 @@ namespace BaseGames.Editor
|
||||
subTmp.color = new Color(0.7f, 0.66f, 0.55f, 0.9f); subTmp.raycastTarget = false; subTmp.characterSpacing = 8f;
|
||||
BindLocalizedText(subtitleRt.gameObject, "MENU_SUBTITLE");
|
||||
|
||||
// ── 主菜单控制器 ──────────────────────────────────────────────────
|
||||
MainMenuController menuCtrl = GetOrAddComponent<MainMenuController>(canvasGo);
|
||||
// ── 数据驱动主菜单控制器(退役旧 MainMenuController)──────────────
|
||||
RemoveComponentByTypeName(canvasGo, "MainMenuController");
|
||||
DataDrivenMainMenuController menuCtrl = GetOrAddComponent<DataDrivenMainMenuController>(canvasGo);
|
||||
AssignAsset(menuCtrl, "_onGameStateChanged", report, false, "EVT_GameStateChanged", "EVT_GameState");
|
||||
AssignAsset(menuCtrl, "_onSceneLoadRequest", report, false, "EVT_SceneLoadRequest");
|
||||
AssignAsset(menuCtrl, "_onSlotConfirmed", report, false, "EVT_SlotConfirmed");
|
||||
// 取消键(ESC / 手柄 B)由 UINavigator 统一消费,主菜单控制器不再订阅。
|
||||
|
||||
// ── 主按钮区域(底部居中竖排,带 CanvasGroup 供入场动画)─────────────
|
||||
// ── 主按钮容器(数据驱动:据 UI_MainMenuConfig 在运行时生成按钮)─────
|
||||
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));
|
||||
@@ -392,28 +383,17 @@ namespace BaseGames.Editor
|
||||
menuVlg.childControlWidth = true; menuVlg.childControlHeight = true;
|
||||
menuVlg.childForceExpandWidth = true; menuVlg.childForceExpandHeight = false;
|
||||
|
||||
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;
|
||||
}
|
||||
BindLocalizedButton(btnNewGameGo, "MENU_NEW_GAME");
|
||||
BindLocalizedButton(btnContinueGo, "MENU_CONTINUE");
|
||||
BindLocalizedButton(btnSettingsGo, "MENU_SETTINGS");
|
||||
BindLocalizedButton(btnCreditsGo, "MENU_CREDITS");
|
||||
BindLocalizedButton(btnQuitGo, "MENU_QUIT");
|
||||
// 移除旧硬编码按钮(改由控制器运行时据表生成)
|
||||
foreach (var bn in new[] { "Btn_NewGame", "Btn_Continue", "Btn_Settings", "Btn_Credits", "Btn_Quit" })
|
||||
DeleteChildIfPresent(menuPanelGo.transform, bn);
|
||||
|
||||
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>());
|
||||
var menuConfig = LoadControlAsset<MainMenuConfigSO>("Assets/_Game/Data/UI/UI_MainMenuConfig.asset", report);
|
||||
var menuBtnPrefab = LoadControlAsset<GameObject>("Assets/_Game/Prefabs/UI/Controls/UI_MainMenu_Button.prefab", report);
|
||||
var menuBtnView = menuBtnPrefab != null ? menuBtnPrefab.GetComponent<MainMenuButtonView>() : null;
|
||||
|
||||
AssignReference(menuCtrl, "_config", menuConfig);
|
||||
AssignReference(menuCtrl, "_container", menuPanelRt);
|
||||
AssignReference(menuCtrl, "_buttonPrefab", menuBtnView);
|
||||
AssignReference(menuCtrl, "_mainButtonsGroup", menuGroup);
|
||||
AssignReference(menuCtrl, "_mainButtonsRect", menuPanelRt);
|
||||
|
||||
@@ -423,9 +403,11 @@ namespace BaseGames.Editor
|
||||
GameObject saveSlotPanelGo = saveSlotPanelRt.gameObject;
|
||||
saveSlotPanelGo.SetActive(false);
|
||||
SaveSlotController saveSlotCtrl = GetOrAddComponent<SaveSlotController>(saveSlotPanelGo);
|
||||
// CanvasGroup:导航器把覆盖确认 / 模式选择对话框模态压在其上时,屏蔽本面板交互(避免方向键穿透)。
|
||||
var saveSlotGroup = GetOrAddComponent<CanvasGroup>(saveSlotPanelGo);
|
||||
AssignAsset(saveSlotCtrl, "_onSlotConfirmed", report, false, "EVT_SlotConfirmed");
|
||||
AssignReference(menuCtrl, "_saveSlotPanel", saveSlotPanelGo);
|
||||
AssignReference(menuCtrl, "_saveSlotController", saveSlotCtrl);
|
||||
AssignReference(saveSlotCtrl, "_canvasGroup", saveSlotGroup);
|
||||
AssignReference(menuCtrl, "_saveSlotPanel", saveSlotCtrl);
|
||||
|
||||
// 近乎不透明的遮罩(拦截背后点击,并遮住主菜单避免文字透出)
|
||||
GetOrCreateImage(saveSlotPanelGo.transform, "Overlay", new Color(0.04f, 0.05f, 0.08f, 0.97f), true)
|
||||
@@ -467,7 +449,7 @@ namespace BaseGames.Editor
|
||||
if (regionRegistry == null)
|
||||
report.Add("未找到 RegionRegistry 资产,SaveSlotUI._regionRegistry 未绑定(存档槽背景图失效)。先运行 BaseGames/Setup/Create Project Assets。");
|
||||
|
||||
// 返回按钮(关闭存档槽面板 → 绑定 MainMenuController._btnCloseSaveSlot)
|
||||
// 返回按钮(关闭存档槽面板 → 绑定 DataDrivenMainMenuController._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),
|
||||
@@ -476,31 +458,41 @@ namespace BaseGames.Editor
|
||||
BindLocalizedButton(slotBackGo, "BTN_BACK");
|
||||
AssignReference(menuCtrl, "_btnCloseSaveSlot", slotBackGo.GetComponent<Button>());
|
||||
|
||||
// ── ConfirmDialog(覆盖 / 删除确认)─────────────────────
|
||||
ConfirmDialogController confirmCtrl = BuildConfirmDialog(saveSlotPanelGo.transform);
|
||||
// ── ConfirmDialog / NewGameMode 作为 Canvas 直接子节点(非 SaveSlotPanel 子节点)──
|
||||
// 关键:模态压栈会屏蔽 SaveSlotPanel 的 CanvasGroup;若对话框是其子节点会被一同屏蔽。
|
||||
// 故挂到 Canvas 根(SaveSlotPanel 的兄弟,且因创建在后渲染于其上),彻底规避父子 CanvasGroup 传播。
|
||||
DeleteChildIfPresent(saveSlotPanelGo.transform, "ConfirmDialog");
|
||||
DeleteChildIfPresent(saveSlotPanelGo.transform, "NewGameMode");
|
||||
|
||||
ConfirmDialogController confirmCtrl = BuildConfirmDialog(canvasGo.transform);
|
||||
AssignReference(saveSlotCtrl, "_confirmDialog", confirmCtrl);
|
||||
|
||||
// ── NewGameMode(新游戏模式选择:普通 / 钢铁之魂)────────────────────
|
||||
NewGameModeController modeCtrl = BuildNewGameMode(saveSlotPanelGo.transform);
|
||||
NewGameModeController modeCtrl = BuildNewGameMode(canvasGo.transform);
|
||||
AssignReference(saveSlotCtrl, "_modeSelect", modeCtrl);
|
||||
|
||||
// ── SettingsPanel(音量 / 画面 / 可访问性 / 语言)──────────────────
|
||||
var settingsPanelRt = GetOrCreateUIChild(canvasGo.transform, "SettingsPanel");
|
||||
StretchFull(settingsPanelRt);
|
||||
BuildSettingsPanel(settingsPanelRt.gameObject, menuCtrl, report);
|
||||
// 整面板根作为导航器压栈对象(UISimplePanel):内含的数据驱动设置实例自管其 OnEnable 构建。
|
||||
var settingsPanel = GetOrAddComponent<UISimplePanel>(settingsPanelRt.gameObject);
|
||||
AssignReference(settingsPanel, "_canvasGroup", GetOrAddComponent<CanvasGroup>(settingsPanelRt.gameObject));
|
||||
settingsPanelRt.gameObject.SetActive(false);
|
||||
AssignReference(menuCtrl, "_settingsPanel", settingsPanelRt.gameObject);
|
||||
AssignReference(menuCtrl, "_settingsPanel", settingsPanel);
|
||||
|
||||
// ── CreditsPanel(制作团队)────────────────────────────────────────
|
||||
var creditsPanelRt = GetOrCreateUIChild(canvasGo.transform, "CreditsPanel");
|
||||
StretchFull(creditsPanelRt);
|
||||
BuildCreditsPanel(creditsPanelRt.gameObject, menuCtrl, report);
|
||||
var creditsPanel = GetOrAddComponent<UISimplePanel>(creditsPanelRt.gameObject);
|
||||
AssignReference(creditsPanel, "_canvasGroup", GetOrAddComponent<CanvasGroup>(creditsPanelRt.gameObject));
|
||||
creditsPanelRt.gameObject.SetActive(false);
|
||||
AssignReference(menuCtrl, "_creditsPanel", creditsPanelRt.gameObject);
|
||||
AssignReference(menuCtrl, "_creditsPanel", creditsPanel);
|
||||
|
||||
report.Add("设置 MainMenuController._firstGameSceneKey 为第一个游戏场景的 Addressable Key(字符串)。");
|
||||
report.Add("设置 DataDrivenMainMenuController._firstGameSceneKey 为第一个游戏场景的 Addressable Key(字符串)。");
|
||||
report.Add("主菜单按钮由 DataDrivenMainMenuController 据 UI_MainMenuConfig 在运行时生成(编辑器下 MenuPanel 为空属正常)。");
|
||||
report.Add("存档槽卡片已含完整布局与文本(区域 / 时长 / 时间 / 灵珠 / 生命 / 钢魂徽章),空槽显示\"开始新游戏\"提示。");
|
||||
report.Add("ConfirmDialog / NewGameMode 已作为 SaveSlotPanel 子节点生成并接线;需补本地化键:"
|
||||
report.Add("ConfirmDialog / NewGameMode 已作为 Canvas 直接子节点(非 SaveSlotPanel 子节点,规避模态 CanvasGroup 传播)生成并接线;需补本地化键:"
|
||||
+ "CONFIRM_OVERWRITE_TITLE / CONFIRM_OVERWRITE_BODY / CONFIRM_DELETE_TITLE / CONFIRM_DELETE_BODY / MODE_STEELSOUL_DESC(UI 表)。");
|
||||
|
||||
MarkDirtyAndLog("Main Menu 场景脚手架", root, report);
|
||||
@@ -625,6 +617,7 @@ namespace BaseGames.Editor
|
||||
GameObject confirmGo = rootRt.gameObject;
|
||||
confirmGo.SetActive(false);
|
||||
ConfirmDialogController confirmCtrl = GetOrAddComponent<ConfirmDialogController>(confirmGo);
|
||||
var confirmGroup = GetOrAddComponent<CanvasGroup>(confirmGo);
|
||||
|
||||
GetOrCreateImage(confirmGo.transform, "Overlay", new Color(0f, 0f, 0f, 0.6f), true).transform.SetAsFirstSibling();
|
||||
|
||||
@@ -645,7 +638,7 @@ namespace BaseGames.Editor
|
||||
BindLocalizedButton(yesGo, "CONFIRM_YES");
|
||||
BindLocalizedButton(noGo, "CONFIRM_NO");
|
||||
|
||||
AssignReference(confirmCtrl, "_root", confirmGo);
|
||||
AssignReference(confirmCtrl, "_canvasGroup", confirmGroup);
|
||||
AssignReference(confirmCtrl, "_titleText", titleTmp);
|
||||
AssignReference(confirmCtrl, "_bodyText", bodyTmp);
|
||||
AssignReference(confirmCtrl, "_confirmLabel", GetButtonLabel(yesGo));
|
||||
@@ -656,46 +649,45 @@ namespace BaseGames.Editor
|
||||
}
|
||||
|
||||
/// <summary>构建新游戏模式选择面板(居中模态:普通 / 钢铁之魂 / 返回 + 钢魂说明),返回控制器。</summary>
|
||||
/// <summary>
|
||||
/// 实例化 / 复用 <c>UI_NewGameModePanel</c> 预制件作为难度选择面板(命名 NewGameMode,挂在 MainMenu Canvas 下)。
|
||||
/// 数据驱动:样式在预制件、难度项在 UI_NewGameModeConfig。返回其 <see cref="NewGameModeController"/>(接 SaveSlot._modeSelect)。
|
||||
/// </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);
|
||||
const string instName = "NewGameMode";
|
||||
const string prefabPath = "Assets/_Game/Prefabs/UI/UI_NewGameModePanel.prefab";
|
||||
const string configPath = "Assets/_Game/Data/UI/UI_NewGameModeConfig.asset";
|
||||
|
||||
GetOrCreateImage(modeGo.transform, "Overlay", new Color(0f, 0f, 0f, 0.6f), true).transform.SetAsFirstSibling();
|
||||
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
|
||||
Transform existing = parent.Find(instName);
|
||||
|
||||
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);
|
||||
if (prefab == null)
|
||||
{
|
||||
Debug.LogWarning("[Scaffold] 缺少 UI_NewGameModePanel 预制件:请先运行「BaseGames/UI/控件库/生成新游戏难度选择(预制件 + 默认配置)」,再重跑本脚手架。");
|
||||
return existing != null ? existing.GetComponent<NewGameModeController>() : null;
|
||||
}
|
||||
|
||||
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 go;
|
||||
if (existing != null && PrefabUtility.GetCorrespondingObjectFromSource(existing.gameObject) != null)
|
||||
{
|
||||
go = existing.gameObject; // 已是预制件实例:复用
|
||||
}
|
||||
else
|
||||
{
|
||||
if (existing != null) Undo.DestroyObjectImmediate(existing.gameObject); // 历史裸物体:替换
|
||||
go = (GameObject)PrefabUtility.InstantiatePrefab(prefab, parent);
|
||||
go.name = instName;
|
||||
Undo.RegisterCreatedObjectUndo(go, "Instantiate UI_NewGameModePanel");
|
||||
}
|
||||
|
||||
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);
|
||||
BindLocalizedText(modeTitle.gameObject, "MODE_SELECT_TITLE");
|
||||
BindLocalizedButton(normalGo, "MODE_NORMAL");
|
||||
BindLocalizedButton(steelGo, "MODE_STEELSOUL");
|
||||
BindLocalizedButton(backGo, "BTN_BACK");
|
||||
|
||||
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;
|
||||
var ctrl = go.GetComponent<NewGameModeController>();
|
||||
if (ctrl != null)
|
||||
{
|
||||
var cfg = AssetDatabase.LoadAssetAtPath<NewGameModeConfigSO>(configPath);
|
||||
if (cfg != null) AssignReference(ctrl, "_config", cfg);
|
||||
}
|
||||
return ctrl;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
@@ -1302,10 +1294,15 @@ namespace BaseGames.Editor
|
||||
// 设置面板 / 制作团队面板构建器
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>构建设置面板内容(音量 / 画面 / 可访问性 / 语言)并绑定 SettingsPanelController 全部字段。</summary>
|
||||
private static void BuildSettingsPanel(GameObject panelGo, MainMenuController menuCtrl, List<string> report)
|
||||
/// <summary>
|
||||
/// 构建设置面板:退役旧 SettingsPanelController,改用数据驱动 UI_SettingsPanel 预制件
|
||||
/// (自带 SettingsSchemaSO + 行预制件 + DataDrivenSettingsPanel,运行时据表生成控件行)。
|
||||
/// </summary>
|
||||
private static void BuildSettingsPanel(GameObject panelGo, DataDrivenMainMenuController menuCtrl, List<string> report)
|
||||
{
|
||||
var ctrl = GetOrAddComponent<BaseGames.UI.SettingsPanelController>(panelGo);
|
||||
// 退役旧硬编码控制器与内联内容(幂等替换)
|
||||
RemoveComponentByTypeName(panelGo, "SettingsPanelController");
|
||||
DeleteChildIfPresent(panelGo.transform, "Content");
|
||||
|
||||
GetOrCreateImage(panelGo.transform, "Overlay", new Color(0.04f, 0.05f, 0.08f, 0.97f), true).transform.SetAsFirstSibling();
|
||||
|
||||
@@ -1314,55 +1311,12 @@ namespace BaseGames.Editor
|
||||
SetRect((RectTransform)titleTmp.transform, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -70f), new Vector2(900f, 80f));
|
||||
BindLocalizedText(titleTmp.gameObject, "SETTINGS_TITLE");
|
||||
|
||||
var content = GetOrCreateUIChild(panelGo.transform, "Content");
|
||||
SetRect(content, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0f, -10f), new Vector2(860f, 720f));
|
||||
var vlg = GetOrAddComponent<VerticalLayoutGroup>(content.gameObject);
|
||||
vlg.spacing = 8f; vlg.childAlignment = TextAnchor.UpperCenter;
|
||||
vlg.childControlWidth = true; vlg.childControlHeight = true;
|
||||
vlg.childForceExpandWidth = true; vlg.childForceExpandHeight = false;
|
||||
|
||||
// 音量
|
||||
var rMaster = CreateSettingRow(content, "Row_Master", "SETTINGS_MASTER_VOLUME");
|
||||
var rBgm = CreateSettingRow(content, "Row_BGM", "SETTINGS_BGM_VOLUME");
|
||||
var rSfx = CreateSettingRow(content, "Row_SFX", "SETTINGS_SFX_VOLUME");
|
||||
var rAmbient = CreateSettingRow(content, "Row_Ambient", "SETTINGS_AMBIENT_VOLUME");
|
||||
var sMaster = GetOrCreateSliderInRow(rMaster, 0f, 1f, 1f);
|
||||
var sBgm = GetOrCreateSliderInRow(rBgm, 0f, 1f, 0.8f);
|
||||
var sSfx = GetOrCreateSliderInRow(rSfx, 0f, 1f, 1f);
|
||||
var sAmbient = GetOrCreateSliderInRow(rAmbient, 0f, 1f, 0.8f);
|
||||
|
||||
// 画面
|
||||
var rVsync = CreateSettingRow(content, "Row_VSync", "SETTINGS_VSYNC");
|
||||
var tVsync = GetOrCreateToggleInRow(rVsync);
|
||||
var rFps = CreateSettingRow(content, "Row_FPS", "SETTINGS_FPS");
|
||||
var dFps = GetOrCreateDropdownInRow(rFps, new[] { "30", "60", "120", "∞" });
|
||||
|
||||
// 可访问性
|
||||
var rUiScale = CreateSettingRow(content, "Row_UIScale", "SETTINGS_UI_SCALE");
|
||||
var sUiScale = GetOrCreateSliderInRow(rUiScale, 0.8f, 1.5f, 1f, 76f);
|
||||
var uiScaleVal = GetOrCreateText(rUiScale, "ValueText", "100%", 22f, new Color(0.8f,0.78f,0.7f,1f), TextAlignmentOptions.MidlineRight);
|
||||
SetRect((RectTransform)uiScaleVal.transform, new Vector2(1f, 0.5f), new Vector2(1f, 0.5f), new Vector2(1f, 0.5f), new Vector2(-12f, 0f), new Vector2(64f, 36f));
|
||||
var rColorblind = CreateSettingRow(content, "Row_Colorblind", "SETTINGS_COLORBLIND");
|
||||
var dColorblind = GetOrCreateDropdownInRow(rColorblind, new[] { "关闭", "红色盲", "绿色盲", "蓝黄色盲" });
|
||||
var rShake = CreateSettingRow(content, "Row_ScreenShake", "SETTINGS_SCREEN_SHAKE");
|
||||
var tShake = GetOrCreateToggleInRow(rShake);
|
||||
|
||||
// 语言
|
||||
var rLang = CreateSettingRow(content, "Row_Language", "SETTINGS_LANGUAGE");
|
||||
var dLang = GetOrCreateDropdownInRow(rLang, new[] { "中文", "English", "日本語", "한국어" });
|
||||
|
||||
// 绑定 SettingsPanelController 字段
|
||||
AssignReference(ctrl, "_masterVolume", sMaster);
|
||||
AssignReference(ctrl, "_bgmVolume", sBgm);
|
||||
AssignReference(ctrl, "_sfxVolume", sSfx);
|
||||
AssignReference(ctrl, "_ambientVolume", sAmbient);
|
||||
AssignReference(ctrl, "_vSyncToggle", tVsync);
|
||||
AssignReference(ctrl, "_fpsDropdown", dFps);
|
||||
AssignReference(ctrl, "_uiScaleSlider", sUiScale);
|
||||
AssignReference(ctrl, "_uiScaleValueText", uiScaleVal);
|
||||
AssignReference(ctrl, "_colorblindDropdown", dColorblind);
|
||||
AssignReference(ctrl, "_screenShakeToggle", tShake);
|
||||
AssignReference(ctrl, "_languageDropdown", dLang);
|
||||
// 数据驱动设置面板预制件(运行时据 UI_SettingsSchema 生成 Slider/Toggle/Dropdown 行)
|
||||
var settingsInst = InstantiateControlPrefabOnce(
|
||||
"Assets/_Game/Prefabs/UI/Controls/UI_SettingsPanel.prefab", panelGo.transform, "DataDrivenSettings", report);
|
||||
if (settingsInst != null && settingsInst.transform is RectTransform settingsRt)
|
||||
SetRect(settingsRt, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f),
|
||||
new Vector2(0f, -10f), new Vector2(560f, 720f));
|
||||
|
||||
// 返回按钮
|
||||
GameObject backGo = GetOrCreateButtonChild(panelGo.transform, "BackButton", "Back");
|
||||
@@ -1373,7 +1327,7 @@ namespace BaseGames.Editor
|
||||
}
|
||||
|
||||
/// <summary>构建制作团队面板(标题 + 滚动正文 + 返回)。</summary>
|
||||
private static void BuildCreditsPanel(GameObject panelGo, MainMenuController menuCtrl, List<string> report)
|
||||
private static void BuildCreditsPanel(GameObject panelGo, DataDrivenMainMenuController menuCtrl, List<string> report)
|
||||
{
|
||||
GetOrCreateImage(panelGo.transform, "Overlay", new Color(0.04f, 0.05f, 0.08f, 0.97f), true).transform.SetAsFirstSibling();
|
||||
|
||||
@@ -1393,6 +1347,54 @@ namespace BaseGames.Editor
|
||||
AssignReference(menuCtrl, "_btnCloseCredits", backGo.GetComponent<Button>());
|
||||
}
|
||||
|
||||
// ── 数据驱动迁移助手 ──────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 按类型名移除组件(不引用类型符号,便于退役后删除脚本而不破坏脚手架编译)。
|
||||
/// 已成为 missing script 的组件不会被此方法移除(GetComponents 返回 null)。
|
||||
/// </summary>
|
||||
private static void RemoveComponentByTypeName(GameObject go, string typeName)
|
||||
{
|
||||
if (go == null) return;
|
||||
foreach (var c in go.GetComponents<MonoBehaviour>())
|
||||
if (c != null && c.GetType().Name == typeName)
|
||||
Object.DestroyImmediate(c);
|
||||
}
|
||||
|
||||
/// <summary>若存在同名子节点则删除(幂等替换旧硬编码内容)。</summary>
|
||||
private static void DeleteChildIfPresent(Transform parent, string name)
|
||||
{
|
||||
var t = parent.Find(name);
|
||||
if (t != null) Object.DestroyImmediate(t.gameObject);
|
||||
}
|
||||
|
||||
/// <summary>加载控件库资产(不存在则记入报告)。</summary>
|
||||
private static T LoadControlAsset<T>(string path, List<string> report) where T : Object
|
||||
{
|
||||
var a = AssetDatabase.LoadAssetAtPath<T>(path);
|
||||
if (a == null)
|
||||
report.Add($"未找到资产 {path}(请先运行 BaseGames/UI/控件库 下的生成菜单)。");
|
||||
return a;
|
||||
}
|
||||
|
||||
/// <summary>把控件库预制件实例化为指定父节点的子节点(按名幂等:已存在则复用)。</summary>
|
||||
private static GameObject InstantiateControlPrefabOnce(string prefabPath, Transform parent,
|
||||
string instanceName, List<string> report)
|
||||
{
|
||||
var existing = parent.Find(instanceName);
|
||||
if (existing != null) return existing.gameObject;
|
||||
|
||||
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
|
||||
if (prefab == null)
|
||||
{
|
||||
report.Add($"未找到预制件 {prefabPath}(请先运行 BaseGames/UI/控件库 下的生成菜单)。");
|
||||
return null;
|
||||
}
|
||||
var inst = (GameObject)PrefabUtility.InstantiatePrefab(prefab, parent);
|
||||
inst.name = instanceName;
|
||||
return inst;
|
||||
}
|
||||
|
||||
private static void AssignReference(Object target, string propertyName, Object value)
|
||||
{
|
||||
AssignReference(target, propertyName, value, null);
|
||||
@@ -1428,6 +1430,95 @@ namespace BaseGames.Editor
|
||||
serializedObject.ApplyModifiedPropertiesWithoutUndo();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 实例化 / 复用 <c>UI_LoadingScreen</c> 预制件作为 Persistent 的加载界面(命名 Canvas_Loading)。
|
||||
/// 历史的非预制件裸物体会被替换为预制件实例;绑定 <c>_config</c> 与三个加载事件频道。
|
||||
/// 预制件缺失时仅在 report 提示(先跑「生成加载界面」菜单),不报错。
|
||||
/// </summary>
|
||||
private static void EnsureLoadingScreenInstance(Transform uiParent, List<string> report)
|
||||
{
|
||||
const string instName = "Canvas_Loading";
|
||||
const string prefabPath = "Assets/_Game/Prefabs/UI/UI_LoadingScreen.prefab";
|
||||
const string configPath = "Assets/_Game/Data/UI/UI_LoadingConfig.asset";
|
||||
|
||||
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
|
||||
if (prefab == null)
|
||||
{
|
||||
report.Add("缺少 UI_LoadingScreen 预制件:请先运行菜单「BaseGames/UI/控件库/生成加载界面(预制件 + 默认配置)」,再重跑本脚手架。");
|
||||
return;
|
||||
}
|
||||
|
||||
Transform existing = uiParent.Find(instName);
|
||||
GameObject go;
|
||||
if (existing != null && PrefabUtility.GetCorrespondingObjectFromSource(existing.gameObject) != null)
|
||||
{
|
||||
go = existing.gameObject; // 已是预制件实例:复用
|
||||
}
|
||||
else
|
||||
{
|
||||
if (existing != null) Undo.DestroyObjectImmediate(existing.gameObject); // 历史裸物体:替换为预制件实例
|
||||
go = (GameObject)PrefabUtility.InstantiatePrefab(prefab, uiParent);
|
||||
go.name = instName;
|
||||
Undo.RegisterCreatedObjectUndo(go, "Instantiate UI_LoadingScreen");
|
||||
}
|
||||
|
||||
var loadingMgr = go.GetComponent<LoadingScreenManager>();
|
||||
if (loadingMgr != null)
|
||||
{
|
||||
var cfg = AssetDatabase.LoadAssetAtPath<BaseGames.UI.LoadingScreenConfigSO>(configPath);
|
||||
if (cfg != null) AssignReference(loadingMgr, "_config", cfg);
|
||||
AssignAsset(loadingMgr, "_onLoadingStarted", report, false, "EVT_LoadingStarted");
|
||||
AssignAsset(loadingMgr, "_onLoadingComplete", report, false, "EVT_LoadingComplete");
|
||||
AssignAsset(loadingMgr, "_onLoadingProgressUpdated", report, false, "EVT_LoadingProgressUpdated");
|
||||
}
|
||||
report.Add("Canvas_Loading:由 UI_LoadingScreen 预制件实例化(样式在预制件内由美术编辑;内容/时长在 UI_LoadingConfig 由策划编辑)。");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 实例化 / 复用 <c>UI_PauseScreen</c> 预制件作为暂停面板(命名 PauseMenuRoot,供 UIManager PanelId.Pause 注册)。
|
||||
/// 历史的裸物体会被替换为预制件实例;绑定 <c>_config</c> 与事件频道。返回面板根 GameObject。
|
||||
/// </summary>
|
||||
private static GameObject EnsurePauseScreenInstance(Transform uiParent, List<string> report)
|
||||
{
|
||||
const string instName = "PauseMenuRoot";
|
||||
const string prefabPath = "Assets/_Game/Prefabs/UI/UI_PauseScreen.prefab";
|
||||
const string configPath = "Assets/_Game/Data/UI/UI_PauseMenuConfig.asset";
|
||||
|
||||
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
|
||||
Transform existing = uiParent.Find(instName);
|
||||
|
||||
if (prefab == null)
|
||||
{
|
||||
report.Add("缺少 UI_PauseScreen 预制件:请先运行「BaseGames/UI/控件库/生成暂停界面(预制件 + 默认配置)」,再重跑本脚手架。");
|
||||
return existing != null ? existing.gameObject : GetOrCreateChild(uiParent, instName).gameObject;
|
||||
}
|
||||
|
||||
GameObject go;
|
||||
if (existing != null && PrefabUtility.GetCorrespondingObjectFromSource(existing.gameObject) != null)
|
||||
{
|
||||
go = existing.gameObject; // 已是预制件实例:复用
|
||||
}
|
||||
else
|
||||
{
|
||||
if (existing != null) Undo.DestroyObjectImmediate(existing.gameObject); // 历史裸物体:替换
|
||||
go = (GameObject)PrefabUtility.InstantiatePrefab(prefab, uiParent);
|
||||
go.name = instName;
|
||||
Undo.RegisterCreatedObjectUndo(go, "Instantiate UI_PauseScreen");
|
||||
}
|
||||
|
||||
var ctrl = go.GetComponent<DataDrivenPauseMenuController>();
|
||||
if (ctrl != null)
|
||||
{
|
||||
var cfg = AssetDatabase.LoadAssetAtPath<PauseMenuConfigSO>(configPath);
|
||||
if (cfg != null) AssignReference(ctrl, "_config", cfg);
|
||||
// 与 GameManager._onResumeRequested 同源:实际资产名为 EVT_PauseResumed(EVT_ResumeRequested 不存在,回退)
|
||||
AssignAsset(ctrl, "_onResumeRequested", report, false, "EVT_ResumeRequested", "EVT_PauseResumed");
|
||||
AssignAsset(ctrl, "_onSceneLoadRequest", report, false, "EVT_SceneLoadRequest");
|
||||
}
|
||||
report.Add("PauseMenuRoot:由 UI_PauseScreen 预制件实例化(样式在预制件、菜单项在 UI_PauseMenuConfig)。");
|
||||
return go;
|
||||
}
|
||||
|
||||
private static void AssignAsset(Object target, string propertyName, List<string> report, bool required, params string[] candidates)
|
||||
{
|
||||
Object asset = FindFirstAsset(candidates);
|
||||
|
||||
Reference in New Issue
Block a user