UI系统
This commit is contained in:
@@ -12,7 +12,7 @@ namespace BaseGames.Editor.Debugging
|
||||
/// 直接加载首关(<see cref="AddressKeys.SceneGameChapter1"/>,当前映射到 TestRoomA)并生成玩家,
|
||||
/// 便于在编辑器中快速验证游戏内系统(如地图)。
|
||||
/// <para>
|
||||
/// 复刻 <c>MainMenuController.HandleSlotConfirmed</c> 的核心动作:建立内存存档 + 发场景过渡请求。
|
||||
/// 复刻 <c>DataDrivenMainMenuController.HandleSlotConfirmed</c> 的核心动作:建立内存存档 + 发场景过渡请求。
|
||||
/// 仅在 Play 模式可用。
|
||||
/// </para>
|
||||
/// 菜单:BaseGames ▸ Debug ▸ Enter First Room (Play)
|
||||
@@ -49,7 +49,6 @@ namespace BaseGames.Editor.Debugging
|
||||
SceneName = AddressKeys.SceneGameChapter1, // 当前映射到 TestRoomA
|
||||
EntryTransitionId = null, // 默认出生点
|
||||
TransitionType = TransitionType.Scene,
|
||||
ShowLoadingScreen = true,
|
||||
IsRespawn = false,
|
||||
});
|
||||
|
||||
|
||||
@@ -138,8 +138,22 @@ namespace BaseGames.Editor
|
||||
() => IsFieldBound(FindInLoadedScene<BootSequencer>(PersistentName), "_onSplashStartRequest")),
|
||||
("BootSequencer._onSplashComplete 已绑定",
|
||||
() => IsFieldBound(FindInLoadedScene<BootSequencer>(PersistentName), "_onSplashComplete")),
|
||||
("SceneLoader._onLoadingStarted 已绑定",
|
||||
() => IsFieldBound(FindInLoadedScene<SceneLoader>(PersistentName), "_onLoadingStarted")),
|
||||
("SceneService._onLoadingStarted 已绑定",
|
||||
() => IsFieldBound(FindInLoadedScene<SceneService>(PersistentName), "_onLoadingStarted")),
|
||||
("SceneService._onLoadingComplete 已绑定",
|
||||
() => IsFieldBound(FindInLoadedScene<SceneService>(PersistentName), "_onLoadingComplete")),
|
||||
("SceneLoader._onLoadingProgressUpdated 已绑定",
|
||||
() => IsFieldBound(FindInLoadedScene<SceneLoader>(PersistentName), "_onLoadingProgressUpdated")),
|
||||
("LoadingScreenManager._loadingRoot 已绑定",
|
||||
() => IsFieldBound(FindInLoadedScene<LoadingScreenManager>(PersistentName), "_loadingRoot")),
|
||||
("LoadingScreenManager._progressFill 已绑定",
|
||||
() => IsFieldBound(FindInLoadedScene<LoadingScreenManager>(PersistentName), "_progressFill")),
|
||||
("LoadingScreenManager._tipText 已绑定",
|
||||
() => IsFieldBound(FindInLoadedScene<LoadingScreenManager>(PersistentName), "_tipText")),
|
||||
("LoadingScreenManager._backgroundArts 非空",
|
||||
() => IsArrayFieldNonEmpty(FindInLoadedScene<LoadingScreenManager>(PersistentName), "_backgroundArts")),
|
||||
("LoadingScreenManager._config 已绑定",
|
||||
() => IsFieldBound(FindInLoadedScene<LoadingScreenManager>(PersistentName), "_config")),
|
||||
("GameManager._bootSequencer 已绑定",
|
||||
() => IsFieldBound(FindInLoadedScene<GameManager>(PersistentName), "_bootSequencer")),
|
||||
("GameManager._onSceneLoadRequest 已绑定",
|
||||
@@ -168,14 +182,14 @@ namespace BaseGames.Editor
|
||||
|
||||
var checks = new (string label, System.Func<bool> fn)[]
|
||||
{
|
||||
("MainMenuController 组件存在", () => FindInLoadedScene<MainMenuController>(MainMenuName) != null),
|
||||
("DataDrivenMainMenuController 组件存在", () => FindInLoadedScene<DataDrivenMainMenuController>(MainMenuName) != null),
|
||||
("SaveSlotController 组件存在", () => FindInLoadedScene<SaveSlotController>(MainMenuName) != null),
|
||||
("MainMenuController._onSceneLoadRequest 已绑定",
|
||||
() => IsFieldBound(FindInLoadedScene<MainMenuController>(MainMenuName), "_onSceneLoadRequest")),
|
||||
("MainMenuController._onSlotConfirmed 已绑定",
|
||||
() => IsFieldBound(FindInLoadedScene<MainMenuController>(MainMenuName), "_onSlotConfirmed")),
|
||||
("MainMenuController._firstGameSceneKey 非空",
|
||||
() => IsStringFieldNonEmpty(FindInLoadedScene<MainMenuController>(MainMenuName), "_firstGameSceneKey")),
|
||||
("DataDrivenMainMenuController._onSceneLoadRequest 已绑定",
|
||||
() => IsFieldBound(FindInLoadedScene<DataDrivenMainMenuController>(MainMenuName), "_onSceneLoadRequest")),
|
||||
("DataDrivenMainMenuController._onSlotConfirmed 已绑定",
|
||||
() => IsFieldBound(FindInLoadedScene<DataDrivenMainMenuController>(MainMenuName), "_onSlotConfirmed")),
|
||||
("DataDrivenMainMenuController._firstGameSceneKey 非空",
|
||||
() => IsStringFieldNonEmpty(FindInLoadedScene<DataDrivenMainMenuController>(MainMenuName), "_firstGameSceneKey")),
|
||||
("Scene_MainMenu 已加入 Build Settings",
|
||||
() => IsInBuildSettings(MainMenuName)),
|
||||
};
|
||||
@@ -296,14 +310,23 @@ namespace BaseGames.Editor
|
||||
CheckField<GameManager>(PersistentName, "_bootSequencer", report, ref passCount, ref failCount);
|
||||
CheckField<GameManager>(PersistentName, "_onSceneLoadRequest", report, ref passCount, ref failCount);
|
||||
CheckField<GameManager>(PersistentName, "_onSceneLoaded", report, ref passCount, ref failCount);
|
||||
CheckField<SceneLoader>(PersistentName, "_onLoadingStarted", report, ref passCount, ref failCount);
|
||||
CheckField<SceneLoader>(PersistentName, "_onLoadingComplete", report, ref passCount, ref failCount);
|
||||
CheckField<SceneService>(PersistentName, "_onLoadingStarted", report, ref passCount, ref failCount);
|
||||
CheckField<SceneService>(PersistentName, "_onLoadingComplete", report, ref passCount, ref failCount);
|
||||
CheckField<SceneLoader>(PersistentName, "_onLoadingProgressUpdated", report, ref passCount, ref failCount);
|
||||
|
||||
// LoadingScreenManager 自身 UI 字段
|
||||
CheckField<LoadingScreenManager>(PersistentName, "_loadingRoot", report, ref passCount, ref failCount);
|
||||
CheckField<LoadingScreenManager>(PersistentName, "_progressFill", report, ref passCount, ref failCount);
|
||||
CheckField<LoadingScreenManager>(PersistentName, "_tipText", report, ref passCount, ref failCount);
|
||||
if (IsArrayFieldNonEmpty(FindInLoadedScene<LoadingScreenManager>(PersistentName), "_backgroundArts"))
|
||||
passCount++;
|
||||
else { failCount++; report.Add("[未绑定] LoadingScreenManager._backgroundArts 为空(在 Persistent)。"); }
|
||||
CheckField<LoadingScreenManager>(PersistentName, "_config", report, ref passCount, ref failCount);
|
||||
|
||||
// Main menu scene
|
||||
CheckComponent<MainMenuController>(MainMenuName, "MainMenuController", report, ref passCount, ref failCount);
|
||||
CheckField<MainMenuController>(MainMenuName, "_onSceneLoadRequest", report, ref passCount, ref failCount);
|
||||
CheckField<MainMenuController>(MainMenuName, "_firstGameSceneKey", report, ref passCount, ref failCount, requireNonEmpty: true);
|
||||
CheckComponent<DataDrivenMainMenuController>(MainMenuName, "DataDrivenMainMenuController", report, ref passCount, ref failCount);
|
||||
CheckField<DataDrivenMainMenuController>(MainMenuName, "_onSceneLoadRequest", report, ref passCount, ref failCount);
|
||||
CheckField<DataDrivenMainMenuController>(MainMenuName, "_firstGameSceneKey", report, ref passCount, ref failCount, requireNonEmpty: true);
|
||||
|
||||
if (!IsInBuildSettings(MainMenuName))
|
||||
{
|
||||
@@ -499,6 +522,19 @@ namespace BaseGames.Editor
|
||||
return prop != null && !string.IsNullOrEmpty(prop.stringValue);
|
||||
}
|
||||
|
||||
/// <summary>数组字段是否存在且至少有一个非空(对象引用)元素。</summary>
|
||||
private static bool IsArrayFieldNonEmpty(Object target, string fieldName)
|
||||
{
|
||||
if (target == null) return false;
|
||||
var so = new SerializedObject(target);
|
||||
var prop = so.FindProperty(fieldName);
|
||||
if (prop == null || !prop.isArray) return false;
|
||||
for (int i = 0; i < prop.arraySize; i++)
|
||||
if (prop.GetArrayElementAtIndex(i).objectReferenceValue != null)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsInBuildSettings(string sceneName)
|
||||
{
|
||||
foreach (var scene in EditorBuildSettings.scenes)
|
||||
|
||||
@@ -12,6 +12,7 @@ using BaseGames.Enemies.Boss;
|
||||
using BaseGames.Enemies.Navigation;
|
||||
using BaseGames.Enemies.Perception;
|
||||
using BaseGames.Equipment;
|
||||
using BaseGames.Feedback;
|
||||
using BaseGames.Parry;
|
||||
using BaseGames.Player;
|
||||
using BaseGames.Player.States;
|
||||
@@ -84,6 +85,9 @@ namespace BaseGames.Editor
|
||||
ShieldComponent shield = GetOrAddComponent<ShieldComponent>(root);
|
||||
PlayerWallDetector wallDetector = GetOrAddComponent<PlayerWallDetector>(root);
|
||||
EquipmentManager equipmentManager = GetOrAddComponent<EquipmentManager>(root);
|
||||
// PlayerFeedback:EquipmentManager.Awake 经 GetComponent<PlayerFeedback>() 取本节点引用(护符效果反馈),
|
||||
// 缺失会触发断言。须与 EquipmentManager 同节点。Feel 反馈链(MMF_Player)留待 Inspector 配置。
|
||||
GetOrAddComponent<PlayerFeedback>(root);
|
||||
GetOrAddComponent<SkillModifierRegistry>(root);
|
||||
StatusEffectManager statusEffectManager = GetOrAddComponent<StatusEffectManager>(root);
|
||||
// PlayerController 最后添加:RequireComponent 会拉取上方已加好的组件
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
using BaseGames.Localization;
|
||||
using BaseGames.UI.MainMenu;
|
||||
|
||||
namespace BaseGames.Editor.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="DataDrivenMainMenuController"/> 自定义 Inspector。
|
||||
///
|
||||
/// 菜单按钮运行时据 UI_MainMenuConfig 生成,编辑器下 MenuPanel 默认为空。
|
||||
/// 本编辑器提供「预览 / 清除」按钮:在编辑器内据表生成按钮,让美术直观查看布局、
|
||||
/// 就近调整 UI_MainMenu_Button 预制件样式与 MenuPanel 布局参数。预览按钮运行时会被自动清空重建。
|
||||
/// </summary>
|
||||
[CustomEditor(typeof(DataDrivenMainMenuController))]
|
||||
public class DataDrivenMainMenuControllerEditor : UnityEditor.Editor
|
||||
{
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
DrawDefaultInspector();
|
||||
|
||||
var ctrl = (DataDrivenMainMenuController)target;
|
||||
EditorGUILayout.Space(8);
|
||||
EditorGUILayout.HelpBox(
|
||||
"菜单按钮在运行时据 UI_MainMenuConfig 自动生成(编辑器下 MenuPanel 为空属正常)。\n" +
|
||||
"点「预览」在编辑器内据表生成按钮以查看 / 调整样式与布局;样式改 UI_MainMenu_Button 预制件 / UI_Theme_Default。\n" +
|
||||
"预览按钮仅用于编辑器查看,进入 Play 会自动清空并据表重建。",
|
||||
MessageType.Info);
|
||||
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
if (GUILayout.Button("▶ 预览菜单(编辑器内)", GUILayout.Height(26)))
|
||||
PreviewMenu(ctrl);
|
||||
if (GUILayout.Button("✕ 清除预览", GUILayout.Height(26), GUILayout.Width(120)))
|
||||
{
|
||||
ctrl.ClearMenu();
|
||||
MarkDirty(ctrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void PreviewMenu(DataDrivenMainMenuController ctrl)
|
||||
{
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
EditorUtility.DisplayDialog("预览菜单", "请退出 Play 模式后再使用编辑器预览。", "确定");
|
||||
return;
|
||||
}
|
||||
|
||||
ctrl.BuildMenu(); // 内部 ClearMenu + 据表实例化按钮(编辑器用 DestroyImmediate)
|
||||
|
||||
// 标签:运行时 SetKey 在无服务时显示 key,这里改用编辑器预览显示本地化译文
|
||||
foreach (var loc in ctrl.GetComponentsInChildren<LocalizedText>(true))
|
||||
loc.UpdateEditorPreview();
|
||||
|
||||
MarkDirty(ctrl);
|
||||
}
|
||||
|
||||
private static void MarkDirty(DataDrivenMainMenuController ctrl)
|
||||
{
|
||||
EditorUtility.SetDirty(ctrl);
|
||||
if (!Application.isPlaying)
|
||||
EditorSceneManager.MarkSceneDirty(ctrl.gameObject.scene);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9a79a1b78119328418b64ed2c384ea9c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,68 @@
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
using BaseGames.Localization;
|
||||
using BaseGames.UI.Settings;
|
||||
|
||||
namespace BaseGames.Editor.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="DataDrivenSettingsPanel"/> 自定义 Inspector。
|
||||
///
|
||||
/// 设置行运行时据 SettingsSchemaSO 生成,编辑器下行容器默认为空。
|
||||
/// 本编辑器提供「预览 / 清除」按钮:在编辑器内据表生成行,让美术直观查看分节与布局、
|
||||
/// 就近调整 UI_Setting_*Row 预制件样式与容器布局参数。预览行运行时会被自动清空重建。
|
||||
/// (与 <see cref="DataDrivenMainMenuControllerEditor"/> 同一套预览模式。)
|
||||
/// </summary>
|
||||
[CustomEditor(typeof(DataDrivenSettingsPanel))]
|
||||
public class DataDrivenSettingsPanelEditor : UnityEditor.Editor
|
||||
{
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
DrawDefaultInspector();
|
||||
|
||||
var panel = (DataDrivenSettingsPanel)target;
|
||||
EditorGUILayout.Space(8);
|
||||
EditorGUILayout.HelpBox(
|
||||
"设置行在运行时据 SettingsSchemaSO 自动生成(编辑器下行容器为空属正常)。\n" +
|
||||
"点「预览」在编辑器内据表生成行以查看 / 调整样式与布局;样式改 UI_Setting_*Row 预制件 / UI_Theme_Default。\n" +
|
||||
"预览行仅用于编辑器查看(不绑定 ISettingsService),进入 Play 会自动清空并据表重建。",
|
||||
MessageType.Info);
|
||||
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
if (GUILayout.Button("▶ 预览设置行(编辑器内)", GUILayout.Height(26)))
|
||||
PreviewRows(panel);
|
||||
if (GUILayout.Button("✕ 清除预览", GUILayout.Height(26), GUILayout.Width(120)))
|
||||
{
|
||||
panel.Clear();
|
||||
MarkDirty(panel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void PreviewRows(DataDrivenSettingsPanel panel)
|
||||
{
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
EditorUtility.DisplayDialog("预览设置行", "请退出 Play 模式后再使用编辑器预览。", "确定");
|
||||
return;
|
||||
}
|
||||
|
||||
panel.Build(); // 内部 Clear + 据表实例化行(编辑器无 ISettingsService,仅生成行 + 标签)
|
||||
|
||||
// 标签:运行时 SetKey 在无服务时显示 key,这里改用编辑器预览显示本地化译文
|
||||
foreach (var loc in panel.GetComponentsInChildren<LocalizedText>(true))
|
||||
loc.UpdateEditorPreview();
|
||||
|
||||
MarkDirty(panel);
|
||||
}
|
||||
|
||||
private static void MarkDirty(DataDrivenSettingsPanel panel)
|
||||
{
|
||||
EditorUtility.SetDirty(panel);
|
||||
if (!Application.isPlaying)
|
||||
EditorSceneManager.MarkSceneDirty(panel.gameObject.scene);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9afba7ce47e6eeb41a8d08751bd4ff13
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -350,7 +350,6 @@ namespace BaseGames.Editor.UI
|
||||
var cancelBtn = MakeButton(boxGo.transform, "CancelButton", "取消", new Vector2( 110, -90), out TMP_Text cancelLabel);
|
||||
|
||||
var dialog = GetOrAddComponent<ConfirmDialogController>(dialogGo);
|
||||
AssignRef(dialog, "_root", dialogGo);
|
||||
AssignRef(dialog, "_titleText", title);
|
||||
AssignRef(dialog, "_bodyText", body);
|
||||
AssignRef(dialog, "_confirmLabel", confirmLabel);
|
||||
|
||||
@@ -359,6 +359,7 @@ namespace BaseGames.Editor.UI
|
||||
private static GameObject BuildSettingHeader(List<string> report)
|
||||
{
|
||||
var go = NewUI(PfSettingHeader, 480, 32);
|
||||
var hdrLe = go.AddComponent<LayoutElement>(); hdrLe.preferredHeight = 36f; hdrLe.minHeight = 32f;
|
||||
var lblGo = NewUIChild(go.transform, "Label", out var rt);
|
||||
Stretch(rt);
|
||||
var lbl = lblGo.AddComponent<TextMeshProUGUI>();
|
||||
@@ -414,6 +415,7 @@ namespace BaseGames.Editor.UI
|
||||
private static GameObject SettingRowRoot(string name, out TMP_Text label)
|
||||
{
|
||||
var go = NewUI(name, 480, 44);
|
||||
var rowLe = go.AddComponent<LayoutElement>(); rowLe.preferredHeight = 44f; rowLe.minHeight = 40f;
|
||||
var h = go.AddComponent<HorizontalLayoutGroup>();
|
||||
h.spacing = 12; h.childAlignment = TextAnchor.MiddleLeft;
|
||||
h.childForceExpandWidth = false; h.childForceExpandHeight = false;
|
||||
@@ -539,6 +541,10 @@ namespace BaseGames.Editor.UI
|
||||
if (s_theme != null) AssignRef(uiBtn, "_theme", s_theme);
|
||||
var view = go.AddComponent<MainMenuButtonView>();
|
||||
|
||||
// LayoutElement:放入竖排布局(VerticalLayoutGroup childControlHeight)时保持固定行高,避免被压扁
|
||||
var le = go.AddComponent<LayoutElement>();
|
||||
le.preferredHeight = 64f; le.minHeight = 56f;
|
||||
|
||||
var iconGo = NewUIChild(go.transform, "Icon", out var irt);
|
||||
irt.anchorMin = new Vector2(0, 0.5f); irt.anchorMax = new Vector2(0, 0.5f); irt.pivot = new Vector2(0, 0.5f);
|
||||
irt.anchoredPosition = new Vector2(14, 0); irt.sizeDelta = new Vector2(32, 32);
|
||||
|
||||
157
Assets/_Game/Scripts/Editor/UI/UIInputPromptScaffold.cs
Normal file
157
Assets/_Game/Scripts/Editor/UI/UIInputPromptScaffold.cs
Normal file
@@ -0,0 +1,157 @@
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
using BaseGames.UI;
|
||||
using BaseGames.UI.Theme;
|
||||
|
||||
namespace BaseGames.Editor.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 输入提示控件脚手架:生成 / 放置 <c>UI_Control_InputPrompt</c> 预制件
|
||||
/// —— 一个「按键图标 + 文字」的可复用提示(如菜单底部「Ⓐ 确定」「Ⓑ 返回」)。
|
||||
///
|
||||
/// <para>图标走已有的设备适配链路(零代码):</para>
|
||||
/// 控件内 <see cref="InputIconImage"/> 据 ActionName 经 <c>IInputIconService</c> 解析
|
||||
/// <b>当前设备</b>(键鼠/PS/Xbox/Switch)+ 改键后的实际绑定 → 自动显示正确按键图标,
|
||||
/// 设备切换/改键时自动刷新。<b>前提:美术把按键 sprite 填入 ICN_Keyboard/Xbox/PlayStation/Switch 资产。</b>
|
||||
///
|
||||
/// <para>策划/美术用法:</para>
|
||||
/// 菜单「向选中节点放置 ▸ InputPrompt」放进任意 Canvas,
|
||||
/// 在 Icon 的 InputIconImage 填 ActionName(如 Interact/Jump),Label 填提示文字(可挂 LocalizedText)。
|
||||
///
|
||||
/// 菜单:BaseGames/UI/控件库/...
|
||||
/// </summary>
|
||||
public static class UIInputPromptScaffold
|
||||
{
|
||||
private const string ControlsDir = "Assets/_Game/Prefabs/UI/Controls";
|
||||
private const string ThemePath = "Assets/_Game/Data/UI/Themes/UI_Theme_Default.asset";
|
||||
private const string PfPrompt = "UI_Control_InputPrompt";
|
||||
|
||||
[MenuItem("BaseGames/UI/控件库/生成输入提示控件(UI_Control_InputPrompt)")]
|
||||
public static void GeneratePrompt()
|
||||
{
|
||||
EnsureFolder(ControlsDir);
|
||||
BuildPrompt();
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
Debug.Log($"[UIInputPrompt] 已生成 {ControlsDir}/{PfPrompt}.prefab。\n" +
|
||||
"用法:菜单「向选中节点放置 ▸ InputPrompt」放进 UI,Icon 填 ActionName、Label 填文字。\n" +
|
||||
"图标随当前设备自动切换;需美术把按键 sprite 填入 ICN_Keyboard/Xbox/PlayStation/Switch 资产。");
|
||||
}
|
||||
|
||||
[MenuItem("BaseGames/UI/控件库/向选中节点放置 ▸ InputPrompt")]
|
||||
private static void PlacePrompt()
|
||||
{
|
||||
string path = $"{ControlsDir}/{PfPrompt}.prefab";
|
||||
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(path);
|
||||
if (prefab == null)
|
||||
{
|
||||
EditorUtility.DisplayDialog("控件库",
|
||||
$"未找到预制件:{path}\n请先执行「生成输入提示控件(UI_Control_InputPrompt)」。", "确定");
|
||||
return;
|
||||
}
|
||||
|
||||
Transform parent = Selection.activeTransform;
|
||||
if (parent == null)
|
||||
{
|
||||
var canvas = Object.FindObjectOfType<Canvas>();
|
||||
parent = canvas != null ? canvas.transform : null;
|
||||
}
|
||||
|
||||
var instance = (GameObject)PrefabUtility.InstantiatePrefab(prefab, parent);
|
||||
if (instance == null) return;
|
||||
Undo.RegisterCreatedObjectUndo(instance, $"Place {PfPrompt}");
|
||||
Selection.activeGameObject = instance;
|
||||
EditorGUIUtility.PingObject(instance);
|
||||
}
|
||||
|
||||
// ── 构建 ─────────────────────────────────────────────────────────────
|
||||
private static void BuildPrompt()
|
||||
{
|
||||
var theme = AssetDatabase.LoadAssetAtPath<UIThemeSO>(ThemePath);
|
||||
|
||||
// 根:横向布局(图标 + 文字),自适应内容宽度
|
||||
var root = NewUI(PfPrompt, 120f, 44f);
|
||||
var h = root.AddComponent<HorizontalLayoutGroup>();
|
||||
h.spacing = 8f; h.childAlignment = TextAnchor.MiddleLeft;
|
||||
h.childForceExpandWidth = false; h.childForceExpandHeight = false;
|
||||
h.childControlWidth = true; h.childControlHeight = true;
|
||||
var fitter = root.AddComponent<ContentSizeFitter>();
|
||||
fitter.horizontalFit = ContentSizeFitter.FitMode.PreferredSize;
|
||||
fitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
|
||||
|
||||
// 图标:Image + InputIconImage(同物体,InputIconImage 要求 Image)
|
||||
var iconGo = NewUIChild(root.transform, "Icon", out _);
|
||||
var iconImg = iconGo.AddComponent<Image>();
|
||||
iconImg.preserveAspect = true;
|
||||
iconImg.sprite = Knob(); // 编辑期占位图;运行时由 InputIconImage 替换为当前设备按键图(无图则自动隐藏)
|
||||
var iconLe = iconGo.AddComponent<LayoutElement>();
|
||||
iconLe.preferredWidth = 40f; iconLe.preferredHeight = 40f;
|
||||
iconLe.minWidth = 32f; iconLe.minHeight = 32f;
|
||||
var iconComp = iconGo.AddComponent<InputIconImage>();
|
||||
SetEnum(iconComp, "_mode", (int)InputIconImage.LookupMode.ByActionName);
|
||||
SetString(iconComp, "_actionName", "Interact"); // 占位 ActionName,策划按需改
|
||||
|
||||
// 文字:提示标签(可挂 LocalizedText;默认占位)
|
||||
var lblGo = NewUIChild(root.transform, "Label", out _);
|
||||
var lbl = lblGo.AddComponent<TextMeshProUGUI>();
|
||||
lbl.text = "提示"; lbl.fontSize = 24f; lbl.alignment = TextAlignmentOptions.MidlineLeft;
|
||||
lbl.color = new Color(0.80f, 0.80f, 0.82f, 1f);
|
||||
lbl.raycastTarget = false;
|
||||
var lblLe = lblGo.AddComponent<LayoutElement>();
|
||||
lblLe.preferredHeight = 40f;
|
||||
SetEnum(lblGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Text_Secondary);
|
||||
|
||||
_ = theme; // 配色随就近父级 UIThemeApplier;本控件不自带 applier,避免嵌套重复应用
|
||||
|
||||
string path = $"{ControlsDir}/{PfPrompt}.prefab";
|
||||
PrefabUtility.SaveAsPrefabAsset(root, path);
|
||||
Object.DestroyImmediate(root);
|
||||
}
|
||||
|
||||
// ── 助手 ─────────────────────────────────────────────────────────────
|
||||
private static GameObject NewUI(string name, float w, float h)
|
||||
{
|
||||
var go = new GameObject(name, typeof(RectTransform));
|
||||
((RectTransform)go.transform).sizeDelta = new Vector2(w, h);
|
||||
return go;
|
||||
}
|
||||
|
||||
private static GameObject NewUIChild(Transform parent, string name, out RectTransform rt)
|
||||
{
|
||||
var go = new GameObject(name, typeof(RectTransform));
|
||||
rt = (RectTransform)go.transform;
|
||||
rt.SetParent(parent, false);
|
||||
return go;
|
||||
}
|
||||
|
||||
private static void SetEnum(Component c, string prop, int value)
|
||||
{
|
||||
var so = new SerializedObject(c);
|
||||
var p = so.FindProperty(prop);
|
||||
if (p != null) { p.enumValueIndex = value; so.ApplyModifiedPropertiesWithoutUndo(); }
|
||||
}
|
||||
|
||||
private static void SetString(Component c, string prop, string value)
|
||||
{
|
||||
var so = new SerializedObject(c);
|
||||
var p = so.FindProperty(prop);
|
||||
if (p != null) { p.stringValue = value; so.ApplyModifiedPropertiesWithoutUndo(); }
|
||||
}
|
||||
|
||||
private static Sprite Knob() => AssetDatabase.GetBuiltinExtraResource<Sprite>("UI/Skin/Knob.psd");
|
||||
|
||||
private static void EnsureFolder(string dir)
|
||||
{
|
||||
string[] parts = dir.Split('/');
|
||||
string cur = parts[0];
|
||||
for (int i = 1; i < parts.Length; i++)
|
||||
{
|
||||
string next = $"{cur}/{parts[i]}";
|
||||
if (!AssetDatabase.IsValidFolder(next)) AssetDatabase.CreateFolder(cur, parts[i]);
|
||||
cur = next;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Editor/UI/UIInputPromptScaffold.cs.meta
Normal file
11
Assets/_Game/Scripts/Editor/UI/UIInputPromptScaffold.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 67910478514f5d342b774044378cd877
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
275
Assets/_Game/Scripts/Editor/UI/UILoadingScreenScaffold.cs
Normal file
275
Assets/_Game/Scripts/Editor/UI/UILoadingScreenScaffold.cs
Normal file
@@ -0,0 +1,275 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
using BaseGames.UI;
|
||||
using BaseGames.UI.Theme;
|
||||
using BaseGames.Core.Events;
|
||||
using BaseGames.Localization;
|
||||
using BaseGames.Editor.Localization;
|
||||
|
||||
namespace BaseGames.Editor.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 加载界面脚手架:一键生成 / 更新 <c>UI_LoadingScreen</c> 预制件 + 默认 <c>UI_LoadingConfig</c> 数据表,
|
||||
/// 并补充默认中/英文案。对齐项目「Prefab(样式/美术)+ Config(数据)+ Theme(配色)」范式。
|
||||
///
|
||||
/// <para>生成后策划美术编辑方式:</para>
|
||||
/// <list type="bullet">
|
||||
/// <item>美术:双击 <c>UI_LoadingScreen.prefab</c> 进 Prefab Mode,改背景图 / 进度条 sprite / 字体 / 加装饰 / 调布局。</item>
|
||||
/// <item>策划:选 <c>UI_LoadingConfig.asset</c>,改提示文案 key、标题 key、加载时长/手感。</item>
|
||||
/// <item>配色:改 <c>UI_Theme_Default</c>(带 UIThemeRole 的元素自动套用)。</item>
|
||||
/// </list>
|
||||
///
|
||||
/// 菜单:BaseGames/UI/控件库/生成加载界面(预制件 + 默认配置)
|
||||
/// </summary>
|
||||
public static class UILoadingScreenScaffold
|
||||
{
|
||||
private const string PrefabDir = "Assets/_Game/Prefabs/UI";
|
||||
private const string ConfigDir = "Assets/_Game/Data/UI";
|
||||
private const string ThemePath = "Assets/_Game/Data/UI/Themes/UI_Theme_Default.asset";
|
||||
private const string PrefabName = "UI_LoadingScreen";
|
||||
private const string ConfigName = "UI_LoadingConfig";
|
||||
|
||||
[MenuItem("BaseGames/UI/控件库/生成加载界面(预制件 + 默认配置)")]
|
||||
public static void GeneratePrefab()
|
||||
{
|
||||
EnsureFolder(PrefabDir);
|
||||
EnsureFolder(ConfigDir);
|
||||
var report = new List<string>();
|
||||
|
||||
var theme = AssetDatabase.LoadAssetAtPath<UIThemeSO>(ThemePath);
|
||||
var config = EnsureDefaultConfig(report);
|
||||
SeedLocalization(report);
|
||||
BuildPrefab(theme, config, report);
|
||||
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
|
||||
var sb = new System.Text.StringBuilder("[UILoadingScreen] 加载界面已生成/更新:\n");
|
||||
foreach (var r in report) sb.AppendLine(" • " + r);
|
||||
sb.AppendLine("用法:重跑「BaseGames/Scene/Setup/Scaffold Persistent Scene」把预制件实例化进 Persistent。");
|
||||
sb.AppendLine("美术改样式 → 编辑 UI_LoadingScreen 预制件;策划改内容 → 编辑 UI_LoadingConfig;配色 → UI_Theme_Default。");
|
||||
Debug.Log(sb.ToString());
|
||||
}
|
||||
|
||||
// ── 预制件构建 ───────────────────────────────────────────────────────
|
||||
private static void BuildPrefab(UIThemeSO theme, LoadingScreenConfigSO config, List<string> report)
|
||||
{
|
||||
// 根:全屏 Overlay Canvas(常驻 active;LoadingScreenManager 挂此处,靠保持 active 才能订阅事件)
|
||||
var root = new GameObject(PrefabName,
|
||||
typeof(RectTransform), typeof(Canvas), typeof(CanvasScaler), typeof(GraphicRaycaster));
|
||||
var canvas = root.GetComponent<Canvas>();
|
||||
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
||||
canvas.sortingOrder = 99;
|
||||
var scaler = root.GetComponent<CanvasScaler>();
|
||||
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
||||
scaler.referenceResolution = new Vector2(1920f, 1080f);
|
||||
scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight;
|
||||
scaler.matchWidthOrHeight = 0.5f;
|
||||
var mgr = root.AddComponent<LoadingScreenManager>();
|
||||
|
||||
// LoadingRoot:铺满全屏,初始 inactive(由管理器 Show/Hide 切换)。
|
||||
// 主题应用器挂在此处:每次 Show(SetActive true)时 OnEnable 应用主题配色。
|
||||
var loadingRootGo = NewUIChild(root.transform, "LoadingRoot", out var loadingRootRt);
|
||||
Stretch(loadingRootRt);
|
||||
var applier = loadingRootGo.AddComponent<UIThemeApplier>();
|
||||
if (theme != null) AssignRef(applier, "_theme", theme);
|
||||
|
||||
// Background(铺满):美术可换 sprite,或复制多个变体作随机背景池
|
||||
var bgGo = NewUIChild(loadingRootGo.transform, "Background", out var bgRt);
|
||||
Stretch(bgRt);
|
||||
var bgImg = bgGo.AddComponent<Image>();
|
||||
bgImg.color = new Color(0.04f, 0.05f, 0.07f, 1f);
|
||||
SetEnum(bgGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Graphic_Background);
|
||||
|
||||
// ProgressBarTrack(底部居中)+ ProgressBarFill
|
||||
var trackGo = NewUIChild(loadingRootGo.transform, "ProgressBarTrack", out var trackRt);
|
||||
SetRect(trackRt, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0.5f, 0f),
|
||||
new Vector2(0f, 90f), new Vector2(760f, 14f));
|
||||
var trackImg = trackGo.AddComponent<Image>();
|
||||
trackImg.sprite = Standard(); trackImg.type = Image.Type.Sliced;
|
||||
trackImg.color = new Color(1f, 1f, 1f, 0.12f);
|
||||
trackImg.raycastTarget = false;
|
||||
|
||||
var fillGo = NewUIChild(trackGo.transform, "ProgressBarFill", out var fillRt);
|
||||
Stretch(fillRt);
|
||||
var fillImg = fillGo.AddComponent<Image>();
|
||||
fillImg.sprite = Standard();
|
||||
fillImg.type = Image.Type.Filled;
|
||||
fillImg.fillMethod = Image.FillMethod.Horizontal;
|
||||
fillImg.fillOrigin = (int)Image.OriginHorizontal.Left;
|
||||
fillImg.fillAmount = 0f;
|
||||
fillImg.color = new Color(0.85f, 0.80f, 0.55f, 1f);
|
||||
fillImg.raycastTarget = false;
|
||||
SetEnum(fillGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Graphic_Accent);
|
||||
|
||||
// Title(居中偏上)
|
||||
var titleGo = NewUIChild(loadingRootGo.transform, "Title", out var titleRt);
|
||||
SetRect(titleRt, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f),
|
||||
new Vector2(0f, 60f), new Vector2(1200f, 80f));
|
||||
var title = titleGo.AddComponent<TextMeshProUGUI>();
|
||||
title.text = "Loading"; title.alignment = TextAlignmentOptions.Center; title.fontSize = 56f;
|
||||
title.raycastTarget = false;
|
||||
SetEnum(titleGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Text_Header);
|
||||
|
||||
// TipText(进度条上方)
|
||||
var tipGo = NewUIChild(loadingRootGo.transform, "TipText", out var tipRt);
|
||||
SetRect(tipRt, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0.5f, 0f),
|
||||
new Vector2(0f, 130f), new Vector2(1200f, 48f));
|
||||
var tip = tipGo.AddComponent<TextMeshProUGUI>();
|
||||
tip.text = ""; tip.alignment = TextAlignmentOptions.Center; tip.fontSize = 28f;
|
||||
tip.raycastTarget = false;
|
||||
SetEnum(tipGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Text_Secondary);
|
||||
|
||||
// 绑定管理器视图引用 + 数据/事件(让预制件自足,掉哪都能用)
|
||||
AssignRef(mgr, "_loadingRoot", loadingRootGo);
|
||||
AssignRef(mgr, "_progressFill", fillImg);
|
||||
AssignRef(mgr, "_titleText", title);
|
||||
AssignRef(mgr, "_tipText", tip);
|
||||
AssignArray(mgr, "_backgroundArts", new Object[] { bgImg });
|
||||
AssignRef(mgr, "_config", config);
|
||||
AssignRef(mgr, "_onLoadingStarted", FindAsset<VoidEventChannelSO>("EVT_LoadingStarted"));
|
||||
AssignRef(mgr, "_onLoadingComplete", FindAsset<VoidEventChannelSO>("EVT_LoadingComplete"));
|
||||
AssignRef(mgr, "_onLoadingProgressUpdated", FindAsset<FloatEventChannelSO>("EVT_LoadingProgressUpdated"));
|
||||
|
||||
// 关键:Canvas 根保持 active,仅 LoadingRoot 初始隐藏(否则管理器被自己关闭、永不订阅事件)
|
||||
loadingRootGo.SetActive(false);
|
||||
|
||||
string path = $"{PrefabDir}/{PrefabName}.prefab";
|
||||
PrefabUtility.SaveAsPrefabAsset(root, path);
|
||||
Object.DestroyImmediate(root);
|
||||
report.Add(path);
|
||||
}
|
||||
|
||||
// ── 默认配置资产 ─────────────────────────────────────────────────────
|
||||
private static LoadingScreenConfigSO EnsureDefaultConfig(List<string> report)
|
||||
{
|
||||
string path = $"{ConfigDir}/{ConfigName}.asset";
|
||||
var cfg = AssetDatabase.LoadAssetAtPath<LoadingScreenConfigSO>(path);
|
||||
bool created = cfg == null;
|
||||
if (created)
|
||||
{
|
||||
cfg = ScriptableObject.CreateInstance<LoadingScreenConfigSO>();
|
||||
AssetDatabase.CreateAsset(cfg, path);
|
||||
}
|
||||
if (created) // 仅新建时填默认,避免覆盖策划已有编辑
|
||||
{
|
||||
var so = new SerializedObject(cfg);
|
||||
so.FindProperty("_titleKey").stringValue = "LOADING_TITLE";
|
||||
var tips = so.FindProperty("_tipKeys");
|
||||
string[] keys = { "LOADING_TIP_EXPLORE", "LOADING_TIP_SAVE", "LOADING_TIP_COMBAT" };
|
||||
tips.arraySize = keys.Length;
|
||||
for (int i = 0; i < keys.Length; i++) tips.GetArrayElementAtIndex(i).stringValue = keys[i];
|
||||
so.ApplyModifiedPropertiesWithoutUndo();
|
||||
report.Add($"{path}(默认标题/提示 key)");
|
||||
}
|
||||
return cfg;
|
||||
}
|
||||
|
||||
// ── 默认本地化文案(中/英;已存在的 key 不覆盖)─────────────────────────
|
||||
private static void SeedLocalization(List<string> report)
|
||||
{
|
||||
var zh = new Dictionary<string, string>
|
||||
{
|
||||
{ "LOADING_TITLE", "加载中…" },
|
||||
{ "LOADING_TIP_EXPLORE", "多探索每个角落,常有隐藏的道路与收集物。" },
|
||||
{ "LOADING_TIP_SAVE", "在存档点休息可保存进度并恢复状态。" },
|
||||
{ "LOADING_TIP_COMBAT", "把握闪避与格挡的时机,是战斗的关键。" },
|
||||
};
|
||||
var en = new Dictionary<string, string>
|
||||
{
|
||||
{ "LOADING_TITLE", "Loading…" },
|
||||
{ "LOADING_TIP_EXPLORE", "Explore every corner — hidden paths and collectibles await." },
|
||||
{ "LOADING_TIP_SAVE", "Rest at save points to save progress and restore yourself." },
|
||||
{ "LOADING_TIP_COMBAT", "Timing your dodge and parry is the key to combat." },
|
||||
};
|
||||
int added = MergeWriteUI(Language.ChineseSimplified, zh) + MergeWriteUI(Language.English, en);
|
||||
report.Add($"本地化新增 {added} 条(已存在的不覆盖;可用「BaseGames/Localization/表格编辑器」补译)。");
|
||||
}
|
||||
|
||||
private static int MergeWriteUI(Language lang, Dictionary<string, string> kv)
|
||||
{
|
||||
var dict = LocalizationFileIO.Read(lang, LocalizationTable.UI);
|
||||
int added = 0;
|
||||
foreach (var p in kv)
|
||||
if (!dict.ContainsKey(p.Key)) { dict[p.Key] = p.Value; added++; }
|
||||
if (added > 0) LocalizationFileIO.Write(lang, LocalizationTable.UI, dict);
|
||||
return added;
|
||||
}
|
||||
|
||||
// ── 通用助手 ─────────────────────────────────────────────────────────
|
||||
private static GameObject NewUIChild(Transform parent, string name, out RectTransform rt)
|
||||
{
|
||||
var go = new GameObject(name, typeof(RectTransform));
|
||||
rt = (RectTransform)go.transform;
|
||||
rt.SetParent(parent, false);
|
||||
return go;
|
||||
}
|
||||
|
||||
private static void Stretch(RectTransform rt)
|
||||
{
|
||||
rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.one;
|
||||
rt.offsetMin = Vector2.zero; rt.offsetMax = Vector2.zero;
|
||||
}
|
||||
|
||||
private static void SetRect(RectTransform rt, Vector2 aMin, Vector2 aMax, Vector2 pivot,
|
||||
Vector2 anchoredPos, Vector2 size)
|
||||
{
|
||||
rt.anchorMin = aMin; rt.anchorMax = aMax; rt.pivot = pivot;
|
||||
rt.anchoredPosition = anchoredPos; rt.sizeDelta = size;
|
||||
}
|
||||
|
||||
private static void SetEnum(Component c, string prop, int value)
|
||||
{
|
||||
var so = new SerializedObject(c);
|
||||
var p = so.FindProperty(prop);
|
||||
if (p != null) { p.enumValueIndex = value; so.ApplyModifiedPropertiesWithoutUndo(); }
|
||||
}
|
||||
|
||||
private static void AssignRef(Object target, string prop, Object value)
|
||||
{
|
||||
var so = new SerializedObject(target);
|
||||
var p = so.FindProperty(prop);
|
||||
if (p == null) { Debug.LogWarning($"[UILoadingScreen] 未找到属性 {target.GetType().Name}.{prop}"); return; }
|
||||
p.objectReferenceValue = value;
|
||||
so.ApplyModifiedPropertiesWithoutUndo();
|
||||
}
|
||||
|
||||
private static void AssignArray(Object target, string prop, Object[] values)
|
||||
{
|
||||
var so = new SerializedObject(target);
|
||||
var p = so.FindProperty(prop);
|
||||
if (p == null || !p.isArray) return;
|
||||
p.arraySize = values.Length;
|
||||
for (int i = 0; i < values.Length; i++) p.GetArrayElementAtIndex(i).objectReferenceValue = values[i];
|
||||
so.ApplyModifiedPropertiesWithoutUndo();
|
||||
}
|
||||
|
||||
private static T FindAsset<T>(string name) where T : Object
|
||||
{
|
||||
foreach (var guid in AssetDatabase.FindAssets($"{name} t:{typeof(T).Name}"))
|
||||
{
|
||||
var path = AssetDatabase.GUIDToAssetPath(guid);
|
||||
var asset = AssetDatabase.LoadAssetAtPath<T>(path);
|
||||
if (asset != null && asset.name == name) return asset;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Sprite Standard() => AssetDatabase.GetBuiltinExtraResource<Sprite>("UI/Skin/UISprite.psd");
|
||||
|
||||
private static void EnsureFolder(string dir)
|
||||
{
|
||||
string[] parts = dir.Split('/');
|
||||
string cur = parts[0];
|
||||
for (int i = 1; i < parts.Length; i++)
|
||||
{
|
||||
string next = $"{cur}/{parts[i]}";
|
||||
if (!AssetDatabase.IsValidFolder(next)) AssetDatabase.CreateFolder(cur, parts[i]);
|
||||
cur = next;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2513f08dc745f2a45b2209eb81f2ca05
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
265
Assets/_Game/Scripts/Editor/UI/UINewGameModeScaffold.cs
Normal file
265
Assets/_Game/Scripts/Editor/UI/UINewGameModeScaffold.cs
Normal file
@@ -0,0 +1,265 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
using BaseGames.UI.MainMenu;
|
||||
using BaseGames.UI.Theme;
|
||||
using BaseGames.Core.Events;
|
||||
using BaseGames.Localization;
|
||||
using BaseGames.Editor.Localization;
|
||||
|
||||
namespace BaseGames.Editor.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 新游戏难度选择脚手架:生成 / 更新 <c>UI_NewGameModePanel</c> 预制件 + 默认 <c>UI_NewGameModeConfig</c> 表 + 中英文案。
|
||||
/// 该面板是 MainMenu Canvas 的子面板(非独立 Canvas),由 SaveSlotController 经 ShowAsync 模态弹出。
|
||||
///
|
||||
/// 美术 → 改 UI_NewGameModePanel 预制件(暗化/对话框/标题/说明/返回样式,难度按钮样式见 UI_MainMenu_Button);
|
||||
/// 策划 → 改 UI_NewGameModeConfig(增删/重排难度、改标签/说明)。
|
||||
///
|
||||
/// 菜单:BaseGames/UI/控件库/生成新游戏难度选择(预制件 + 默认配置)
|
||||
/// </summary>
|
||||
public static class UINewGameModeScaffold
|
||||
{
|
||||
private const string PrefabDir = "Assets/_Game/Prefabs/UI";
|
||||
private const string ConfigDir = "Assets/_Game/Data/UI";
|
||||
private const string ThemePath = "Assets/_Game/Data/UI/Themes/UI_Theme_Default.asset";
|
||||
private const string PrefabName = "UI_NewGameModePanel";
|
||||
private const string ConfigName = "UI_NewGameModeConfig";
|
||||
private const string BtnPrefabPath = "Assets/_Game/Prefabs/UI/Controls/UI_MainMenu_Button.prefab";
|
||||
|
||||
[MenuItem("BaseGames/UI/控件库/生成新游戏难度选择(预制件 + 默认配置)")]
|
||||
public static void GeneratePrefab()
|
||||
{
|
||||
EnsureFolder(PrefabDir);
|
||||
EnsureFolder(ConfigDir);
|
||||
var report = new List<string>();
|
||||
|
||||
var theme = AssetDatabase.LoadAssetAtPath<UIThemeSO>(ThemePath);
|
||||
var config = EnsureDefaultConfig(report);
|
||||
SeedLocalization(report);
|
||||
BuildPrefab(theme, config, report);
|
||||
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
|
||||
var sb = new System.Text.StringBuilder("[UINewGameMode] 难度选择已生成/更新:\n");
|
||||
foreach (var r in report) sb.AppendLine(" • " + r);
|
||||
sb.AppendLine("用法:重跑「BaseGames/Scene/Setup/Scaffold MainMenu Scene」实例化进 MainMenu(接 SaveSlotController._modeSelect)。");
|
||||
Debug.Log(sb.ToString());
|
||||
}
|
||||
|
||||
private static void BuildPrefab(UIThemeSO theme, NewGameModeConfigSO config, List<string> report)
|
||||
{
|
||||
// 根:铺满全屏的面板(非 Canvas;作为 MainMenu Canvas 子节点)。初始隐藏,由导航器激活。
|
||||
var root = new GameObject(PrefabName, typeof(RectTransform), typeof(CanvasGroup));
|
||||
var rootRt = (RectTransform)root.transform;
|
||||
Stretch(rootRt);
|
||||
var ctrl = root.AddComponent<NewGameModeController>();
|
||||
var applier = root.AddComponent<UIThemeApplier>();
|
||||
if (theme != null) AssignRef(applier, "_theme", theme);
|
||||
|
||||
// 暗化遮罩(铺满,挡点击)
|
||||
var overlayGo = NewUIChild(root.transform, "Overlay", out var overlayRt);
|
||||
Stretch(overlayRt);
|
||||
var overlay = overlayGo.AddComponent<Image>();
|
||||
overlay.color = new Color(0f, 0f, 0f, 0.6f); overlay.raycastTarget = true;
|
||||
|
||||
// 对话框
|
||||
var boxGo = NewUIChild(root.transform, "DialogBox", out var boxRt);
|
||||
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, 520f));
|
||||
var box = boxGo.AddComponent<Image>();
|
||||
box.sprite = Standard(); box.type = Image.Type.Sliced;
|
||||
box.color = new Color(0.10f, 0.11f, 0.15f, 0.98f);
|
||||
SetEnum(boxGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Graphic_Background);
|
||||
|
||||
// 标题
|
||||
var titleGo = NewUIChild(boxGo.transform, "Title", out var titleRt);
|
||||
SetRect(titleRt, new Vector2(0f, 1f), new Vector2(1f, 1f), new Vector2(0.5f, 1f),
|
||||
new Vector2(0f, -50f), new Vector2(-60f, 60f));
|
||||
var title = titleGo.AddComponent<TextMeshProUGUI>();
|
||||
title.text = "选择难度"; title.alignment = TextAlignmentOptions.Center; title.fontSize = 40f;
|
||||
title.raycastTarget = false;
|
||||
var titleLoc = titleGo.AddComponent<LocalizedText>();
|
||||
SetString(titleLoc, "_key", "MODE_SELECT_TITLE");
|
||||
SetEnum(titleGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Text_Header);
|
||||
|
||||
// 难度按钮容器(竖排)
|
||||
var optionGo = NewUIChild(boxGo.transform, "OptionContainer", out var optionRt);
|
||||
SetRect(optionRt, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0.5f, 1f),
|
||||
new Vector2(0f, -130f), new Vector2(460f, 200f));
|
||||
var vlg = optionGo.AddComponent<VerticalLayoutGroup>();
|
||||
vlg.spacing = 12f; vlg.childAlignment = TextAnchor.UpperCenter;
|
||||
vlg.childControlWidth = true; vlg.childControlHeight = true;
|
||||
vlg.childForceExpandWidth = true; vlg.childForceExpandHeight = false;
|
||||
|
||||
// 说明文本(选中项时显示)
|
||||
var descGo = NewUIChild(boxGo.transform, "DescText", out var descRt);
|
||||
SetRect(descRt, new Vector2(0f, 0f), new Vector2(1f, 0f), new Vector2(0.5f, 0f),
|
||||
new Vector2(0f, 130f), new Vector2(-80f, 80f));
|
||||
var desc = descGo.AddComponent<TextMeshProUGUI>();
|
||||
desc.text = ""; desc.alignment = TextAlignmentOptions.Center; desc.fontSize = 22f;
|
||||
desc.color = new Color(0.82f, 0.6f, 0.6f, 1f); desc.raycastTarget = false; desc.enableWordWrapping = true;
|
||||
SetEnum(descGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Text_Secondary);
|
||||
|
||||
// 返回按钮
|
||||
var backGo = NewUIChild(boxGo.transform, "Btn_Back", out var backRt);
|
||||
SetRect(backRt, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0.5f, 0f),
|
||||
new Vector2(0f, 46f), new Vector2(260f, 60f));
|
||||
var backImg = backGo.AddComponent<Image>();
|
||||
backImg.sprite = Standard(); backImg.type = Image.Type.Sliced; backImg.color = new Color(1f, 1f, 1f, 0.06f);
|
||||
var backBtn = backGo.AddComponent<Button>(); backBtn.targetGraphic = backImg;
|
||||
var backLblGo = NewUIChild(backGo.transform, "Label", out var backLblRt);
|
||||
Stretch(backLblRt);
|
||||
var backLbl = backLblGo.AddComponent<TextMeshProUGUI>();
|
||||
backLbl.text = "返回"; backLbl.alignment = TextAlignmentOptions.Center; backLbl.fontSize = 28f;
|
||||
backLblGo.AddComponent<LocalizedText>();
|
||||
SetString(backLblGo.GetComponent<LocalizedText>(), "_key", "BTN_BACK");
|
||||
SetEnum(backLblGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Text_Primary);
|
||||
|
||||
// 绑定控制器
|
||||
AssignRef(ctrl, "_config", config);
|
||||
AssignRef(ctrl, "_container", optionRt);
|
||||
AssignRef(ctrl, "_canvasGroup", root.GetComponent<CanvasGroup>());
|
||||
AssignRef(ctrl, "_titleText", titleLoc);
|
||||
AssignRef(ctrl, "_descText", desc);
|
||||
AssignRef(ctrl, "_btnBack", backBtn);
|
||||
|
||||
var btnPrefab = AssetDatabase.LoadAssetAtPath<GameObject>(BtnPrefabPath);
|
||||
var btnView = btnPrefab != null ? btnPrefab.GetComponent<MainMenuButtonView>() : null;
|
||||
if (btnView == null) report.Add($"未找到按钮预制件 {BtnPrefabPath}(请先运行「生成主菜单」)。");
|
||||
AssignRef(ctrl, "_buttonPrefab", btnView);
|
||||
|
||||
root.SetActive(false); // 结果面板由导航器激活
|
||||
|
||||
string path = $"{PrefabDir}/{PrefabName}.prefab";
|
||||
PrefabUtility.SaveAsPrefabAsset(root, path);
|
||||
Object.DestroyImmediate(root);
|
||||
report.Add(path);
|
||||
}
|
||||
|
||||
private static NewGameModeConfigSO EnsureDefaultConfig(List<string> report)
|
||||
{
|
||||
string path = $"{ConfigDir}/{ConfigName}.asset";
|
||||
var cfg = AssetDatabase.LoadAssetAtPath<NewGameModeConfigSO>(path);
|
||||
bool created = cfg == null;
|
||||
if (created) { cfg = ScriptableObject.CreateInstance<NewGameModeConfigSO>(); AssetDatabase.CreateAsset(cfg, path); }
|
||||
|
||||
if (created)
|
||||
{
|
||||
var items = new (DifficultyLevel lvl, string label, string desc)[]
|
||||
{
|
||||
(DifficultyLevel.Normal, "MODE_NORMAL", ""),
|
||||
(DifficultyLevel.SteelSoul, "MODE_STEELSOUL", "MODE_STEELSOUL_DESC"),
|
||||
};
|
||||
var so = new SerializedObject(cfg);
|
||||
var prop = so.FindProperty("_items");
|
||||
prop.arraySize = items.Length;
|
||||
for (int i = 0; i < items.Length; i++)
|
||||
{
|
||||
var el = prop.GetArrayElementAtIndex(i);
|
||||
el.FindPropertyRelative("level").enumValueIndex = (int)items[i].lvl;
|
||||
el.FindPropertyRelative("labelKey").stringValue = items[i].label;
|
||||
el.FindPropertyRelative("descKey").stringValue = items[i].desc;
|
||||
}
|
||||
so.ApplyModifiedPropertiesWithoutUndo();
|
||||
report.Add($"{path}(默认 {items.Length} 项:普通 / 钢铁之魂)");
|
||||
}
|
||||
return cfg;
|
||||
}
|
||||
|
||||
private static void SeedLocalization(List<string> report)
|
||||
{
|
||||
var zh = new Dictionary<string, string>
|
||||
{
|
||||
{ "MODE_SELECT_TITLE", "选择难度" },
|
||||
{ "MODE_NORMAL", "普通" },
|
||||
{ "MODE_STEELSOUL", "钢铁之魂" },
|
||||
{ "MODE_STEELSOUL_DESC", "一命模式:死亡即清空存档,请谨慎选择。" },
|
||||
{ "BTN_BACK", "返回" },
|
||||
};
|
||||
var en = new Dictionary<string, string>
|
||||
{
|
||||
{ "MODE_SELECT_TITLE", "Select Mode" },
|
||||
{ "MODE_NORMAL", "Normal" },
|
||||
{ "MODE_STEELSOUL", "Steel Soul" },
|
||||
{ "MODE_STEELSOUL_DESC", "One life. Death wipes the save — choose with care." },
|
||||
{ "BTN_BACK", "Back" },
|
||||
};
|
||||
int added = MergeWriteUI(Language.ChineseSimplified, zh) + MergeWriteUI(Language.English, en);
|
||||
report.Add($"本地化新增 {added} 条(已存在不覆盖)。");
|
||||
}
|
||||
|
||||
private static int MergeWriteUI(Language lang, Dictionary<string, string> kv)
|
||||
{
|
||||
var dict = LocalizationFileIO.Read(lang, LocalizationTable.UI);
|
||||
int added = 0;
|
||||
foreach (var p in kv)
|
||||
if (!dict.ContainsKey(p.Key)) { dict[p.Key] = p.Value; added++; }
|
||||
if (added > 0) LocalizationFileIO.Write(lang, LocalizationTable.UI, dict);
|
||||
return added;
|
||||
}
|
||||
|
||||
// ── 助手 ─────────────────────────────────────────────────────────────
|
||||
private static GameObject NewUIChild(Transform parent, string name, out RectTransform rt)
|
||||
{
|
||||
var go = new GameObject(name, typeof(RectTransform));
|
||||
rt = (RectTransform)go.transform;
|
||||
rt.SetParent(parent, false);
|
||||
return go;
|
||||
}
|
||||
|
||||
private static void Stretch(RectTransform rt)
|
||||
{
|
||||
rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.one;
|
||||
rt.offsetMin = Vector2.zero; rt.offsetMax = Vector2.zero;
|
||||
}
|
||||
|
||||
private static void SetRect(RectTransform rt, Vector2 aMin, Vector2 aMax, Vector2 pivot,
|
||||
Vector2 anchoredPos, Vector2 size)
|
||||
{
|
||||
rt.anchorMin = aMin; rt.anchorMax = aMax; rt.pivot = pivot;
|
||||
rt.anchoredPosition = anchoredPos; rt.sizeDelta = size;
|
||||
}
|
||||
|
||||
private static void SetEnum(Component c, string prop, int value)
|
||||
{
|
||||
var so = new SerializedObject(c);
|
||||
var p = so.FindProperty(prop);
|
||||
if (p != null) { p.enumValueIndex = value; so.ApplyModifiedPropertiesWithoutUndo(); }
|
||||
}
|
||||
|
||||
private static void SetString(Component c, string prop, string value)
|
||||
{
|
||||
if (c == null) return;
|
||||
var so = new SerializedObject(c);
|
||||
var p = so.FindProperty(prop);
|
||||
if (p != null) { p.stringValue = value; so.ApplyModifiedPropertiesWithoutUndo(); }
|
||||
}
|
||||
|
||||
private static void AssignRef(Object target, string prop, Object value)
|
||||
{
|
||||
var so = new SerializedObject(target);
|
||||
var p = so.FindProperty(prop);
|
||||
if (p == null) { Debug.LogWarning($"[UINewGameMode] 未找到属性 {target.GetType().Name}.{prop}"); return; }
|
||||
p.objectReferenceValue = value;
|
||||
so.ApplyModifiedPropertiesWithoutUndo();
|
||||
}
|
||||
|
||||
private static Sprite Standard() => AssetDatabase.GetBuiltinExtraResource<Sprite>("UI/Skin/UISprite.psd");
|
||||
|
||||
private static void EnsureFolder(string dir)
|
||||
{
|
||||
string[] parts = dir.Split('/');
|
||||
string cur = parts[0];
|
||||
for (int i = 1; i < parts.Length; i++)
|
||||
{
|
||||
string next = $"{cur}/{parts[i]}";
|
||||
if (!AssetDatabase.IsValidFolder(next)) AssetDatabase.CreateFolder(cur, parts[i]);
|
||||
cur = next;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Editor/UI/UINewGameModeScaffold.cs.meta
Normal file
11
Assets/_Game/Scripts/Editor/UI/UINewGameModeScaffold.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: df82a9fb6a00ca740badcfaefc083560
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
255
Assets/_Game/Scripts/Editor/UI/UIPauseScreenScaffold.cs
Normal file
255
Assets/_Game/Scripts/Editor/UI/UIPauseScreenScaffold.cs
Normal file
@@ -0,0 +1,255 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
using BaseGames.UI;
|
||||
using BaseGames.UI.MainMenu;
|
||||
using BaseGames.UI.Theme;
|
||||
using BaseGames.Core.Events;
|
||||
using BaseGames.Localization;
|
||||
using BaseGames.Editor.Localization;
|
||||
|
||||
namespace BaseGames.Editor.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 暂停界面脚手架:生成 / 更新 <c>UI_PauseScreen</c> 预制件 + 默认 <c>UI_PauseMenuConfig</c> 表 + 中英文案。
|
||||
/// 对齐「Prefab(样式)+ Config(数据)+ Theme(配色)」范式。
|
||||
///
|
||||
/// <para>编辑方式:</para>
|
||||
/// 美术 → 双击 <c>UI_PauseScreen.prefab</c> 改暗化背景/标题/布局/按钮样式(按钮样式见 UI_MainMenu_Button);
|
||||
/// 策划 → 选 <c>UI_PauseMenuConfig.asset</c> 增删/重排/改标签/改动作。
|
||||
///
|
||||
/// 菜单:BaseGames/UI/控件库/生成暂停界面(预制件 + 默认配置)
|
||||
/// </summary>
|
||||
public static class UIPauseScreenScaffold
|
||||
{
|
||||
private const string PrefabDir = "Assets/_Game/Prefabs/UI";
|
||||
private const string ControlsDir = "Assets/_Game/Prefabs/UI/Controls";
|
||||
private const string ConfigDir = "Assets/_Game/Data/UI";
|
||||
private const string ThemePath = "Assets/_Game/Data/UI/Themes/UI_Theme_Default.asset";
|
||||
private const string PrefabName = "UI_PauseScreen";
|
||||
private const string ConfigName = "UI_PauseMenuConfig";
|
||||
private const string BtnPrefabPath = "Assets/_Game/Prefabs/UI/Controls/UI_MainMenu_Button.prefab";
|
||||
|
||||
[MenuItem("BaseGames/UI/控件库/生成暂停界面(预制件 + 默认配置)")]
|
||||
public static void GeneratePrefab()
|
||||
{
|
||||
EnsureFolder(PrefabDir);
|
||||
EnsureFolder(ConfigDir);
|
||||
var report = new List<string>();
|
||||
|
||||
var theme = AssetDatabase.LoadAssetAtPath<UIThemeSO>(ThemePath);
|
||||
var config = EnsureDefaultConfig(report);
|
||||
SeedLocalization(report);
|
||||
BuildPrefab(theme, config, report);
|
||||
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
|
||||
var sb = new System.Text.StringBuilder("[UIPauseScreen] 暂停界面已生成/更新:\n");
|
||||
foreach (var r in report) sb.AppendLine(" • " + r);
|
||||
sb.AppendLine("用法:重跑「BaseGames/Scene/Setup/Scaffold Persistent Scene」实例化进 Persistent(作为 PanelId.Pause 面板)。");
|
||||
Debug.Log(sb.ToString());
|
||||
}
|
||||
|
||||
// ── 预制件构建 ───────────────────────────────────────────────────────
|
||||
private static void BuildPrefab(UIThemeSO theme, PauseMenuConfigSO config, List<string> report)
|
||||
{
|
||||
// 根:全屏 Overlay Canvas(sortOrder 50:HUD 之上、加载/Splash 之下)
|
||||
var root = new GameObject(PrefabName,
|
||||
typeof(RectTransform), typeof(Canvas), typeof(CanvasScaler), typeof(GraphicRaycaster), typeof(CanvasGroup));
|
||||
var canvas = root.GetComponent<Canvas>();
|
||||
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
||||
canvas.sortingOrder = 50;
|
||||
var scaler = root.GetComponent<CanvasScaler>();
|
||||
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
||||
scaler.referenceResolution = new Vector2(1920f, 1080f);
|
||||
scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight;
|
||||
scaler.matchWidthOrHeight = 0.5f;
|
||||
var ctrl = root.AddComponent<DataDrivenPauseMenuController>();
|
||||
|
||||
// 暗化背景(铺满,挡住下层点击)
|
||||
var dimGo = NewUIChild(root.transform, "Dim", out var dimRt);
|
||||
Stretch(dimRt);
|
||||
var dimImg = dimGo.AddComponent<Image>();
|
||||
dimImg.color = new Color(0f, 0f, 0f, 0.72f);
|
||||
dimImg.raycastTarget = true;
|
||||
|
||||
// 主题应用器挂根(每次 OnEnable 应用配色到带 UIThemeRole 的子节点)
|
||||
var applier = root.AddComponent<UIThemeApplier>();
|
||||
if (theme != null) AssignRef(applier, "_theme", theme);
|
||||
|
||||
// 标题
|
||||
var titleGo = NewUIChild(root.transform, "Title", out var titleRt);
|
||||
SetRect(titleRt, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f),
|
||||
new Vector2(0f, 220f), new Vector2(800f, 90f));
|
||||
var title = titleGo.AddComponent<TextMeshProUGUI>();
|
||||
title.text = "暂停"; title.alignment = TextAlignmentOptions.Center; title.fontSize = 64f;
|
||||
title.raycastTarget = false;
|
||||
titleGo.AddComponent<LocalizedText>(); // key 由脚手架下面绑 PAUSE_TITLE
|
||||
SetString(titleGo.GetComponent<LocalizedText>(), "_key", "PAUSE_TITLE");
|
||||
SetEnum(titleGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Text_Header);
|
||||
|
||||
// 按钮容器(居中竖排;运行时据表生成按钮)
|
||||
var menuGo = NewUIChild(root.transform, "MenuPanel", out var menuRt);
|
||||
SetRect(menuRt, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f),
|
||||
new Vector2(0f, -40f), new Vector2(520f, 360f));
|
||||
var vlg = menuGo.AddComponent<VerticalLayoutGroup>();
|
||||
vlg.spacing = 12f; vlg.childAlignment = TextAnchor.MiddleCenter;
|
||||
vlg.childControlWidth = true; vlg.childControlHeight = true;
|
||||
vlg.childForceExpandWidth = true; vlg.childForceExpandHeight = false;
|
||||
|
||||
// 绑定控制器
|
||||
AssignRef(ctrl, "_config", config);
|
||||
AssignRef(ctrl, "_container", menuRt);
|
||||
AssignRef(ctrl, "_canvasGroup", root.GetComponent<CanvasGroup>());
|
||||
|
||||
var btnPrefab = AssetDatabase.LoadAssetAtPath<GameObject>(BtnPrefabPath);
|
||||
var btnView = btnPrefab != null ? btnPrefab.GetComponent<MainMenuButtonView>() : null;
|
||||
if (btnView == null) report.Add($"未找到按钮预制件 {BtnPrefabPath}(请先运行「生成主菜单」)。");
|
||||
AssignRef(ctrl, "_buttonPrefab", btnView);
|
||||
|
||||
// resume 频道与 GameManager 同源:实际资产名 EVT_PauseResumed(EVT_ResumeRequested 不存在)
|
||||
AssignRef(ctrl, "_onResumeRequested", FindAsset<VoidEventChannelSO>("EVT_PauseResumed")
|
||||
?? FindAsset<VoidEventChannelSO>("EVT_ResumeRequested"));
|
||||
AssignRef(ctrl, "_onSceneLoadRequest", FindAsset<SceneLoadRequestEventChannelSO>("EVT_SceneLoadRequest"));
|
||||
|
||||
string path = $"{PrefabDir}/{PrefabName}.prefab";
|
||||
PrefabUtility.SaveAsPrefabAsset(root, path);
|
||||
Object.DestroyImmediate(root);
|
||||
report.Add(path);
|
||||
}
|
||||
|
||||
// ── 默认配置 ─────────────────────────────────────────────────────────
|
||||
private static PauseMenuConfigSO EnsureDefaultConfig(List<string> report)
|
||||
{
|
||||
string path = $"{ConfigDir}/{ConfigName}.asset";
|
||||
var cfg = AssetDatabase.LoadAssetAtPath<PauseMenuConfigSO>(path);
|
||||
bool created = cfg == null;
|
||||
if (created) { cfg = ScriptableObject.CreateInstance<PauseMenuConfigSO>(); AssetDatabase.CreateAsset(cfg, path); }
|
||||
|
||||
if (created)
|
||||
{
|
||||
var items = new (string key, PauseMenuAction a)[]
|
||||
{
|
||||
("PAUSE_RESUME", PauseMenuAction.Resume),
|
||||
("MENU_SETTINGS", PauseMenuAction.OpenSettings),
|
||||
("PAUSE_MAIN_MENU", PauseMenuAction.ReturnToMainMenu),
|
||||
("MENU_QUIT", PauseMenuAction.Quit),
|
||||
};
|
||||
var so = new SerializedObject(cfg);
|
||||
var prop = so.FindProperty("_items");
|
||||
prop.arraySize = items.Length;
|
||||
for (int i = 0; i < items.Length; i++)
|
||||
{
|
||||
var el = prop.GetArrayElementAtIndex(i);
|
||||
el.FindPropertyRelative("labelKey").stringValue = items[i].key;
|
||||
el.FindPropertyRelative("action").enumValueIndex = (int)items[i].a;
|
||||
el.FindPropertyRelative("sceneKey").stringValue = "";
|
||||
}
|
||||
so.ApplyModifiedPropertiesWithoutUndo();
|
||||
report.Add($"{path}(默认 {items.Length} 项)");
|
||||
}
|
||||
return cfg;
|
||||
}
|
||||
|
||||
// ── 默认本地化(已存在的 key 不覆盖)────────────────────────────────────
|
||||
private static void SeedLocalization(List<string> report)
|
||||
{
|
||||
var zh = new Dictionary<string, string>
|
||||
{
|
||||
{ "PAUSE_TITLE", "暂停" },
|
||||
{ "PAUSE_RESUME", "继续" },
|
||||
{ "PAUSE_MAIN_MENU", "返回主菜单" },
|
||||
};
|
||||
var en = new Dictionary<string, string>
|
||||
{
|
||||
{ "PAUSE_TITLE", "Paused" },
|
||||
{ "PAUSE_RESUME", "Resume" },
|
||||
{ "PAUSE_MAIN_MENU", "Return to Title" },
|
||||
};
|
||||
int added = MergeWriteUI(Language.ChineseSimplified, zh) + MergeWriteUI(Language.English, en);
|
||||
report.Add($"本地化新增 {added} 条(已存在不覆盖;MENU_SETTINGS/MENU_QUIT 复用主菜单译文)。");
|
||||
}
|
||||
|
||||
private static int MergeWriteUI(Language lang, Dictionary<string, string> kv)
|
||||
{
|
||||
var dict = LocalizationFileIO.Read(lang, LocalizationTable.UI);
|
||||
int added = 0;
|
||||
foreach (var p in kv)
|
||||
if (!dict.ContainsKey(p.Key)) { dict[p.Key] = p.Value; added++; }
|
||||
if (added > 0) LocalizationFileIO.Write(lang, LocalizationTable.UI, dict);
|
||||
return added;
|
||||
}
|
||||
|
||||
// ── 助手 ─────────────────────────────────────────────────────────────
|
||||
private static GameObject NewUIChild(Transform parent, string name, out RectTransform rt)
|
||||
{
|
||||
var go = new GameObject(name, typeof(RectTransform));
|
||||
rt = (RectTransform)go.transform;
|
||||
rt.SetParent(parent, false);
|
||||
return go;
|
||||
}
|
||||
|
||||
private static void Stretch(RectTransform rt)
|
||||
{
|
||||
rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.one;
|
||||
rt.offsetMin = Vector2.zero; rt.offsetMax = Vector2.zero;
|
||||
}
|
||||
|
||||
private static void SetRect(RectTransform rt, Vector2 aMin, Vector2 aMax, Vector2 pivot,
|
||||
Vector2 anchoredPos, Vector2 size)
|
||||
{
|
||||
rt.anchorMin = aMin; rt.anchorMax = aMax; rt.pivot = pivot;
|
||||
rt.anchoredPosition = anchoredPos; rt.sizeDelta = size;
|
||||
}
|
||||
|
||||
private static void SetEnum(Component c, string prop, int value)
|
||||
{
|
||||
var so = new SerializedObject(c);
|
||||
var p = so.FindProperty(prop);
|
||||
if (p != null) { p.enumValueIndex = value; so.ApplyModifiedPropertiesWithoutUndo(); }
|
||||
}
|
||||
|
||||
private static void SetString(Component c, string prop, string value)
|
||||
{
|
||||
if (c == null) return;
|
||||
var so = new SerializedObject(c);
|
||||
var p = so.FindProperty(prop);
|
||||
if (p != null) { p.stringValue = value; so.ApplyModifiedPropertiesWithoutUndo(); }
|
||||
}
|
||||
|
||||
private static void AssignRef(Object target, string prop, Object value)
|
||||
{
|
||||
var so = new SerializedObject(target);
|
||||
var p = so.FindProperty(prop);
|
||||
if (p == null) { Debug.LogWarning($"[UIPauseScreen] 未找到属性 {target.GetType().Name}.{prop}"); return; }
|
||||
p.objectReferenceValue = value;
|
||||
so.ApplyModifiedPropertiesWithoutUndo();
|
||||
}
|
||||
|
||||
private static T FindAsset<T>(string name) where T : Object
|
||||
{
|
||||
foreach (var guid in AssetDatabase.FindAssets($"{name} t:{typeof(T).Name}"))
|
||||
{
|
||||
var path = AssetDatabase.GUIDToAssetPath(guid);
|
||||
var asset = AssetDatabase.LoadAssetAtPath<T>(path);
|
||||
if (asset != null && asset.name == name) return asset;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void EnsureFolder(string dir)
|
||||
{
|
||||
string[] parts = dir.Split('/');
|
||||
string cur = parts[0];
|
||||
for (int i = 1; i < parts.Length; i++)
|
||||
{
|
||||
string next = $"{cur}/{parts[i]}";
|
||||
if (!AssetDatabase.IsValidFolder(next)) AssetDatabase.CreateFolder(cur, parts[i]);
|
||||
cur = next;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Editor/UI/UIPauseScreenScaffold.cs.meta
Normal file
11
Assets/_Game/Scripts/Editor/UI/UIPauseScreenScaffold.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8d74eff677fd2a04ab74c05b8cced069
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user