1598 lines
98 KiB
C#
1598 lines
98 KiB
C#
using System.Collections.Generic;
|
||
using BaseGames.Audio;
|
||
using BaseGames.Camera;
|
||
using BaseGames.Core;
|
||
using BaseGames.Core.Events;
|
||
using BaseGames.Core.Save;
|
||
using BaseGames.Core.Pool;
|
||
using BaseGames.Input;
|
||
using BaseGames.UI;
|
||
using BaseGames.UI.HUD;
|
||
using BaseGames.UI.MainMenu;
|
||
using BaseGames.UI.Menus;
|
||
using BaseGames.UI.Splash;
|
||
using BaseGames.World;
|
||
using BaseGames.World.Map;
|
||
using BaseGames.World.Streaming;
|
||
using PathBerserker2d;
|
||
using Unity.Cinemachine;
|
||
using UnityEditor;
|
||
using UnityEditor.SceneManagement;
|
||
using UnityEngine;
|
||
using UnityEngine.SceneManagement;
|
||
using UnityEngine.Tilemaps;
|
||
using UnityEngine.UI;
|
||
using BaseGames.Feedback;
|
||
using MoreMountains.Feedbacks;
|
||
using TMPro;
|
||
|
||
namespace BaseGames.Editor
|
||
{
|
||
public static class SceneScaffoldTools
|
||
{
|
||
|
||
[MenuItem("BaseGames/Scene/Setup/Scaffold Persistent Scene")]
|
||
public static void ScaffoldPersistentScene()
|
||
{
|
||
var report = new List<string>();
|
||
|
||
EnsureEventChannelAssets(report);
|
||
|
||
GameObject root = GetOrCreateRoot("[Persistent]");
|
||
Transform services = GetOrCreateChild(root.transform, "[Services]");
|
||
Transform input = GetOrCreateChild(root.transform, "[Input]");
|
||
Transform camera = GetOrCreateChild(root.transform, "[Camera]");
|
||
Transform ui = GetOrCreateChild(root.transform, "[UI]");
|
||
|
||
GameObject registrarGo = GetOrCreateChild(services, "GameServiceRegistrar").gameObject;
|
||
GameObject deathRespawnGo = GetOrCreateChild(services, "DeathRespawnService").gameObject;
|
||
GameObject sceneServiceGo = GetOrCreateChild(services, "SceneService").gameObject;
|
||
GameObject sceneLoaderGo = GetOrCreateChild(services, "SceneLoader").gameObject;
|
||
GameObject registryGo = GetOrCreateChild(services, "EventChannelRegistry").gameObject;
|
||
GameObject settingsGo = GetOrCreateChild(services, "SettingsManager").gameObject;
|
||
GameObject poolGo = GetOrCreateChild(services, "GlobalObjectPool").gameObject;
|
||
GameObject gameManagerGo = GetOrCreateChild(services, "GameManager").gameObject;
|
||
GameObject audioManagerGo = GetOrCreateChild(services, "AudioManager").gameObject;
|
||
GameObject saveManagerGo = GetOrCreateChild(services, "GameSaveManager").gameObject;
|
||
GameObject checkpointServiceGo = GetOrCreateChild(services, "CheckpointService").gameObject;
|
||
|
||
GameServiceRegistrar registrar = GetOrAddComponent<GameServiceRegistrar>(registrarGo);
|
||
DeathRespawnService deathRespawnService = GetOrAddComponent<DeathRespawnService>(deathRespawnGo);
|
||
SceneService sceneService = GetOrAddComponent<SceneService>(sceneServiceGo);
|
||
SceneLoader sceneLoader = GetOrAddComponent<SceneLoader>(sceneLoaderGo);
|
||
EventChannelRegistry registry = GetOrAddComponent<EventChannelRegistry>(registryGo);
|
||
SettingsManager settingsManager = GetOrAddComponent<SettingsManager>(settingsGo);
|
||
GetOrAddComponent<GlobalObjectPool>(poolGo);
|
||
GameManager gameManager = GetOrAddComponent<GameManager>(gameManagerGo);
|
||
AudioManager audioManager = GetOrAddComponent<AudioManager>(audioManagerGo);
|
||
GameSaveManager gameSaveManager = GetOrAddComponent<GameSaveManager>(saveManagerGo);
|
||
CheckpointService checkpointService = GetOrAddComponent<CheckpointService>(checkpointServiceGo);
|
||
|
||
GameObject inputHolderGo = GetOrCreateChild(input, "InputReaderHolder").gameObject;
|
||
Object inputReaderAsset = FindFirstAssetByType<InputReaderSO>("InputReader", "InputReaderSO");
|
||
if (inputReaderAsset == null)
|
||
inputReaderAsset = EnsureInputReaderAsset(report);
|
||
|
||
InputReaderBootstrap inputBootstrap = GetOrAddComponent<InputReaderBootstrap>(inputHolderGo);
|
||
AssignReference(inputBootstrap, "_inputReader", inputReaderAsset, report);
|
||
// 输入模式由游戏状态驱动(Gameplay/BossFight→游戏输入,其余→UI 输入):绑定状态变化频道
|
||
AssignAsset(inputBootstrap, "_onGameStateChanged", report, true, "EVT_GameStateChanged", "EVT_GameState");
|
||
if (inputReaderAsset != null)
|
||
{
|
||
AssignReference(inputReaderAsset, "_onPauseRequested", FindFirstAssetByType<VoidEventChannelSO>("EVT_PauseRequested"), report);
|
||
AssignReference(inputReaderAsset, "_inputActions", FindFirstAssetWithExtension(".inputactions", "PlayerInputActions", "InputActions"), report);
|
||
}
|
||
if (inputReaderAsset == null)
|
||
report.Add("未找到 InputReaderSO 资产,InputReaderBootstrap 将保持空引用。请补齐 Assets/_Game/Data/Player/Input/InputReader.asset。");
|
||
|
||
GameObject mainCameraGo = GetOrCreateChild(camera, "Main Camera").gameObject;
|
||
UnityEngine.Camera mainCamera = GetOrAddComponent<UnityEngine.Camera>(mainCameraGo);
|
||
mainCamera.orthographic = false;
|
||
mainCamera.fieldOfView = 60f;
|
||
// 2D 游戏使用纯色清除(非 Skybox),避免背景层缝隙处露出 skybox/黑色;深蓝灰与场景雾色协调
|
||
mainCamera.clearFlags = UnityEngine.CameraClearFlags.SolidColor;
|
||
mainCamera.backgroundColor = new Color(0.192f, 0.302f, 0.475f, 1f);
|
||
mainCameraGo.tag = "MainCamera";
|
||
AudioListener mainCameraAudioListener = GetOrAddComponent<AudioListener>(mainCameraGo);
|
||
CinemachineBrain brain = GetOrAddComponent<CinemachineBrain>(mainCameraGo);
|
||
|
||
GameObject cameraStateGo = GetOrCreateChild(camera, "CameraStateController").gameObject;
|
||
CameraStateController cameraStateController = GetOrAddComponent<CameraStateController>(cameraStateGo);
|
||
CinemachineImpulseSource impulseSource = GetOrAddComponent<CinemachineImpulseSource>(cameraStateGo);
|
||
|
||
// 垂直窥视系统:独立节点,CameraStateController 持引用
|
||
GameObject lookSystemGo = GetOrCreateChild(camera, "CameraLookSystem").gameObject;
|
||
CameraLookSystem lookSystem = GetOrAddComponent<CameraLookSystem>(lookSystemGo);
|
||
|
||
GameObject vcamAGo = GetOrCreateChild(camera, "VCamA").gameObject;
|
||
CinemachineCamera vcamA = GetOrAddComponent<CinemachineCamera>(vcamAGo);
|
||
GetOrAddComponent<CinemachineConfiner3D>(vcamAGo);
|
||
GetOrAddComponent<CameraAxisLockExtension>(vcamAGo);
|
||
GetOrAddComponent<CameraAsymmetricDampingExtension>(vcamAGo);
|
||
GetOrAddComponent<CameraAdaptiveLookaheadExtension>(vcamAGo);
|
||
// CinemachinePositionComposer:Body 阶段组件,必须存在;ConfigureSlot 依赖它写入所有相机跟随参数
|
||
var composerA = GetOrAddComponent<CinemachinePositionComposer>(vcamAGo);
|
||
ApplyComposerDefaults(composerA);
|
||
|
||
GameObject vcamBGo = GetOrCreateChild(camera, "VCamB").gameObject;
|
||
CinemachineCamera vcamB = GetOrAddComponent<CinemachineCamera>(vcamBGo);
|
||
GetOrAddComponent<CinemachineConfiner3D>(vcamBGo);
|
||
GetOrAddComponent<CameraAxisLockExtension>(vcamBGo);
|
||
GetOrAddComponent<CameraAsymmetricDampingExtension>(vcamBGo);
|
||
GetOrAddComponent<CameraAdaptiveLookaheadExtension>(vcamBGo);
|
||
var composerB = GetOrAddComponent<CinemachinePositionComposer>(vcamBGo);
|
||
ApplyComposerDefaults(composerB);
|
||
|
||
GameObject uiRootGo = GetOrCreateChild(ui, "UIRoot").gameObject;
|
||
UIManager uiManager = GetOrAddComponent<UIManager>(uiRootGo);
|
||
|
||
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);
|
||
GameObject settingsRootGo = GetOrCreateChild(uiRootGo.transform, "SettingsRoot").gameObject;
|
||
GameObject mapRootGo = GetOrCreateChild(uiRootGo.transform, "MapRoot").gameObject;
|
||
GameObject shopRootGo = GetOrCreateChild(uiRootGo.transform, "ShopRoot").gameObject;
|
||
pauseRootGo.SetActive(false);
|
||
settingsRootGo.SetActive(false);
|
||
mapRootGo.SetActive(false);
|
||
shopRootGo.SetActive(false);
|
||
|
||
GameObject deathCanvasGo = GetOrCreateCanvas(uiRootGo.transform, "DeathScreen Canvas", 10);
|
||
GameObject deathRootGo = GetOrCreateChild(deathCanvasGo.transform, "DeathScreenRoot").gameObject;
|
||
DeathScreenController deathScreenController = GetOrAddComponent<DeathScreenController>(deathRootGo);
|
||
deathRootGo.SetActive(false);
|
||
GameObject respawnButtonGo = GetOrCreateChild(deathRootGo.transform, "RespawnButton").gameObject;
|
||
GetOrAddComponent<Image>(respawnButtonGo);
|
||
Button respawnButton = GetOrAddComponent<Button>(respawnButtonGo);
|
||
GameObject deathMessageGo = GetOrCreateChild(deathRootGo.transform, "DeathMessage").gameObject;
|
||
TextMeshProUGUI deathMessage = GetOrAddComponent<TextMeshProUGUI>(deathMessageGo);
|
||
|
||
// ── BootSequencer(启动流程)──────────────────────────────────────
|
||
GameObject bootSequencerGo = GetOrCreateChild(services, "BootSequencer").gameObject;
|
||
BootSequencer bootSequencer = GetOrAddComponent<BootSequencer>(bootSequencerGo);
|
||
AssignAsset(bootSequencer, "_onSplashStartRequest", report, false, "EVT_SplashStartRequest");
|
||
AssignAsset(bootSequencer, "_onSplashComplete", report, false, "EVT_SplashComplete");
|
||
AssignAsset(bootSequencer, "_onPreloadProgress", report, false, "EVT_LoadingProgressUpdated");
|
||
|
||
AssignReference(gameManager, "_bootSequencer", bootSequencer);
|
||
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");
|
||
AssignAsset(sceneLoader, "_onLoadingProgressUpdated", report, false, "EVT_LoadingProgressUpdated");
|
||
|
||
// ── Canvas_Splash(启动演出)──────────────────────────────────────
|
||
GameObject splashCanvasGo = GetOrCreateCanvas(ui.transform, "Canvas_Splash", 100);
|
||
SplashScreenController splashCtrl = GetOrAddComponent<SplashScreenController>(splashCanvasGo);
|
||
CanvasGroup splashRootGroup = GetOrAddComponent<CanvasGroup>(splashCanvasGo);
|
||
AssignReference(splashCtrl, "_splashRoot", splashRootGroup);
|
||
GameObject studioLogoGo = GetOrCreateChild(splashCanvasGo.transform, "StudioLogo").gameObject;
|
||
CanvasGroup studioLogoGroup = GetOrAddComponent<CanvasGroup>(studioLogoGo);
|
||
AssignReference(splashCtrl, "_studioLogoGroup", studioLogoGroup);
|
||
GameObject gameTitleGo = GetOrCreateChild(splashCanvasGo.transform, "GameTitle").gameObject;
|
||
CanvasGroup gameTitleGroup = GetOrAddComponent<CanvasGroup>(gameTitleGo);
|
||
AssignReference(splashCtrl, "_gameTitleGroup", gameTitleGroup);
|
||
AssignAsset(splashCtrl, "_onSplashStartRequest", report, false, "EVT_SplashStartRequest");
|
||
AssignAsset(splashCtrl, "_onSplashComplete", report, false, "EVT_SplashComplete");
|
||
|
||
// ── LoadingScreenManager(加载遮罩)──────────────────────────────
|
||
GameObject loadingCanvasGo = GetOrCreateCanvas(ui.transform, "Canvas_Loading", 99);
|
||
LoadingScreenManager loadingMgr = GetOrAddComponent<LoadingScreenManager>(loadingCanvasGo);
|
||
GameObject loadingRootGo = GetOrCreateChild(loadingCanvasGo.transform, "LoadingRoot").gameObject;
|
||
AssignReference(loadingMgr, "_loadingRoot", loadingRootGo);
|
||
GameObject progressFillGo = GetOrCreateChild(loadingRootGo.transform, "ProgressBarFill").gameObject;
|
||
Image progressFillImg = GetOrAddComponent<Image>(progressFillGo);
|
||
progressFillImg.type = Image.Type.Filled;
|
||
progressFillImg.fillMethod = Image.FillMethod.Horizontal;
|
||
AssignReference(loadingMgr, "_progressFill", progressFillImg);
|
||
GameObject tipTextGo = GetOrCreateChild(loadingRootGo.transform, "TipText").gameObject;
|
||
TextMeshProUGUI tipText = GetOrAddComponent<TextMeshProUGUI>(tipTextGo);
|
||
AssignReference(loadingMgr, "_tipText", tipText);
|
||
AssignAsset(loadingMgr, "_onLoadingStarted", report, false, "EVT_LoadingStarted");
|
||
AssignAsset(loadingMgr, "_onLoadingComplete", report, false, "EVT_LoadingComplete");
|
||
AssignAsset(loadingMgr, "_onLoadingProgressUpdated", report, false, "EVT_LoadingProgressUpdated");
|
||
loadingCanvasGo.SetActive(false);
|
||
|
||
// ── SceneFadeController(场景切换黑幕)───────────────────────────
|
||
// 纯逻辑节点:监听淡出/淡入事件后调用 SceneFeedback.Play()。
|
||
// 实际 UI 效果完全由 SceneFeedback 内部的 MMF_Player 负责。
|
||
GameObject fadeCtrGo = GetOrCreateChild(ui.transform, "SYS_SceneFade").gameObject;
|
||
SceneFadeController fadeCtr = GetOrAddComponent<SceneFadeController>(fadeCtrGo);
|
||
GameObject fadeOutGo = GetOrCreateChild(fadeCtrGo.transform, "FeedbackFadeOut").gameObject;
|
||
MMF_Player fadeOutPlayer = GetOrAddComponent<MMF_Player>(fadeOutGo);
|
||
SceneFeedback fadeOutFeedback = GetOrAddComponent<SceneFeedback>(fadeOutGo);
|
||
AssignReference(fadeOutFeedback, "_player", fadeOutPlayer);
|
||
AssignReference(fadeCtr, "_fadeOut", fadeOutFeedback);
|
||
GameObject fadeInGo = GetOrCreateChild(fadeCtrGo.transform, "FeedbackFadeIn").gameObject;
|
||
MMF_Player fadeInPlayer = GetOrAddComponent<MMF_Player>(fadeInGo);
|
||
SceneFeedback fadeInFeedback = GetOrAddComponent<SceneFeedback>(fadeInGo);
|
||
AssignReference(fadeInFeedback, "_player", fadeInPlayer);
|
||
AssignReference(fadeCtr, "_fadeIn", fadeInFeedback);
|
||
AssignAsset(fadeCtr, "_onFadeOutRequest", report, false, "EVT_FadeOutRequest");
|
||
AssignAsset(fadeCtr, "_onFadeInRequest", report, false, "EVT_FadeInRequest");
|
||
|
||
report.Add("SYS_SceneFade:SceneFeedback 子节点已创建并绑定。请在 FeedbackFadeOut / FeedbackFadeIn 的 MMF_Player 中配置所需效果(如全屏黑幕淡入淡出)。" +
|
||
"MMF_Player 总时长应 ≤ SceneService._sceneFadeDuration(默认 0.4 s)。");
|
||
|
||
EnsureAudioSources(audioManagerGo, audioManager, report);
|
||
|
||
AssignReference(registrar, "_deathRespawnService", deathRespawnService);
|
||
AssignReference(registrar, "_sceneService", sceneService);
|
||
AssignReference(registrar, "_eventChannelRegistry", registry);
|
||
AssignReference(registrar, "_saveManager", gameSaveManager);
|
||
AssignReference(registrar, "_checkpointService", checkpointService);
|
||
AssignReference(registrar, "_primaryListener", mainCameraAudioListener);
|
||
|
||
AssignReference(gameManager, "_settingsManager", settingsManager);
|
||
AssignAsset(gameManager, "_onPlayerDied", report, true, "EVT_PlayerDied");
|
||
AssignAsset(gameManager, "_onPauseRequested", report, false, "EVT_PauseRequested");
|
||
AssignAsset(gameManager, "_onResumeRequested", report, false, "EVT_ResumeRequested", "EVT_PauseResumed");
|
||
AssignAsset(gameManager, "_onBossFightStarted", report, false, "EVT_BossFightStarted", "EVT_BossFight");
|
||
AssignAsset(gameManager, "_onBossFightEnded", report, false, "EVT_BossFightEnded");
|
||
AssignAsset(gameManager, "_onDeathScreenConfirmed", report, true, "EVT_DeathScreenConfirmed");
|
||
AssignAsset(gameManager, "_onGameStateChanged", report, true, "EVT_GameStateChanged", "EVT_GameState");
|
||
AssignAsset(gameManager, "_onPlayerRespawned", report, false, "EVT_PlayerRespawned", "EVT_PlayerRespawn");
|
||
|
||
AssignAsset(sceneService, "_onSceneLoadRequest", report, false, "EVT_SceneLoadRequest");
|
||
AssignAsset(sceneService, "_onFadeInRequest", report, false, "EVT_FadeInRequest");
|
||
AssignAsset(sceneService, "_onFadeOutRequest", report, false, "EVT_FadeOutRequest");
|
||
// 场景加载完毕、世界状态恢复后触发;场景物体据此应用存档状态,淡入前保证画面正确
|
||
AssignAsset(sceneService, "_onSceneWorldStateRestored", report, true, "EVT_SceneWorldStateRestored");
|
||
AssignReference(sceneService, "_sceneLoader", sceneLoader);
|
||
|
||
AssignAsset(sceneLoader, "_onSceneLoaded", report, false, "EVT_SceneLoaded");
|
||
|
||
AssignAsset(deathRespawnService, "_onRespawnStarted", report, false, "EVT_RespawnStarted");
|
||
AssignAsset(deathRespawnService, "_onRespawnCompleted", report, false, "EVT_RespawnCompleted");
|
||
AssignAsset(deathRespawnService, "_onDeathScreenConfirmed", report, true, "EVT_DeathScreenConfirmed");
|
||
AssignAsset(deathRespawnService, "_onSceneLoadRequest", report, false, "EVT_SceneLoadRequest");
|
||
|
||
AssignAsset(settingsManager, "_defaultSettings", report, false, "SET_GlobalSettings");
|
||
|
||
AssignAsset(checkpointService, "_onSceneLoaded", report, false, "EVT_SceneLoaded");
|
||
|
||
AssignAsset(audioManager, "_onPlayerDied", report, false, "EVT_PlayerDied");
|
||
|
||
AssignReference(cameraStateController, "_brain", brain);
|
||
AssignReference(cameraStateController, "_impulseSource", impulseSource);
|
||
AssignReference(cameraStateController, "_lookSystem", lookSystem);
|
||
AssignAsset(cameraStateController, "_onPlayerSpawned", report, true, "EVT_PlayerSpawned");
|
||
AssignAsset(cameraStateController, "_lensConfig", report, false, "CAM_LensConfig", "LensConfig", "CameraLensConfig");
|
||
|
||
AssignReference(uiManager, "_hudRoot", hudRootGo);
|
||
AssignReference(uiManager, "_deathScreenRoot", deathRootGo);
|
||
AssignReference(uiManager, "_addressablePanelParent", uiRootGo.transform);
|
||
{
|
||
// UIManager uses _panels (PanelRegistration[]) — NOT individual _pauseMenuRoot/_settingsRoot etc.
|
||
var so = new SerializedObject(uiManager);
|
||
var panelsProp = so.FindProperty("_panels");
|
||
panelsProp.arraySize = 4;
|
||
var p0 = panelsProp.GetArrayElementAtIndex(0);
|
||
p0.FindPropertyRelative("id").intValue = (int)PanelId.Pause;
|
||
p0.FindPropertyRelative("root").objectReferenceValue = pauseRootGo;
|
||
var p1 = panelsProp.GetArrayElementAtIndex(1);
|
||
p1.FindPropertyRelative("id").intValue = (int)PanelId.Settings;
|
||
p1.FindPropertyRelative("root").objectReferenceValue = settingsRootGo;
|
||
var p2 = panelsProp.GetArrayElementAtIndex(2);
|
||
p2.FindPropertyRelative("id").intValue = (int)PanelId.Map;
|
||
p2.FindPropertyRelative("root").objectReferenceValue = mapRootGo;
|
||
var p3 = panelsProp.GetArrayElementAtIndex(3);
|
||
p3.FindPropertyRelative("id").intValue = (int)PanelId.Shop;
|
||
p3.FindPropertyRelative("root").objectReferenceValue = shopRootGo;
|
||
so.ApplyModifiedPropertiesWithoutUndo();
|
||
}
|
||
AssignAsset(uiManager, "_onGameStateChanged", report, true, "EVT_GameStateChanged", "EVT_GameState");
|
||
AssignAsset(uiManager, "_onPauseRequested", report, false, "EVT_PauseRequested");
|
||
AssignAsset(uiManager, "_onFastTravelOpen", report, false, "EVT_FastTravelOpen");
|
||
AssignAsset(uiManager, "_onShopOpen", report, false, "EVT_ShopOpen");
|
||
AssignAsset(uiManager, "_onMapOpen", report, false, "EVT_MapOpen");
|
||
AssignAsset(uiManager, "_onCharmPanelOpen", report, false, "EVT_CharmPanelOpen");
|
||
AssignAsset(uiManager, "_onSpellSelectOpen", report, false, "EVT_SpellSelectOpen");
|
||
|
||
AssignReference(deathScreenController, "_btnRespawn", respawnButton);
|
||
AssignReference(deathScreenController, "_deathMessage", deathMessage);
|
||
AssignAsset(deathScreenController, "_onDeathScreenConfirmed", report, true, "EVT_DeathScreenConfirmed");
|
||
|
||
AssignReference(pauseMenuCtrl, "_btnResume", pauseBtnResume);
|
||
AssignReference(pauseMenuCtrl, "_btnSettings", pauseBtnSettings);
|
||
AssignReference(pauseMenuCtrl, "_btnMainMenu", pauseBtnMainMenu);
|
||
AssignReference(pauseMenuCtrl, "_btnQuit", pauseBtnQuit);
|
||
AssignAsset(pauseMenuCtrl, "_onResumeRequested", report, false, "EVT_ResumeRequested");
|
||
AssignAsset(pauseMenuCtrl, "_onSceneLoadRequest", report, false, "EVT_SceneLoadRequest");
|
||
|
||
AddScaffoldNote(hudRootGo, "HUDController 已挂载。其内部图片/文本/图标 Prefab 依赖较多,需后续手工补 UI 资源与事件频道。", report);
|
||
|
||
// ── 流式加载系统 ──────────────────────────────────────────────────
|
||
ScaffoldStreamingSystem(services, report);
|
||
|
||
// ── 多设备 UI 焦点守护(EventSystem 上挂 UISelectionRestorer)──────────
|
||
EnsureUISelectionRestorer(report);
|
||
|
||
MarkDirtyAndLog("Persistent 场景脚手架", root, report);
|
||
}
|
||
|
||
/// <summary>确保场景中的 EventSystem 挂有 <see cref="BaseGames.UI.UISelectionRestorer"/>(多设备焦点守护,幂等)。</summary>
|
||
private static void EnsureUISelectionRestorer(List<string> report)
|
||
{
|
||
var es = Object.FindObjectOfType<UnityEngine.EventSystems.EventSystem>();
|
||
if (es == null)
|
||
{
|
||
report.Add("未找到 EventSystem,UISelectionRestorer 未挂载(键盘/手柄丢失焦点后无法自动恢复)。请确认 Persistent 场景含 EventSystem + InputSystemUIInputModule。");
|
||
return;
|
||
}
|
||
if (es.GetComponent<BaseGames.UI.UISelectionRestorer>() == null)
|
||
Undo.AddComponent<BaseGames.UI.UISelectionRestorer>(es.gameObject);
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
// Scaffold Main Menu Scene
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// 在当前活动场景中生成完整的主菜单场景层级结构:
|
||
/// [MainMenu] → [Canvas_MainMenu] → 主按钮组 / SaveSlotPanel / SettingsPanel / CreditsPanel
|
||
/// 自动绑定所有已存在的相关事件频道 SO 资产。
|
||
/// </summary>
|
||
[MenuItem("BaseGames/Scene/Setup/Scaffold Main Menu Scene", priority = 202)]
|
||
public static void ScaffoldMainMenuScene()
|
||
{
|
||
var report = new List<string>();
|
||
|
||
EnsureEventChannelAssets(report);
|
||
|
||
// ── [MainMenu] 根节点 ─────────────────────────────────────────────
|
||
GameObject root = GetOrCreateRoot("[MainMenu]");
|
||
|
||
// ── Canvas_MainMenu(排序层 10,显示在 HUD 之上)────────────────
|
||
GameObject canvasGo = GetOrCreateCanvas(root.transform, "Canvas_MainMenu", 10);
|
||
|
||
// ── 全屏暗色背景(幽暗基调)────────────────────────────────
|
||
GetOrCreateImage(canvasGo.transform, "Background", new Color(0.05f, 0.06f, 0.09f, 1f), false)
|
||
.transform.SetAsFirstSibling();
|
||
|
||
// ── 标题 ──────────────────────────────────────────────────────────
|
||
var titleRt = GetOrCreateUIChild(canvasGo.transform, "TitleText");
|
||
SetRect(titleRt, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0.5f, 1f),
|
||
new Vector2(0f, -150f), new Vector2(1400f, 180f));
|
||
var titleTmp = GetOrAddComponent<TextMeshProUGUI>(titleRt.gameObject);
|
||
titleTmp.text = "ZELING"; titleTmp.fontSize = 130f; titleTmp.fontStyle = FontStyles.Bold;
|
||
titleTmp.alignment = TextAlignmentOptions.Center; titleTmp.color = GoldText; titleTmp.raycastTarget = false;
|
||
titleTmp.characterSpacing = 14f;
|
||
|
||
var subtitleRt = GetOrCreateUIChild(canvasGo.transform, "SubtitleText");
|
||
SetRect(subtitleRt, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0.5f, 1f),
|
||
new Vector2(0f, -300f), new Vector2(1000f, 60f));
|
||
var subTmp = GetOrAddComponent<TextMeshProUGUI>(subtitleRt.gameObject);
|
||
subTmp.text = "A 2D Action Adventure"; subTmp.fontSize = 40f; subTmp.alignment = TextAlignmentOptions.Center;
|
||
subTmp.color = new Color(0.7f, 0.66f, 0.55f, 0.9f); subTmp.raycastTarget = false; subTmp.characterSpacing = 8f;
|
||
BindLocalizedText(subtitleRt.gameObject, "MENU_SUBTITLE");
|
||
|
||
// ── 主菜单控制器 ──────────────────────────────────────────────────
|
||
MainMenuController menuCtrl = GetOrAddComponent<MainMenuController>(canvasGo);
|
||
AssignAsset(menuCtrl, "_onGameStateChanged", report, false, "EVT_GameStateChanged", "EVT_GameState");
|
||
AssignAsset(menuCtrl, "_onSceneLoadRequest", report, false, "EVT_SceneLoadRequest");
|
||
AssignAsset(menuCtrl, "_onSlotConfirmed", report, false, "EVT_SlotConfirmed");
|
||
|
||
// ── 主按钮区域(底部居中竖排,带 CanvasGroup 供入场动画)─────────────
|
||
var menuPanelRt = GetOrCreateUIChild(canvasGo.transform, "MenuPanel");
|
||
SetRect(menuPanelRt, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0.5f, 0f),
|
||
new Vector2(0f, 170f), new Vector2(560f, 470f));
|
||
GameObject menuPanelGo = menuPanelRt.gameObject;
|
||
var menuGroup = GetOrAddComponent<CanvasGroup>(menuPanelGo);
|
||
var menuVlg = GetOrAddComponent<VerticalLayoutGroup>(menuPanelGo);
|
||
menuVlg.spacing = 12f; menuVlg.childAlignment = TextAnchor.MiddleCenter;
|
||
menuVlg.childControlWidth = true; menuVlg.childControlHeight = true;
|
||
menuVlg.childForceExpandWidth = true; menuVlg.childForceExpandHeight = false;
|
||
|
||
GameObject btnNewGameGo = GetOrCreateButtonChild(menuPanelGo.transform, "Btn_NewGame", "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");
|
||
|
||
AssignReference(menuCtrl, "_btnNewGame", btnNewGameGo.GetComponent<Button>());
|
||
AssignReference(menuCtrl, "_btnContinue", btnContinueGo.GetComponent<Button>());
|
||
AssignReference(menuCtrl, "_btnSettings", btnSettingsGo.GetComponent<Button>());
|
||
AssignReference(menuCtrl, "_btnCredits", btnCreditsGo.GetComponent<Button>());
|
||
AssignReference(menuCtrl, "_btnQuit", btnQuitGo.GetComponent<Button>());
|
||
AssignReference(menuCtrl, "_mainButtonsGroup", menuGroup);
|
||
AssignReference(menuCtrl, "_mainButtonsRect", menuPanelRt);
|
||
|
||
// ── SaveSlotPanel(全屏模态:半透明遮罩 + 竖排 3 卡片)──────────────
|
||
var saveSlotPanelRt = GetOrCreateUIChild(canvasGo.transform, "SaveSlotPanel");
|
||
StretchFull(saveSlotPanelRt);
|
||
GameObject saveSlotPanelGo = saveSlotPanelRt.gameObject;
|
||
saveSlotPanelGo.SetActive(false);
|
||
SaveSlotController saveSlotCtrl = GetOrAddComponent<SaveSlotController>(saveSlotPanelGo);
|
||
AssignAsset(saveSlotCtrl, "_onSlotConfirmed", report, false, "EVT_SlotConfirmed");
|
||
AssignReference(menuCtrl, "_saveSlotPanel", saveSlotPanelGo);
|
||
AssignReference(menuCtrl, "_saveSlotController", saveSlotCtrl);
|
||
|
||
// 近乎不透明的遮罩(拦截背后点击,并遮住主菜单避免文字透出)
|
||
GetOrCreateImage(saveSlotPanelGo.transform, "Overlay", new Color(0.04f, 0.05f, 0.08f, 0.97f), true)
|
||
.transform.SetAsFirstSibling();
|
||
|
||
// 面板标题
|
||
var slotTitleRt = GetOrCreateUIChild(saveSlotPanelGo.transform, "PanelTitle");
|
||
SetRect(slotTitleRt, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0.5f, 1f),
|
||
new Vector2(0f, -70f), new Vector2(900f, 80f));
|
||
var slotTitleTmp = GetOrAddComponent<TextMeshProUGUI>(slotTitleRt.gameObject);
|
||
slotTitleTmp.text = "Select Save"; slotTitleTmp.fontSize = 56f; slotTitleTmp.fontStyle = FontStyles.Bold;
|
||
slotTitleTmp.alignment = TextAlignmentOptions.Center; slotTitleTmp.color = GoldText; slotTitleTmp.raycastTarget = false;
|
||
BindLocalizedText(slotTitleRt.gameObject, "SAVESLOT_TITLE");
|
||
|
||
// 卡片容器(居中竖排)
|
||
var slotsContainerRt = GetOrCreateUIChild(saveSlotPanelGo.transform, "SlotsContainer");
|
||
SetRect(slotsContainerRt, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f),
|
||
new Vector2(0f, -10f), new Vector2(960f, 660f));
|
||
var slotsVlg = GetOrAddComponent<VerticalLayoutGroup>(slotsContainerRt.gameObject);
|
||
slotsVlg.spacing = 22f; slotsVlg.childAlignment = TextAnchor.MiddleCenter;
|
||
slotsVlg.childControlWidth = true; slotsVlg.childControlHeight = true;
|
||
slotsVlg.childForceExpandWidth = true; slotsVlg.childForceExpandHeight = false;
|
||
|
||
// ── 存档槽卡片 Slot_0/1/2(挂 SaveSlotUI,绑定到 _slotUIs)─────────────
|
||
var regionRegistry = FindFirstAssetByType<BaseGames.World.Map.RegionRegistrySO>("RegionRegistry");
|
||
var slotUIs = new SaveSlotUI[3];
|
||
for (int i = 0; i < 3; i++)
|
||
slotUIs[i] = BuildSaveSlotCard(slotsContainerRt, i, regionRegistry);
|
||
|
||
// _slotUIs 数组与默认聚焦按钮
|
||
var saveSlotSO = new UnityEditor.SerializedObject(saveSlotCtrl);
|
||
var slotUIsProp = saveSlotSO.FindProperty("_slotUIs");
|
||
slotUIsProp.arraySize = 3;
|
||
for (int i = 0; i < 3; i++)
|
||
slotUIsProp.GetArrayElementAtIndex(i).objectReferenceValue = slotUIs[i];
|
||
saveSlotSO.ApplyModifiedProperties();
|
||
AssignReference(saveSlotCtrl, "_defaultFocusButton",
|
||
slotUIs[0].transform.Find("SelectButton")?.GetComponent<Button>());
|
||
if (regionRegistry == null)
|
||
report.Add("未找到 RegionRegistry 资产,SaveSlotUI._regionRegistry 未绑定(存档槽背景图失效)。先运行 BaseGames/Setup/Create Project Assets。");
|
||
|
||
// 返回按钮(关闭存档槽面板 → 绑定 MainMenuController._btnCloseSaveSlot)
|
||
GameObject slotBackGo = GetOrCreateButtonChild(saveSlotPanelGo.transform, "BackButton", "Back");
|
||
var slotBackRt = (RectTransform)slotBackGo.transform;
|
||
SetRect(slotBackRt, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0.5f, 0f),
|
||
new Vector2(0f, 70f), new Vector2(260f, 64f));
|
||
StyleAsTextButton(slotBackGo, 30f);
|
||
BindLocalizedButton(slotBackGo, "BTN_BACK");
|
||
AssignReference(menuCtrl, "_btnCloseSaveSlot", slotBackGo.GetComponent<Button>());
|
||
|
||
// ── ConfirmDialog(覆盖 / 删除确认)─────────────────────
|
||
ConfirmDialogController confirmCtrl = BuildConfirmDialog(saveSlotPanelGo.transform);
|
||
AssignReference(saveSlotCtrl, "_confirmDialog", confirmCtrl);
|
||
|
||
// ── NewGameMode(新游戏模式选择:普通 / 钢铁之魂)────────────────────
|
||
NewGameModeController modeCtrl = BuildNewGameMode(saveSlotPanelGo.transform);
|
||
AssignReference(saveSlotCtrl, "_modeSelect", modeCtrl);
|
||
|
||
// ── SettingsPanel(音量 / 画面 / 可访问性 / 语言)──────────────────
|
||
var settingsPanelRt = GetOrCreateUIChild(canvasGo.transform, "SettingsPanel");
|
||
StretchFull(settingsPanelRt);
|
||
BuildSettingsPanel(settingsPanelRt.gameObject, menuCtrl, report);
|
||
settingsPanelRt.gameObject.SetActive(false);
|
||
AssignReference(menuCtrl, "_settingsPanel", settingsPanelRt.gameObject);
|
||
|
||
// ── CreditsPanel(制作团队)────────────────────────────────────────
|
||
var creditsPanelRt = GetOrCreateUIChild(canvasGo.transform, "CreditsPanel");
|
||
StretchFull(creditsPanelRt);
|
||
BuildCreditsPanel(creditsPanelRt.gameObject, menuCtrl, report);
|
||
creditsPanelRt.gameObject.SetActive(false);
|
||
AssignReference(menuCtrl, "_creditsPanel", creditsPanelRt.gameObject);
|
||
|
||
report.Add("设置 MainMenuController._firstGameSceneKey 为第一个游戏场景的 Addressable Key(字符串)。");
|
||
report.Add("存档槽卡片已含完整布局与文本(区域 / 时长 / 时间 / 灵珠 / 生命 / 钢魂徽章),空槽显示\"开始新游戏\"提示。");
|
||
report.Add("ConfirmDialog / NewGameMode 已作为 SaveSlotPanel 子节点生成并接线;需补本地化键:"
|
||
+ "CONFIRM_OVERWRITE_TITLE / CONFIRM_OVERWRITE_BODY / CONFIRM_DELETE_TITLE / CONFIRM_DELETE_BODY / MODE_STEELSOUL_DESC(UI 表)。");
|
||
|
||
MarkDirtyAndLog("Main Menu 场景脚手架", root, report);
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
// Main Menu — 子结构构建器
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// 构建单张存档槽卡片(含背景框 / 全覆盖选择按钮 / 空槽提示 / 有档信息区 / 删除按钮),
|
||
/// 并完成 SaveSlotUI 字段绑定。卡片由父容器的 VerticalLayoutGroup 排版,高度经 LayoutElement 固定。
|
||
/// </summary>
|
||
private static SaveSlotUI BuildSaveSlotCard(Transform parent, int index, Object regionRegistry)
|
||
{
|
||
var cardRt = GetOrCreateUIChild(parent, $"Slot_{index}");
|
||
GameObject slotGo = cardRt.gameObject;
|
||
var cardLe = GetOrAddComponent<LayoutElement>(slotGo);
|
||
cardLe.preferredHeight = 180f; cardLe.minHeight = 160f;
|
||
SaveSlotUI slotUI = GetOrAddComponent<SaveSlotUI>(slotGo);
|
||
|
||
// 卡片框底(半透明深色,作为按钮 targetGraphic 的视觉基底)
|
||
var frameImg = GetOrCreateImage(slotGo.transform, "Frame", new Color(0.12f, 0.13f, 0.18f, 0.92f), false);
|
||
frameImg.transform.SetAsFirstSibling();
|
||
|
||
// 区域背景图(默认隐藏,由 SaveSlotUI.RefreshBackground 控制)
|
||
var bgImg = GetOrCreateImage(slotGo.transform, "Background", Color.white, false);
|
||
bgImg.type = Image.Type.Simple; bgImg.preserveAspect = true; bgImg.enabled = false;
|
||
bgImg.transform.SetSiblingIndex(1);
|
||
|
||
// 选中/悬停高亮叠加(基色白不透明;显隐由按钮 ColorBlock 的 alpha 控制 → 可见的选中反馈)
|
||
var hlImg = GetOrCreateImage(slotGo.transform, "Highlight", Color.white, false);
|
||
hlImg.transform.SetSiblingIndex(2);
|
||
|
||
// 全覆盖选择按钮(透明,靠 raycast 接收点击;着色目标为 Highlight 叠加图)
|
||
GameObject selectGo = GetOrCreateButtonChild(slotGo.transform, "SelectButton", "");
|
||
StretchFull((RectTransform)selectGo.transform);
|
||
var selImg = selectGo.GetComponent<Image>();
|
||
if (selImg != null) selImg.color = new Color(1f, 1f, 1f, 0f);
|
||
var selLabel = GetButtonLabel(selectGo);
|
||
if (selLabel != null) selLabel.gameObject.SetActive(false);
|
||
var selBtn = selectGo.GetComponent<Button>();
|
||
if (selBtn != null)
|
||
{
|
||
selBtn.targetGraphic = hlImg;
|
||
var cc = selBtn.colors;
|
||
cc.normalColor = new Color(1f, 0.85f, 0.45f, 0f); // 静态:透明
|
||
cc.highlightedColor = new Color(1f, 0.86f, 0.5f, 0.08f); // 悬停:极淡金
|
||
cc.selectedColor = new Color(1f, 0.86f, 0.5f, 0.13f); // 键盘/手柄选中:淡金叠加
|
||
cc.pressedColor = new Color(1f, 0.86f, 0.5f, 0.22f);
|
||
cc.disabledColor = new Color(1f, 1f, 1f, 0f);
|
||
cc.colorMultiplier = 1f; cc.fadeDuration = 0.1f;
|
||
selBtn.colors = cc;
|
||
}
|
||
|
||
// 空槽提示
|
||
var emptyRt = GetOrCreateUIChild(slotGo.transform, "EmptyIndicator");
|
||
StretchFull(emptyRt);
|
||
GameObject emptyGo = emptyRt.gameObject;
|
||
var emptyText = GetOrCreateText(emptyGo.transform, "EmptyText", "Empty Slot · New Game", 34f,
|
||
new Color(0.7f, 0.66f, 0.55f, 0.85f), TextAlignmentOptions.Center);
|
||
BindLocalizedText(emptyText.gameObject, "SAVESLOT_EMPTY");
|
||
|
||
// 有档信息区(左侧竖排:区域 / 时长 / 时间)+ 右侧(灵珠 / 生命 / 钢魂)
|
||
var dataRt = GetOrCreateUIChild(slotGo.transform, "DataIndicator");
|
||
StretchFull(dataRt, 28f);
|
||
GameObject dataGo = dataRt.gameObject;
|
||
|
||
var regionText = GetOrCreateText(dataGo.transform, "RegionText", "Region", 38f, GoldText, TextAlignmentOptions.TopLeft);
|
||
SetRect((RectTransform)regionText.transform, new Vector2(0f, 1f), new Vector2(0.7f, 1f), new Vector2(0f, 1f), new Vector2(0f, -4f), new Vector2(0f, 48f));
|
||
var playtimeText = GetOrCreateText(dataGo.transform, "PlaytimeText", "00:00:00", 26f, new Color(0.8f,0.78f,0.7f,1f), TextAlignmentOptions.TopLeft);
|
||
SetRect((RectTransform)playtimeText.transform, new Vector2(0f, 1f), new Vector2(0.7f, 1f), new Vector2(0f, 1f), new Vector2(0f, -58f), new Vector2(0f, 34f));
|
||
var lastSavedText= GetOrCreateText(dataGo.transform, "LastSavedText", "—", 22f, new Color(0.6f,0.58f,0.52f,1f), TextAlignmentOptions.TopLeft);
|
||
SetRect((RectTransform)lastSavedText.transform, new Vector2(0f, 1f), new Vector2(0.7f, 1f), new Vector2(0f, 1f), new Vector2(0f, -98f), new Vector2(0f, 30f));
|
||
|
||
var lingZhuText = GetOrCreateText(dataGo.transform, "LingZhuText", "0", 28f, new Color(0.85f,0.8f,0.55f,1f), TextAlignmentOptions.TopRight);
|
||
SetRect((RectTransform)lingZhuText.transform, new Vector2(0.7f, 1f), new Vector2(1f, 1f), new Vector2(1f, 1f), new Vector2(0f, -4f), new Vector2(0f, 40f));
|
||
var hpText = GetOrCreateText(dataGo.transform, "HPText", "0", 28f, new Color(0.85f,0.5f,0.5f,1f), TextAlignmentOptions.TopRight);
|
||
SetRect((RectTransform)hpText.transform, new Vector2(0.7f, 1f), new Vector2(1f, 1f), new Vector2(1f, 1f), new Vector2(0f, -48f), new Vector2(0f, 40f));
|
||
|
||
var badgeRt = GetOrCreateUIChild(dataGo.transform, "SteelSoulBadge");
|
||
SetRect(badgeRt, new Vector2(1f, 0f), new Vector2(1f, 0f), new Vector2(1f, 0f), new Vector2(0f, 4f), new Vector2(120f, 40f));
|
||
GetOrAddComponent<Image>(badgeRt.gameObject).color = new Color(0.5f, 0.55f, 0.6f, 0.5f);
|
||
var badgeText = GetOrCreateText(badgeRt.transform, "BadgeText", "STEEL", 22f, new Color(0.85f,0.9f,1f,1f), TextAlignmentOptions.Center);
|
||
BindLocalizedText(badgeText.gameObject, "BADGE_STEELSOUL");
|
||
GameObject badgeGo = badgeRt.gameObject;
|
||
|
||
// 删除按钮(右上角小 ×)
|
||
GameObject deleteGo = GetOrCreateButtonChild(slotGo.transform, "DeleteButton", "×");
|
||
SetRect((RectTransform)deleteGo.transform, new Vector2(1f, 1f), new Vector2(1f, 1f), new Vector2(1f, 1f), new Vector2(-10f, -10f), new Vector2(48f, 48f));
|
||
var delImg = deleteGo.GetComponent<Image>();
|
||
if (delImg != null) delImg.color = new Color(0.4f, 0.12f, 0.12f, 0.7f);
|
||
var delLabel = GetButtonLabel(deleteGo);
|
||
if (delLabel != null) { delLabel.fontSize = 32f; delLabel.color = new Color(1f, 0.8f, 0.8f, 1f); }
|
||
|
||
// 绑定 SaveSlotUI 字段
|
||
AssignReference(slotUI, "_emptyIndicator", emptyGo);
|
||
AssignReference(slotUI, "_dataIndicator", dataGo);
|
||
AssignReference(slotUI, "_selectButton", selectGo.GetComponent<Button>());
|
||
AssignReference(slotUI, "_deleteButton", deleteGo.GetComponent<Button>());
|
||
AssignReference(slotUI, "_backgroundImage", bgImg);
|
||
AssignReference(slotUI, "_regionText", regionText);
|
||
AssignReference(slotUI, "_playtimeText", playtimeText);
|
||
AssignReference(slotUI, "_lastSavedText", lastSavedText);
|
||
AssignReference(slotUI, "_lingZhuText", lingZhuText);
|
||
AssignReference(slotUI, "_hpText", hpText);
|
||
AssignReference(slotUI, "_steelSoulBadge", badgeGo);
|
||
if (regionRegistry != null)
|
||
AssignReference(slotUI, "_regionRegistry", regionRegistry);
|
||
|
||
// 初始隐藏数据层(运行时由 Refresh 控制;编辑器下让空槽提示可见)
|
||
emptyGo.SetActive(true);
|
||
dataGo.SetActive(false);
|
||
return slotUI;
|
||
}
|
||
|
||
/// <summary>构建通用确认对话框(居中模态:遮罩 + 对话框 + 标题 / 正文 / 确认 / 取消),返回控制器。</summary>
|
||
private static ConfirmDialogController BuildConfirmDialog(Transform parent)
|
||
{
|
||
var rootRt = GetOrCreateUIChild(parent, "ConfirmDialog");
|
||
StretchFull(rootRt);
|
||
GameObject confirmGo = rootRt.gameObject;
|
||
confirmGo.SetActive(false);
|
||
ConfirmDialogController confirmCtrl = GetOrAddComponent<ConfirmDialogController>(confirmGo);
|
||
|
||
GetOrCreateImage(confirmGo.transform, "Overlay", new Color(0f, 0f, 0f, 0.6f), true).transform.SetAsFirstSibling();
|
||
|
||
var boxRt = GetOrCreateUIChild(confirmGo.transform, "DialogBox");
|
||
SetRect(boxRt, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(720f, 380f));
|
||
GetOrAddComponent<Image>(boxRt.gameObject).color = new Color(0.10f, 0.11f, 0.15f, 0.98f);
|
||
|
||
var titleTmp = GetOrCreateText(boxRt.transform, "TitleText", "Confirm", 40f, GoldText, TextAlignmentOptions.Center);
|
||
SetRect((RectTransform)titleTmp.transform, new Vector2(0f,1f), new Vector2(1f,1f), new Vector2(0.5f,1f), new Vector2(0f,-50f), new Vector2(-60f,60f));
|
||
var bodyTmp = GetOrCreateText(boxRt.transform, "BodyText", "Are you sure?", 28f, new Color(0.82f,0.8f,0.74f,1f), TextAlignmentOptions.Center);
|
||
SetRect((RectTransform)bodyTmp.transform, new Vector2(0f,0.5f), new Vector2(1f,0.5f), new Vector2(0.5f,0.5f), new Vector2(0f,10f), new Vector2(-80f,120f));
|
||
|
||
GameObject yesGo = GetOrCreateButtonChild(boxRt.transform, "Btn_Confirm", "Confirm");
|
||
SetRect((RectTransform)yesGo.transform, new Vector2(0.5f,0f), new Vector2(0.5f,0f), new Vector2(0.5f,0f), new Vector2(-150f,40f), new Vector2(220f,64f));
|
||
yesGo.GetComponent<Image>().color = new Color(0.45f, 0.12f, 0.12f, 0.85f);
|
||
GameObject noGo = GetOrCreateButtonChild(boxRt.transform, "Btn_Cancel", "Cancel");
|
||
SetRect((RectTransform)noGo.transform, new Vector2(0.5f,0f), new Vector2(0.5f,0f), new Vector2(0.5f,0f), new Vector2(150f,40f), new Vector2(220f,64f));
|
||
BindLocalizedButton(yesGo, "CONFIRM_YES");
|
||
BindLocalizedButton(noGo, "CONFIRM_NO");
|
||
|
||
AssignReference(confirmCtrl, "_root", confirmGo);
|
||
AssignReference(confirmCtrl, "_titleText", titleTmp);
|
||
AssignReference(confirmCtrl, "_bodyText", bodyTmp);
|
||
AssignReference(confirmCtrl, "_confirmLabel", GetButtonLabel(yesGo));
|
||
AssignReference(confirmCtrl, "_cancelLabel", GetButtonLabel(noGo));
|
||
AssignReference(confirmCtrl, "_btnConfirm", yesGo.GetComponent<Button>());
|
||
AssignReference(confirmCtrl, "_btnCancel", noGo.GetComponent<Button>());
|
||
return confirmCtrl;
|
||
}
|
||
|
||
/// <summary>构建新游戏模式选择面板(居中模态:普通 / 钢铁之魂 / 返回 + 钢魂说明),返回控制器。</summary>
|
||
private static NewGameModeController BuildNewGameMode(Transform parent)
|
||
{
|
||
var rootRt = GetOrCreateUIChild(parent, "NewGameMode");
|
||
StretchFull(rootRt);
|
||
GameObject modeGo = rootRt.gameObject;
|
||
modeGo.SetActive(false);
|
||
NewGameModeController modeCtrl = GetOrAddComponent<NewGameModeController>(modeGo);
|
||
|
||
GetOrCreateImage(modeGo.transform, "Overlay", new Color(0f, 0f, 0f, 0.6f), true).transform.SetAsFirstSibling();
|
||
|
||
var boxRt = GetOrCreateUIChild(modeGo.transform, "DialogBox");
|
||
SetRect(boxRt, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(760f, 460f));
|
||
GetOrAddComponent<Image>(boxRt.gameObject).color = new Color(0.10f, 0.11f, 0.15f, 0.98f);
|
||
|
||
var modeTitle = GetOrCreateText(boxRt.transform, "TitleText", "Select Mode", 40f, GoldText, TextAlignmentOptions.Center);
|
||
SetRect((RectTransform)modeTitle.transform, new Vector2(0f,1f), new Vector2(1f,1f), new Vector2(0.5f,1f), new Vector2(0f,-46f), new Vector2(-60f,56f));
|
||
|
||
GameObject normalGo = GetOrCreateButtonChild(boxRt.transform, "Btn_Normal", "Normal");
|
||
SetRect((RectTransform)normalGo.transform, new Vector2(0.5f,1f), new Vector2(0.5f,1f), new Vector2(0.5f,1f), new Vector2(0f,-130f), new Vector2(420f,66f));
|
||
GameObject steelGo = GetOrCreateButtonChild(boxRt.transform, "Btn_SteelSoul", "Steel Soul");
|
||
SetRect((RectTransform)steelGo.transform, new Vector2(0.5f,1f), new Vector2(0.5f,1f), new Vector2(0.5f,1f), new Vector2(0f,-206f), new Vector2(420f,66f));
|
||
steelGo.GetComponent<Image>().color = new Color(0.30f, 0.33f, 0.40f, 0.85f);
|
||
|
||
var steelDesc = GetOrCreateText(boxRt.transform, "SteelSoulDesc", "Steel Soul: one life. Death wipes the save.", 22f, new Color(0.8f,0.55f,0.55f,1f), TextAlignmentOptions.Center);
|
||
SetRect((RectTransform)steelDesc.transform, new Vector2(0f,0f), new Vector2(1f,0f), new Vector2(0.5f,0f), new Vector2(0f,130f), new Vector2(-80f,60f));
|
||
|
||
GameObject backGo = GetOrCreateButtonChild(boxRt.transform, "Btn_Back", "Back");
|
||
SetRect((RectTransform)backGo.transform, new Vector2(0.5f,0f), new Vector2(0.5f,0f), new Vector2(0.5f,0f), new Vector2(0f,46f), new Vector2(260f,60f));
|
||
StyleAsTextButton(backGo, 28f);
|
||
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;
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
// Scaffold Game Room
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// 在当前活动场景中生成标准游戏关卡房间的完整层级结构:
|
||
/// [RoomRoot] → [Camera] / [SpawnPoints] / [Environment] / [Transitions]
|
||
/// 可配合 SceneObjectPlacerTool 在层级内快速追加更多对象。
|
||
/// </summary>
|
||
[MenuItem("BaseGames/Scene/Setup/Scaffold Game Room", priority = 201)]
|
||
public static void ScaffoldGameRoom()
|
||
{
|
||
var report = new List<string>();
|
||
|
||
// ── [RoomRoot] ─────────────────────────────────────────────────
|
||
GameObject root = GetOrCreateRoot("[RoomRoot]");
|
||
RoomController roomController = GetOrAddComponent<RoomController>(root);
|
||
|
||
// ── [Camera] ───────────────────────────────────────────────────
|
||
Transform cameraGroup = GetOrCreateChild(root.transform, "[Camera]");
|
||
|
||
// CameraArea — 定义相机区域(限位 + 混合配置 + 可选专有 VCam)
|
||
GameObject cameraAreaGo = GetOrCreateChild(cameraGroup, "CameraArea").gameObject;
|
||
CameraArea cameraArea = GetOrAddComponent<CameraArea>(cameraAreaGo);
|
||
|
||
// AreaBoundary — 提供 CinemachineConfiner3D 所需的限位体积
|
||
Transform boundaryT = GetOrCreateChild(cameraAreaGo.transform, "AreaBoundary");
|
||
BoxCollider boundaryCollider = GetOrAddComponent<BoxCollider>(boundaryT.gameObject);
|
||
boundaryCollider.center = new Vector3(0f, 0f, -10f); // Z 占位符,实际深度由 SyncConfiner 按 LensConfig 计算
|
||
boundaryCollider.size = new Vector3(24f, 12f, 1f); // 默认房间尺寸占位符
|
||
|
||
AssignReference(cameraArea, "_confinerCollider", boundaryCollider);
|
||
|
||
// ── [SpawnPoints] ──────────────────────────────────────────────
|
||
Transform spawnGroup = GetOrCreateChild(root.transform, "[SpawnPoints]");
|
||
|
||
GameObject defaultSpawnGo = GetOrCreateChild(spawnGroup, "SpawnPoint_Default").gameObject;
|
||
PlayerSpawnPoint defaultSpawn = GetOrAddComponent<PlayerSpawnPoint>(defaultSpawnGo);
|
||
AssignString(defaultSpawn, "_transitionId", "default");
|
||
AssignInt(defaultSpawn, "_facingDirection", 1);
|
||
|
||
// ── [Environment] ──────────────────────────────────────────────
|
||
Transform envGroup = GetOrCreateChild(root.transform, "[Environment]");
|
||
|
||
// Ground Tilemap
|
||
GameObject gridGo = GetOrCreateChild(envGroup, "GroundGrid").gameObject;
|
||
GetOrAddComponent<Grid>(gridGo);
|
||
|
||
GameObject groundTileGo = GetOrCreateChild(gridGo.transform, "Ground").gameObject;
|
||
int groundLayer = LayerMask.NameToLayer("Platform");
|
||
if (groundLayer >= 0) groundTileGo.layer = groundLayer;
|
||
else report.Add("Layer 'Platform' 不存在,请在 Tags and Layers 中创建。");
|
||
|
||
GetOrAddComponent<Tilemap>(groundTileGo);
|
||
GetOrAddComponent<TilemapRenderer>(groundTileGo);
|
||
TilemapCollider2D tilemapCol = GetOrAddComponent<TilemapCollider2D>(groundTileGo);
|
||
tilemapCol.usedByComposite = true;
|
||
Rigidbody2D groundRb = GetOrAddComponent<Rigidbody2D>(groundTileGo);
|
||
groundRb.bodyType = RigidbodyType2D.Static;
|
||
GetOrAddComponent<CompositeCollider2D>(groundTileGo);
|
||
|
||
// NavSurface for PathBerserker2d
|
||
GameObject navGo = GetOrCreateChild(envGroup, "NavSurface").gameObject;
|
||
GetOrAddComponent<NavSurface>(navGo);
|
||
|
||
// ── [Transitions] ──────────────────────────────────────────────
|
||
GetOrCreateChild(root.transform, "[Transitions]");
|
||
|
||
// ── Wire RoomController ────────────────────────────────────────
|
||
AssignReference(roomController, "_cameraArea", cameraArea);
|
||
|
||
SerializedObject roomSO = new SerializedObject(roomController);
|
||
SerializedProperty spawnArrayProp = roomSO.FindProperty("_spawnPoints");
|
||
if (spawnArrayProp != null && spawnArrayProp.isArray)
|
||
{
|
||
spawnArrayProp.arraySize = 1;
|
||
spawnArrayProp.GetArrayElementAtIndex(0).objectReferenceValue = defaultSpawn;
|
||
roomSO.ApplyModifiedPropertiesWithoutUndo();
|
||
}
|
||
|
||
// ── MapRoomDataSO 模板(幂等:同名资产已存在时跳过)──────────────
|
||
string roomIdHint = SceneManager.GetActiveScene().name;
|
||
if (string.IsNullOrEmpty(roomIdHint) || roomIdHint == "Untitled")
|
||
roomIdHint = "Room_Unknown";
|
||
|
||
string roomSoFolder = "Assets/_Game/Data/Map/Rooms";
|
||
string roomSoPath = $"{roomSoFolder}/{roomIdHint}.asset";
|
||
|
||
bool roomSoCreated = false;
|
||
if (!System.IO.File.Exists(Application.dataPath + "/../" + roomSoPath))
|
||
{
|
||
EnsureFolder(roomSoFolder);
|
||
var roomData = ScriptableObject.CreateInstance<MapRoomDataSO>();
|
||
roomData.RoomId = roomIdHint;
|
||
roomData.RegionId = roomIdHint.Contains("_")
|
||
? roomIdHint.Split('_')[1]
|
||
: "";
|
||
AssetDatabase.CreateAsset(roomData, roomSoPath);
|
||
AssetDatabase.SaveAssets();
|
||
AssetDatabase.Refresh();
|
||
roomSoCreated = true;
|
||
}
|
||
|
||
// ── Report ─────────────────────────────────────────────────────
|
||
report.Add("在 RoomController._roomId 填写唯一房间 ID(如 \"Room_Forest_01\")。");
|
||
if (roomSoCreated)
|
||
report.Add($"已自动创建 MapRoomDataSO 模板:{roomSoPath}。请填写 GridPosition / GridSize / Exits,并将其添加到 MapDatabaseSO.AllRooms 中。");
|
||
else
|
||
report.Add($"MapRoomDataSO 已存在({roomSoPath}),请确认其 RoomId / Exits 与场景保持同步。");
|
||
report.Add("绑定 LensConfig SO 后单击 Inspector 中「从可视区域更新限位区域」计算正确的 BoxCollider。");
|
||
report.Add("使用 Tile Palette 在 Ground Tilemap 上绘制地形,然后在 NavSurface Inspector 中点击 Bake。");
|
||
report.Add("[Transitions] 子节点下使用 BaseGames/Scene/Place/Room Transition 添加过渡点。");
|
||
|
||
MarkDirtyAndLog("Game Room 脚手架", root, report);
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
// 流式加载系统(RoomStreamingManager + TransitionDirector)
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// 在 [Services] 下创建或更新 SYS_RoomStreamingManager,
|
||
/// 挂载 <see cref="RoomStreamingManager"/> 与 <see cref="TransitionDirector"/>,
|
||
/// 并自动绑定已存在的事件频道与配置资产。
|
||
/// </summary>
|
||
private static void ScaffoldStreamingSystem(Transform services, List<string> report)
|
||
{
|
||
// 预算配置 SO(不存在时自动创建)
|
||
StreamingBudgetConfigSO budgetConfig = EnsureStreamingBudgetConfigAsset(report);
|
||
|
||
// MapDatabaseSO(查找已存在的资产)
|
||
Object mapDbAsset = FindFirstAssetByType<MapDatabaseSO>("MapDatabase", "MAP_Database", "MapDatabaseSO");
|
||
if (mapDbAsset == null)
|
||
report.Add("未找到 MapDatabaseSO 资产。请将 MapDatabaseSO 手工赋给 RoomStreamingManager._mapDatabase 与 TransitionDirector._mapDatabase。");
|
||
|
||
// ── SYS_RoomStreamingManager GameObject ──────────────────────────
|
||
GameObject streamingGo = GetOrCreateChild(services, "SYS_RoomStreamingManager").gameObject;
|
||
|
||
RoomStreamingManager streamingMgr = GetOrAddComponent<RoomStreamingManager>(streamingGo);
|
||
TransitionDirector transitionDir = GetOrAddComponent<TransitionDirector>(streamingGo);
|
||
|
||
// ── RoomStreamingManager 字段 ─────────────────────────────────────
|
||
AssignReference(streamingMgr, "_mapDatabase", mapDbAsset);
|
||
AssignReference(streamingMgr, "_budget", budgetConfig);
|
||
AssignAsset(streamingMgr, "_onRoomEntered", report, false, "EVT_RoomEntered");
|
||
AssignAsset(streamingMgr, "_onRoomPreloaded", report, false, "EVT_RoomPreloaded");
|
||
AssignAsset(streamingMgr, "_onRoomActivated", report, false, "EVT_RoomActivated");
|
||
|
||
// ── TransitionDirector 字段 ───────────────────────────────────────
|
||
AssignReference(transitionDir, "_streamingManagerRef", streamingMgr);
|
||
AssignReference(transitionDir, "_mapDatabase", mapDbAsset);
|
||
AssignReference(transitionDir, "_budget", budgetConfig);
|
||
AssignAsset(transitionDir, "_onFadeOutRequest", report, false, "EVT_FadeOutRequest");
|
||
AssignAsset(transitionDir, "_onFadeInRequest", report, false, "EVT_FadeInRequest");
|
||
AssignAsset(transitionDir, "_onRegionNameDisplay", report, false, "EVT_RegionNameDisplay");
|
||
AssignAsset(transitionDir, "_onSceneWorldStateRestored", report, false, "EVT_SceneWorldStateRestored");
|
||
|
||
report.Add("SYS_RoomStreamingManager:流式加载系统已创建。如 EVT_RoomEntered / EVT_RoomPreloaded 频道尚未存在,请通过 DataHub > Streaming 创建后重新运行脚手架。");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 在 <c>Assets/_Game/Data/Streaming/</c> 下确保默认预算配置 SO 存在。
|
||
/// 已存在时直接返回;不存在时自动创建 <c>STR_BudgetConfig_Default.asset</c>。
|
||
/// </summary>
|
||
private static StreamingBudgetConfigSO EnsureStreamingBudgetConfigAsset(List<string> report)
|
||
{
|
||
// 先查找已有资产
|
||
string[] guids = AssetDatabase.FindAssets("t:StreamingBudgetConfigSO");
|
||
if (guids != null && guids.Length > 0)
|
||
{
|
||
string path = AssetDatabase.GUIDToAssetPath(guids[0]);
|
||
var existing = AssetDatabase.LoadAssetAtPath<StreamingBudgetConfigSO>(path);
|
||
if (existing != null)
|
||
return existing;
|
||
}
|
||
|
||
// 没有则创建默认资产
|
||
const string folder = "Assets/_Game/Data/Streaming";
|
||
const string assetPath = folder + "/STR_BudgetConfig_Default.asset";
|
||
EnsureFolder(folder);
|
||
|
||
var created = ScriptableObject.CreateInstance<StreamingBudgetConfigSO>();
|
||
AssetDatabase.CreateAsset(created, assetPath);
|
||
AssetDatabase.SaveAssets();
|
||
AssetDatabase.Refresh();
|
||
|
||
report.Add($"已自动创建流式加载预算配置:{assetPath}。可在 DataHub > 流式加载 中编辑默认参数。");
|
||
return created;
|
||
}
|
||
|
||
private static void AssignString(Object target, string propertyName, string value, List<string> report = null)
|
||
{
|
||
SerializedObject serializedObject = new SerializedObject(target);
|
||
SerializedProperty property = serializedObject.FindProperty(propertyName);
|
||
if (property == null)
|
||
{
|
||
report?.Add($"{target.GetType().Name}.{propertyName} 字段不存在,未写入字符串值。");
|
||
return;
|
||
}
|
||
property.stringValue = value;
|
||
serializedObject.ApplyModifiedPropertiesWithoutUndo();
|
||
}
|
||
|
||
private static void AssignInt(Object target, string propertyName, int value, List<string> report = null)
|
||
{
|
||
SerializedObject serializedObject = new SerializedObject(target);
|
||
SerializedProperty property = serializedObject.FindProperty(propertyName);
|
||
if (property == null)
|
||
{
|
||
report?.Add($"{target.GetType().Name}.{propertyName} 字段不存在,未写入整型值。");
|
||
return;
|
||
}
|
||
property.intValue = value;
|
||
serializedObject.ApplyModifiedPropertiesWithoutUndo();
|
||
}
|
||
|
||
private static void EnsureEventChannelAssets(List<string> report)
|
||
{
|
||
bool hasCoreSet =
|
||
FindFirstAsset("EVT_PlayerDied") != null &&
|
||
FindFirstAsset("EVT_DeathScreenConfirmed") != null &&
|
||
(FindFirstAsset("EVT_GameStateChanged") != null || FindFirstAsset("EVT_GameState") != null) &&
|
||
FindFirstAsset("EVT_PauseRequested") != null &&
|
||
FindFirstAsset("EVT_SceneLoadRequest") != null;
|
||
|
||
if (hasCoreSet)
|
||
return;
|
||
|
||
CreateEventChannelAssets.CreateAll();
|
||
report?.Add("检测到关键事件频道缺失,已自动执行 Create Event Channel Assets。");
|
||
}
|
||
|
||
|
||
|
||
private static void EnsureAudioSources(GameObject audioManagerGo, AudioManager audioManager, List<string> report)
|
||
{
|
||
GameObject bgmAGo = GetOrCreateChild(audioManagerGo.transform, "BGM Source A").gameObject;
|
||
GameObject bgmBGo = GetOrCreateChild(audioManagerGo.transform, "BGM Source B").gameObject;
|
||
GameObject sfxRootGo = GetOrCreateChild(audioManagerGo.transform, "SFX Sources").gameObject;
|
||
|
||
AudioSource bgmA = GetOrAddComponent<AudioSource>(bgmAGo);
|
||
AudioSource bgmB = GetOrAddComponent<AudioSource>(bgmBGo);
|
||
bgmA.playOnAwake = false;
|
||
bgmB.playOnAwake = false;
|
||
bgmA.loop = true;
|
||
bgmB.loop = true;
|
||
|
||
var sfxSources = new AudioSource[6];
|
||
for (int i = 0; i < sfxSources.Length; i++)
|
||
{
|
||
GameObject sfxGo = GetOrCreateChild(sfxRootGo.transform, $"SFX Source {i + 1}").gameObject;
|
||
AudioSource sfxSource = GetOrAddComponent<AudioSource>(sfxGo);
|
||
sfxSource.playOnAwake = false;
|
||
sfxSources[i] = sfxSource;
|
||
}
|
||
|
||
AssignReference(audioManager, "_bgmSourceA", bgmA);
|
||
AssignReference(audioManager, "_bgmSourceB", bgmB);
|
||
AssignArrayReferences(audioManager, "_sfxSources", sfxSources, report);
|
||
// 尝试自动绑定 AudioMixer 与 AudioConfig(缺失时报告,需音频资产补齐)
|
||
AssignReference(audioManager, "_mixer", FindFirstAssetWithExtension(".mixer", "MainAudioMixer", "GameAudioMixer", "AudioMixer"), report);
|
||
AssignAsset(audioManager, "_audioConfig", report, false, "AUD_AudioConfig", "AudioConfig");
|
||
report.Add("AudioManager 已生成 2 个 BGM Source 和 6 个 SFX Source;_mixer/_audioConfig 若缺失需补齐音频资产。");
|
||
}
|
||
|
||
private static GameObject GetOrCreateRoot(string name)
|
||
{
|
||
Scene scene = SceneManager.GetActiveScene();
|
||
foreach (GameObject rootObject in scene.GetRootGameObjects())
|
||
{
|
||
if (rootObject.name == name)
|
||
return rootObject;
|
||
}
|
||
|
||
GameObject root = new GameObject(name);
|
||
Undo.RegisterCreatedObjectUndo(root, $"Create {name}");
|
||
return root;
|
||
}
|
||
|
||
private static Transform GetOrCreateChild(Transform parent, string name)
|
||
{
|
||
Transform child = parent.Find(name);
|
||
if (child != null)
|
||
return child;
|
||
|
||
GameObject go = new GameObject(name);
|
||
Undo.RegisterCreatedObjectUndo(go, $"Create {name}");
|
||
go.transform.SetParent(parent, false);
|
||
return go.transform;
|
||
}
|
||
|
||
private static T GetOrAddComponent<T>(GameObject go) where T : Component
|
||
{
|
||
T component = go.GetComponent<T>();
|
||
if (component != null)
|
||
return component;
|
||
|
||
return Undo.AddComponent<T>(go);
|
||
}
|
||
|
||
private static GameObject GetOrCreateCanvas(Transform parent, string name, int sortOrder)
|
||
{
|
||
GameObject canvasGo = GetOrCreateChild(parent, name).gameObject;
|
||
Canvas canvas = GetOrAddComponent<Canvas>(canvasGo);
|
||
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
||
canvas.sortingOrder = sortOrder;
|
||
var scaler = GetOrAddComponent<CanvasScaler>(canvasGo);
|
||
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
||
scaler.referenceResolution = new Vector2(1920f, 1080f);
|
||
scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight;
|
||
scaler.matchWidthOrHeight = 0.5f;
|
||
GetOrAddComponent<GraphicRaycaster>(canvasGo);
|
||
return canvasGo;
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
// UI 布局辅助(RectTransform 感知)
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// 创建/获取 UI 子节点并保证其 transform 为 <see cref="RectTransform"/>。
|
||
/// 旧的普通 <see cref="Transform"/> 节点无法原地转换,会被销毁并以 RectTransform 重建(含其子树),
|
||
/// 以支持脚手架对历史场景的"重建"修复。
|
||
/// </summary>
|
||
private static RectTransform GetOrCreateUIChild(Transform parent, string name)
|
||
{
|
||
Transform child = parent.Find(name);
|
||
if (child is RectTransform existing) return existing;
|
||
if (child != null) Undo.DestroyObjectImmediate(child.gameObject);
|
||
|
||
GameObject go = new GameObject(name, typeof(RectTransform));
|
||
Undo.RegisterCreatedObjectUndo(go, $"Create {name}");
|
||
go.transform.SetParent(parent, false);
|
||
return (RectTransform)go.transform;
|
||
}
|
||
|
||
/// <summary>将 RectTransform 设为四向拉伸(铺满父节点,可带统一内边距)。</summary>
|
||
private static void StretchFull(RectTransform rt, float padding = 0f)
|
||
{
|
||
rt.anchorMin = Vector2.zero;
|
||
rt.anchorMax = Vector2.one;
|
||
rt.pivot = new Vector2(0.5f, 0.5f);
|
||
rt.offsetMin = new Vector2(padding, padding);
|
||
rt.offsetMax = new Vector2(-padding, -padding);
|
||
}
|
||
|
||
/// <summary>按锚点 + 锚定位置 + 尺寸配置 RectTransform。</summary>
|
||
private static void SetRect(RectTransform rt, Vector2 anchorMin, Vector2 anchorMax,
|
||
Vector2 pivot, Vector2 anchoredPos, Vector2 size)
|
||
{
|
||
rt.anchorMin = anchorMin;
|
||
rt.anchorMax = anchorMax;
|
||
rt.pivot = pivot;
|
||
rt.anchoredPosition = anchoredPos;
|
||
rt.sizeDelta = size;
|
||
}
|
||
|
||
/// <summary>创建/获取一个 <see cref="TextMeshProUGUI"/> 文本子节点(默认铺满父节点、不拦截射线)。</summary>
|
||
private static TextMeshProUGUI GetOrCreateText(Transform parent, string name, string text,
|
||
float fontSize, Color color, TextAlignmentOptions align = TextAlignmentOptions.Center)
|
||
{
|
||
RectTransform rt = GetOrCreateUIChild(parent, name);
|
||
StretchFull(rt);
|
||
var tmp = GetOrAddComponent<TextMeshProUGUI>(rt.gameObject);
|
||
tmp.text = text;
|
||
tmp.fontSize = fontSize;
|
||
tmp.color = color;
|
||
tmp.alignment = align;
|
||
tmp.enableWordWrapping = true;
|
||
tmp.raycastTarget = false;
|
||
return tmp;
|
||
}
|
||
|
||
/// <summary>创建/获取一个铺满父节点的 <see cref="Image"/>(纯色块,可作背景 / 遮罩)。</summary>
|
||
private static Image GetOrCreateImage(Transform parent, string name, Color color, bool raycastTarget)
|
||
{
|
||
RectTransform rt = GetOrCreateUIChild(parent, name);
|
||
StretchFull(rt);
|
||
var img = GetOrAddComponent<Image>(rt.gameObject);
|
||
img.color = color;
|
||
img.raycastTarget = raycastTarget;
|
||
return img;
|
||
}
|
||
|
||
// 金色高亮配色(文字按钮 / 卡片选择)
|
||
private static readonly Color GoldText = new Color(0.92f, 0.86f, 0.66f, 1f);
|
||
private static readonly Color GoldHighlight = new Color(1f, 0.84f, 0.40f, 0.20f);
|
||
private static readonly Color GoldPressed = new Color(1f, 0.84f, 0.40f, 0.34f);
|
||
|
||
/// <summary>
|
||
/// 在父节点下创建/获取一个带 <see cref="Image"/> + <see cref="Button"/> + TMP 文本标签的按钮(幂等,RectTransform 化)。
|
||
/// 默认带低调的深色底 + 金色悬停 / 选中高亮;文本标签位于子节点 "Label"。
|
||
/// </summary>
|
||
private static GameObject GetOrCreateButtonChild(Transform parent, string name, string label)
|
||
{
|
||
RectTransform rt = GetOrCreateUIChild(parent, name);
|
||
GameObject go = rt.gameObject;
|
||
|
||
var img = GetOrAddComponent<Image>(go);
|
||
img.color = new Color(0.10f, 0.11f, 0.14f, 0.55f);
|
||
img.raycastTarget = true;
|
||
|
||
var btn = GetOrAddComponent<Button>(go);
|
||
btn.targetGraphic = img;
|
||
var colors = btn.colors;
|
||
colors.normalColor = Color.white;
|
||
colors.highlightedColor = new Color(1f, 0.95f, 0.80f, 1f);
|
||
colors.pressedColor = new Color(1f, 0.84f, 0.40f, 1f);
|
||
colors.selectedColor = new Color(1f, 0.95f, 0.80f, 1f);
|
||
colors.disabledColor = new Color(0.5f, 0.5f, 0.5f, 0.4f);
|
||
colors.fadeDuration = 0.08f;
|
||
btn.colors = colors;
|
||
|
||
// 文本标签(铺满按钮,居中)
|
||
GetOrCreateText(go.transform, "Label", label ?? string.Empty, 30f, GoldText, TextAlignmentOptions.Center);
|
||
|
||
return go;
|
||
}
|
||
|
||
/// <summary>取按钮的 TMP 文本标签(GetOrCreateButtonChild 生成的 "Label" 子节点)。</summary>
|
||
private static TextMeshProUGUI GetButtonLabel(GameObject buttonGo)
|
||
{
|
||
var t = buttonGo.transform.Find("Label");
|
||
return t != null ? t.GetComponent<TextMeshProUGUI>() : null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将按钮改造为"纯文字"风格(透明底)。关键:把 Button 的 targetGraphic 指向文字 Label,
|
||
/// 这样鼠标悬停(Highlighted)与键盘/手柄选中(Selected)会直接给文字着色 → 导航有可见反馈。
|
||
/// Label 基色设为白,由 ColorBlock 决定静态/高亮/选中的可见色。
|
||
/// </summary>
|
||
private static void StyleAsTextButton(GameObject buttonGo, float fontSize = 34f)
|
||
{
|
||
var img = buttonGo.GetComponent<Image>();
|
||
if (img != null) { img.color = new Color(1f, 1f, 1f, 0f); img.raycastTarget = true; } // 透明底仍作 raycast target
|
||
var label = GetButtonLabel(buttonGo);
|
||
if (label == null) return;
|
||
label.fontSize = fontSize;
|
||
label.fontStyle = FontStyles.Normal;
|
||
label.color = Color.white; // 基色白,实际可见色由 ColorBlock × 该色得到
|
||
|
||
var btn = buttonGo.GetComponent<Button>();
|
||
if (btn == null) return;
|
||
btn.targetGraphic = label; // 文字作为着色目标 → 选中/悬停可见
|
||
var c = btn.colors;
|
||
c.normalColor = new Color(0.60f, 0.56f, 0.42f, 1f); // 静态:暗金
|
||
c.highlightedColor = new Color(1f, 0.95f, 0.72f, 1f); // 鼠标悬停:亮金
|
||
c.selectedColor = new Color(1f, 0.95f, 0.72f, 1f); // 键盘/手柄选中:亮金
|
||
c.pressedColor = new Color(1f, 0.82f, 0.38f, 1f);
|
||
c.disabledColor = new Color(0.4f, 0.4f, 0.4f, 0.5f);
|
||
c.colorMultiplier = 1f; c.fadeDuration = 0.1f;
|
||
btn.colors = c;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 给含 TMP_Text 的节点挂上 <see cref="BaseGames.Localization.LocalizedText"/> 并绑定本地化 key(UI 表)。
|
||
/// 自动绑定 FontConfig(语言切换时换 CJK 字体),并立即刷新编辑器预览(显示当前语言文本)。
|
||
/// </summary>
|
||
private static void BindLocalizedText(GameObject textGo, string key)
|
||
{
|
||
if (textGo == null || textGo.GetComponent<TMP_Text>() == null) return;
|
||
var loc = GetOrAddComponent<BaseGames.Localization.LocalizedText>(textGo);
|
||
var so = new SerializedObject(loc);
|
||
so.FindProperty("_key").stringValue = key;
|
||
var fontCfg = FindFirstAssetByType<BaseGames.Localization.LanguageFontConfigSO>("FontConfig");
|
||
if (fontCfg != null) so.FindProperty("_fontConfig").objectReferenceValue = fontCfg;
|
||
so.ApplyModifiedPropertiesWithoutUndo();
|
||
loc.UpdateEditorPreview();
|
||
}
|
||
|
||
/// <summary>给 GetOrCreateButtonChild 生成的按钮的 "Label" 子节点绑定本地化 key。</summary>
|
||
private static void BindLocalizedButton(GameObject buttonGo, string key)
|
||
{
|
||
var label = GetButtonLabel(buttonGo);
|
||
if (label != null) BindLocalizedText(label.gameObject, key);
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
// 设置控件辅助(复用 Unity DefaultControls / TMP_DefaultControls 标准控件层级)
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
|
||
private static UnityEngine.UI.DefaultControls.Resources _uiRes;
|
||
private static UnityEngine.UI.DefaultControls.Resources UIRes()
|
||
{
|
||
if (_uiRes.standard == null)
|
||
_uiRes = new UnityEngine.UI.DefaultControls.Resources
|
||
{
|
||
standard = AssetDatabase.GetBuiltinExtraResource<Sprite>("UI/Skin/UISprite.psd"),
|
||
background = AssetDatabase.GetBuiltinExtraResource<Sprite>("UI/Skin/Background.psd"),
|
||
inputField = AssetDatabase.GetBuiltinExtraResource<Sprite>("UI/Skin/InputFieldBackground.psd"),
|
||
knob = AssetDatabase.GetBuiltinExtraResource<Sprite>("UI/Skin/Knob.psd"),
|
||
checkmark = AssetDatabase.GetBuiltinExtraResource<Sprite>("UI/Skin/Checkmark.psd"),
|
||
dropdown = AssetDatabase.GetBuiltinExtraResource<Sprite>("UI/Skin/DropdownArrow.psd"),
|
||
mask = AssetDatabase.GetBuiltinExtraResource<Sprite>("UI/Skin/UIMask.psd"),
|
||
};
|
||
return _uiRes;
|
||
}
|
||
|
||
private static TMPro.TMP_DefaultControls.Resources _tmpRes;
|
||
private static TMPro.TMP_DefaultControls.Resources TMPRes()
|
||
{
|
||
if (_tmpRes.standard == null)
|
||
_tmpRes = new TMPro.TMP_DefaultControls.Resources
|
||
{
|
||
standard = AssetDatabase.GetBuiltinExtraResource<Sprite>("UI/Skin/UISprite.psd"),
|
||
background = AssetDatabase.GetBuiltinExtraResource<Sprite>("UI/Skin/Background.psd"),
|
||
inputField = AssetDatabase.GetBuiltinExtraResource<Sprite>("UI/Skin/InputFieldBackground.psd"),
|
||
knob = AssetDatabase.GetBuiltinExtraResource<Sprite>("UI/Skin/Knob.psd"),
|
||
checkmark = AssetDatabase.GetBuiltinExtraResource<Sprite>("UI/Skin/Checkmark.psd"),
|
||
dropdown = AssetDatabase.GetBuiltinExtraResource<Sprite>("UI/Skin/DropdownArrow.psd"),
|
||
mask = AssetDatabase.GetBuiltinExtraResource<Sprite>("UI/Skin/UIMask.psd"),
|
||
};
|
||
return _tmpRes;
|
||
}
|
||
|
||
/// <summary>将控件 RectTransform 放到行的右半区(横向拉伸、固定高度、垂直居中)。</summary>
|
||
private static void PlaceRightHalf(RectTransform rt, float height, float rightInset = 12f)
|
||
{
|
||
rt.anchorMin = new Vector2(0.5f, 0.5f);
|
||
rt.anchorMax = new Vector2(1f, 0.5f);
|
||
rt.pivot = new Vector2(0.5f, 0.5f);
|
||
rt.offsetMin = new Vector2(8f, -height / 2f);
|
||
rt.offsetMax = new Vector2(-rightInset, height / 2f);
|
||
}
|
||
|
||
/// <summary>创建/获取一行设置项(左侧本地化标签 + 右半区控件,由调用方填充),返回行 RectTransform。</summary>
|
||
private static RectTransform CreateSettingRow(Transform content, string name, string labelKey)
|
||
{
|
||
var row = GetOrCreateUIChild(content, name);
|
||
var le = GetOrAddComponent<LayoutElement>(row.gameObject);
|
||
le.preferredHeight = 56f; le.minHeight = 50f;
|
||
var label = GetOrCreateText(row, "Label", labelKey, 26f, GoldText, TextAlignmentOptions.MidlineLeft);
|
||
SetRect((RectTransform)label.transform, new Vector2(0f, 0f), new Vector2(0.5f, 1f), new Vector2(0f, 0.5f), new Vector2(12f, 0f), Vector2.zero);
|
||
((RectTransform)label.transform).offsetMin = new Vector2(12f, 0f);
|
||
((RectTransform)label.transform).offsetMax = new Vector2(0f, 0f);
|
||
BindLocalizedText(label.gameObject, labelKey);
|
||
return row;
|
||
}
|
||
|
||
private static Slider GetOrCreateSliderInRow(RectTransform row, float min, float max, float val, float rightInset = 12f)
|
||
{
|
||
var existing = row.Find("Slider");
|
||
GameObject go;
|
||
if (existing != null && existing.GetComponent<Slider>() != null) go = existing.gameObject;
|
||
else
|
||
{
|
||
if (existing != null) Undo.DestroyObjectImmediate(existing.gameObject);
|
||
go = UnityEngine.UI.DefaultControls.CreateSlider(UIRes());
|
||
go.name = "Slider";
|
||
Undo.RegisterCreatedObjectUndo(go, "Create Slider");
|
||
go.transform.SetParent(row, false);
|
||
}
|
||
PlaceRightHalf((RectTransform)go.transform, 20f, rightInset);
|
||
var s = go.GetComponent<Slider>();
|
||
s.minValue = min; s.maxValue = max; s.value = val;
|
||
return s;
|
||
}
|
||
|
||
private static Toggle GetOrCreateToggleInRow(RectTransform row)
|
||
{
|
||
var existing = row.Find("Toggle");
|
||
GameObject go;
|
||
if (existing != null && existing.GetComponent<Toggle>() != null) go = existing.gameObject;
|
||
else
|
||
{
|
||
if (existing != null) Undo.DestroyObjectImmediate(existing.gameObject);
|
||
go = UnityEngine.UI.DefaultControls.CreateToggle(UIRes());
|
||
go.name = "Toggle";
|
||
Undo.RegisterCreatedObjectUndo(go, "Create Toggle");
|
||
go.transform.SetParent(row, false);
|
||
}
|
||
// 隐藏勾选框自带的 Label(设置项标签由行左侧统一提供)
|
||
var lbl = go.transform.Find("Label");
|
||
if (lbl != null) lbl.gameObject.SetActive(false);
|
||
PlaceRightHalf((RectTransform)go.transform, 28f);
|
||
return go.GetComponent<Toggle>();
|
||
}
|
||
|
||
private static TMPro.TMP_Dropdown GetOrCreateDropdownInRow(RectTransform row, string[] options)
|
||
{
|
||
var existing = row.Find("Dropdown");
|
||
GameObject go;
|
||
if (existing != null && existing.GetComponent<TMPro.TMP_Dropdown>() != null) go = existing.gameObject;
|
||
else
|
||
{
|
||
if (existing != null) Undo.DestroyObjectImmediate(existing.gameObject);
|
||
go = TMPro.TMP_DefaultControls.CreateDropdown(TMPRes());
|
||
go.name = "Dropdown";
|
||
Undo.RegisterCreatedObjectUndo(go, "Create Dropdown");
|
||
go.transform.SetParent(row, false);
|
||
}
|
||
PlaceRightHalf((RectTransform)go.transform, 36f);
|
||
var dd = go.GetComponent<TMPro.TMP_Dropdown>();
|
||
dd.options.Clear();
|
||
foreach (var o in options) dd.options.Add(new TMPro.TMP_Dropdown.OptionData(o));
|
||
dd.value = 0;
|
||
dd.RefreshShownValue();
|
||
return dd;
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
// 设置面板 / 制作团队面板构建器
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
|
||
/// <summary>构建设置面板内容(音量 / 画面 / 可访问性 / 语言)并绑定 SettingsPanelController 全部字段。</summary>
|
||
private static void BuildSettingsPanel(GameObject panelGo, MainMenuController menuCtrl, List<string> report)
|
||
{
|
||
var ctrl = GetOrAddComponent<BaseGames.UI.SettingsPanelController>(panelGo);
|
||
|
||
GetOrCreateImage(panelGo.transform, "Overlay", new Color(0.04f, 0.05f, 0.08f, 0.97f), true).transform.SetAsFirstSibling();
|
||
|
||
var titleTmp = GetOrCreateText(panelGo.transform, "PanelTitle", "Settings", 56f, GoldText, TextAlignmentOptions.Center);
|
||
titleTmp.fontStyle = FontStyles.Bold;
|
||
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);
|
||
|
||
// 返回按钮
|
||
GameObject backGo = GetOrCreateButtonChild(panelGo.transform, "BackButton", "Back");
|
||
SetRect((RectTransform)backGo.transform, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, 60f), new Vector2(260f, 64f));
|
||
StyleAsTextButton(backGo, 30f);
|
||
BindLocalizedButton(backGo, "BTN_BACK");
|
||
AssignReference(menuCtrl, "_btnCloseSettings", backGo.GetComponent<Button>());
|
||
}
|
||
|
||
/// <summary>构建制作团队面板(标题 + 滚动正文 + 返回)。</summary>
|
||
private static void BuildCreditsPanel(GameObject panelGo, MainMenuController menuCtrl, List<string> report)
|
||
{
|
||
GetOrCreateImage(panelGo.transform, "Overlay", new Color(0.04f, 0.05f, 0.08f, 0.97f), true).transform.SetAsFirstSibling();
|
||
|
||
var titleTmp = GetOrCreateText(panelGo.transform, "PanelTitle", "Credits", 56f, GoldText, TextAlignmentOptions.Center);
|
||
titleTmp.fontStyle = FontStyles.Bold;
|
||
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, "CREDITS_TITLE");
|
||
|
||
var body = GetOrCreateText(panelGo.transform, "Body", "Credits", 30f, new Color(0.82f, 0.8f, 0.74f, 1f), TextAlignmentOptions.Top);
|
||
SetRect((RectTransform)body.transform, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0f, 0f), new Vector2(900f, 600f));
|
||
BindLocalizedText(body.gameObject, "CREDITS_BODY");
|
||
|
||
GameObject backGo = GetOrCreateButtonChild(panelGo.transform, "BackButton", "Back");
|
||
SetRect((RectTransform)backGo.transform, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, 60f), new Vector2(260f, 64f));
|
||
StyleAsTextButton(backGo, 30f);
|
||
BindLocalizedButton(backGo, "BTN_BACK");
|
||
AssignReference(menuCtrl, "_btnCloseCredits", backGo.GetComponent<Button>());
|
||
}
|
||
|
||
private static void AssignReference(Object target, string propertyName, Object value)
|
||
{
|
||
AssignReference(target, propertyName, value, null);
|
||
}
|
||
|
||
private static void AssignReference(Object target, string propertyName, Object value, List<string> report)
|
||
{
|
||
SerializedObject serializedObject = new SerializedObject(target);
|
||
SerializedProperty property = serializedObject.FindProperty(propertyName);
|
||
if (property == null)
|
||
{
|
||
report?.Add($"{target.GetType().Name}.{propertyName} 字段不存在,未写入引用。");
|
||
return;
|
||
}
|
||
|
||
property.objectReferenceValue = value;
|
||
serializedObject.ApplyModifiedPropertiesWithoutUndo();
|
||
}
|
||
|
||
private static void AssignArrayReferences(Object target, string propertyName, IReadOnlyList<Object> values, List<string> report)
|
||
{
|
||
SerializedObject serializedObject = new SerializedObject(target);
|
||
SerializedProperty property = serializedObject.FindProperty(propertyName);
|
||
if (property == null || !property.isArray)
|
||
{
|
||
report.Add($"{target.GetType().Name}.{propertyName} 不是可写数组字段。");
|
||
return;
|
||
}
|
||
|
||
property.arraySize = values.Count;
|
||
for (int i = 0; i < values.Count; i++)
|
||
property.GetArrayElementAtIndex(i).objectReferenceValue = values[i];
|
||
serializedObject.ApplyModifiedPropertiesWithoutUndo();
|
||
}
|
||
|
||
private static void AssignAsset(Object target, string propertyName, List<string> report, bool required, params string[] candidates)
|
||
{
|
||
Object asset = FindFirstAsset(candidates);
|
||
if (asset == null && required)
|
||
report.Add($"未找到 {target.GetType().Name}.{propertyName} 需要的资产: {string.Join(" / ", candidates)}");
|
||
|
||
AssignReference(target, propertyName, asset, report);
|
||
}
|
||
|
||
private static Object FindFirstAsset(params string[] candidates)
|
||
{
|
||
foreach (string candidate in candidates)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(candidate))
|
||
continue;
|
||
|
||
string[] guids = AssetDatabase.FindAssets(candidate);
|
||
foreach (string guid in guids)
|
||
{
|
||
string path = AssetDatabase.GUIDToAssetPath(guid);
|
||
Object asset = AssetDatabase.LoadMainAssetAtPath(path);
|
||
if (asset != null && asset.name == candidate)
|
||
return asset;
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
private static Object FindFirstAssetByType<T>(params string[] candidates) where T : Object
|
||
{
|
||
foreach (string candidate in candidates)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(candidate))
|
||
continue;
|
||
|
||
string[] guids = AssetDatabase.FindAssets(candidate);
|
||
foreach (string guid in guids)
|
||
{
|
||
string path = AssetDatabase.GUIDToAssetPath(guid);
|
||
T asset = AssetDatabase.LoadAssetAtPath<T>(path);
|
||
if (asset != null && asset.name == candidate)
|
||
return asset;
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
private static Object FindFirstAssetWithExtension(string extension, params string[] candidates)
|
||
{
|
||
foreach (string candidate in candidates)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(candidate))
|
||
continue;
|
||
|
||
string[] guids = AssetDatabase.FindAssets(candidate);
|
||
foreach (string guid in guids)
|
||
{
|
||
string path = AssetDatabase.GUIDToAssetPath(guid);
|
||
if (string.IsNullOrEmpty(path) || !path.EndsWith(extension, System.StringComparison.OrdinalIgnoreCase))
|
||
continue;
|
||
|
||
Object asset = AssetDatabase.LoadMainAssetAtPath(path);
|
||
if (asset != null && asset.name == candidate)
|
||
return asset;
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
private static Object EnsureInputReaderAsset(List<string> report)
|
||
{
|
||
string[] existing = AssetDatabase.FindAssets("t:InputReaderSO");
|
||
if (existing != null && existing.Length > 0)
|
||
{
|
||
string firstPath = AssetDatabase.GUIDToAssetPath(existing[0]);
|
||
Object found = AssetDatabase.LoadMainAssetAtPath(firstPath);
|
||
if (found != null)
|
||
return found;
|
||
}
|
||
|
||
const string inputFolder = "Assets/_Game/Data/Player/Input";
|
||
EnsureFolder(inputFolder);
|
||
|
||
const string assetPath = "Assets/_Game/Data/Player/Input/InputReader.asset";
|
||
InputReaderSO created = ScriptableObject.CreateInstance<InputReaderSO>();
|
||
AssetDatabase.CreateAsset(created, assetPath);
|
||
AssetDatabase.SaveAssets();
|
||
AssetDatabase.Refresh();
|
||
|
||
report?.Add("未找到 InputReaderSO,已自动创建 Assets/_Game/Data/Player/Input/InputReader.asset。");
|
||
return created;
|
||
}
|
||
|
||
private static void EnsureFolder(string fullPath)
|
||
{
|
||
string[] parts = fullPath.Split('/');
|
||
if (parts.Length == 0 || parts[0] != "Assets")
|
||
return;
|
||
|
||
string current = "Assets";
|
||
for (int i = 1; i < parts.Length; i++)
|
||
{
|
||
string next = current + "/" + parts[i];
|
||
if (!AssetDatabase.IsValidFolder(next))
|
||
AssetDatabase.CreateFolder(current, parts[i]);
|
||
current = next;
|
||
}
|
||
}
|
||
|
||
private static void AddScaffoldNote(GameObject go, string message)
|
||
{
|
||
AddScaffoldNote(go, message, null);
|
||
}
|
||
|
||
private static void AddScaffoldNote(GameObject go, string message, List<string> report)
|
||
{
|
||
// 注意:不再添加 MonoBehaviour 组件,避免 Editor 程序集组件在 Play 模式下出现 Missing Script
|
||
report?.Add($"{go.name}: {message}");
|
||
Debug.Log($"[SceneScaffold] {go.name}: {message}");
|
||
}
|
||
|
||
private static void MarkDirtyAndLog(string scaffoldName, GameObject root, List<string> report)
|
||
{
|
||
EditorSceneManager.MarkSceneDirty(SceneManager.GetActiveScene());
|
||
Selection.activeGameObject = root;
|
||
|
||
if (report.Count == 0)
|
||
{
|
||
Debug.Log($"[SceneScaffoldTools] {scaffoldName} 完成。所有可自动补齐的对象与引用均已生成。", root);
|
||
return;
|
||
}
|
||
|
||
Debug.LogWarning($"[SceneScaffoldTools] {scaffoldName} 完成,但仍有 {report.Count} 项需要手工确认:\n- {string.Join("\n- ", report)}", root);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 为 VCam 上的 CinemachinePositionComposer 写入初始默认展示参数。
|
||
/// 这些値与 <see cref="CameraArea"/> 的默认値一致,确保脆架生成后 Scene 预览即有正确感觉。
|
||
/// 运行时 CameraStateController.ConfigureSlot 会在每次 SwitchArea 时用 per-area 配置覆写。
|
||
/// </summary>
|
||
private static void ApplyComposerDefaults(CinemachinePositionComposer composer)
|
||
{
|
||
if (composer == null) return;
|
||
|
||
// 屏幕位置:玩家稍低于中心,上方有更多视野
|
||
var comp = composer.Composition;
|
||
comp.ScreenPosition = new Vector2(0f, -0.15f);
|
||
comp.DeadZone.Enabled = true;
|
||
comp.DeadZone.Size = new Vector2(0.15f, 0.05f);
|
||
composer.Composition = comp;
|
||
|
||
// 阻尼:X 轻度缓冲,Y = 0(由 CameraAsymmetricDampingExtension 接管非对称 Y 阻尼)
|
||
composer.Damping = new Vector3(0.5f, 0f, 0f);
|
||
|
||
// Lookahead:水平引领预测开启,IgnoreY = true(平台游戏 Y 轴不预测,避免起跳时镜头猛拉)
|
||
var lah = composer.Lookahead;
|
||
lah.Enabled = true;
|
||
lah.Time = 0.28f;
|
||
lah.Smoothing = 5f;
|
||
lah.IgnoreY = true;
|
||
composer.Lookahead = lah;
|
||
}
|
||
}
|
||
|
||
} |