This commit is contained in:
2026-06-07 11:49:55 +08:00
parent ff0f3bde54
commit 1897658a00
98 changed files with 9903 additions and 13907 deletions

View File

@@ -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);
// CinemachinePositionComposerBody 阶段组件必须存在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_DESCUI 表)。");
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_PauseResumedEVT_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);