- Add AddressableRules.cs: single source of truth for prefix->group and prefix->label rules - Add AddressableRuleSyncWindow.cs: scan/fix/export-CSV tool (BaseGames > Addressables > Rule Sync) - AddressableBatchTool.cs: delegate DeriveGroupName to AddressableRules, remove duplicate PrefixGroupMap - AddressKeys.cs: add Labels constants (Preload, Poolable, Enemy, BGM, SFX, Charms, Config, Weapon) - Docs/Standards/AddressablesLabelSpec.md: new label naming & assignment spec - Docs/Standards/AssetFolderSpec.md: update Addressables group strategy section - SplashScreenController.cs: fix MainMenu loading flow - BootFlowSetupWizard.cs / SceneScaffoldTools.cs: scene scaffold fixes - PlayerInputActions: set UI/Point to Pass-Through type - Persistent.unity: add BootSequencer to auto-load MainMenu on play - EditorBuildSettings.asset: register scenes for build Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
718 lines
40 KiB
C#
718 lines
40 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 PathBerserker2d;
|
||
using Unity.Cinemachine;
|
||
using UnityEditor;
|
||
using UnityEditor.SceneManagement;
|
||
using UnityEngine;
|
||
using UnityEngine.SceneManagement;
|
||
using UnityEngine.Tilemaps;
|
||
using UnityEngine.UI;
|
||
|
||
namespace BaseGames.Editor
|
||
{
|
||
public static class SceneScaffoldTools
|
||
{
|
||
|
||
[MenuItem("BaseGames/Tools/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;
|
||
|
||
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);
|
||
|
||
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);
|
||
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;
|
||
mainCameraGo.tag = "MainCamera";
|
||
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;
|
||
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);
|
||
|
||
// ── 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);
|
||
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);
|
||
AssignAsset(loadingMgr, "_onLoadingStarted", report, false, "EVT_LoadingStarted");
|
||
AssignAsset(loadingMgr, "_onLoadingComplete", report, false, "EVT_LoadingComplete");
|
||
AssignAsset(loadingMgr, "_onLoadingProgressUpdated", report, false, "EVT_LoadingProgressUpdated");
|
||
loadingCanvasGo.SetActive(false);
|
||
|
||
report.Add("Canvas_Splash:请将工作室 Logo CanvasGroup 赋给 _studioLogoGroup,游戏标题 CanvasGroup 赋给 _gameTitleGroup。");
|
||
report.Add("Canvas_Loading:请为 LoadingScreenManager 绑定 _progressBar(Slider)和 _loadingPanel(GameObject)。");
|
||
|
||
EnsureAudioSources(audioManagerGo, audioManager, report);
|
||
|
||
AssignReference(registrar, "_deathRespawnService", deathRespawnService);
|
||
AssignReference(registrar, "_sceneService", sceneService);
|
||
AssignReference(registrar, "_eventChannelRegistry", registry);
|
||
AssignReference(registrar, "_saveManager", gameSaveManager);
|
||
|
||
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");
|
||
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(settingsManager, "_defaultSettings", report, false, "SET_GlobalSettings");
|
||
|
||
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, "_pauseMenuRoot", pauseRootGo);
|
||
AssignReference(uiManager, "_deathScreenRoot", deathRootGo);
|
||
AssignReference(uiManager, "_settingsRoot", settingsRootGo);
|
||
AssignReference(uiManager, "_mapRoot", mapRootGo);
|
||
AssignReference(uiManager, "_shopRoot", shopRootGo);
|
||
AssignAsset(uiManager, "_onGameStateChanged", report, true, "EVT_GameStateChanged", "EVT_GameState");
|
||
AssignAsset(uiManager, "_onPauseRequested", 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");
|
||
|
||
AssignReference(deathScreenController, "_btnRespawn", respawnButton);
|
||
AssignAsset(deathScreenController, "_onPlayerDied", report, true, "EVT_PlayerDied");
|
||
AssignAsset(deathScreenController, "_onDeathScreenConfirmed", report, true, "EVT_DeathScreenConfirmed");
|
||
|
||
AddScaffoldNote(hudRootGo, "HUDController 已挂载。其内部图片/文本/图标 Prefab 依赖较多,需后续手工补 UI 资源与事件频道。", report);
|
||
|
||
MarkDirtyAndLog("Persistent 场景脚手架", root, report);
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
// Scaffold Main Menu Scene
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// 在当前活动场景中生成完整的主菜单场景层级结构:
|
||
/// [MainMenu] → [Canvas_MainMenu] → 主按钮组 / SaveSlotPanel / SettingsPanel / CreditsPanel
|
||
/// 自动绑定所有已存在的相关事件频道 SO 资产。
|
||
/// </summary>
|
||
[MenuItem("BaseGames/Tools/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);
|
||
|
||
// ── 主菜单控制器 ──────────────────────────────────────────────────
|
||
MainMenuController menuCtrl = GetOrAddComponent<MainMenuController>(canvasGo);
|
||
AssignAsset(menuCtrl, "_onGameStateChanged", report, false, "EVT_GameStateChanged", "EVT_GameState");
|
||
AssignAsset(menuCtrl, "_onSceneLoadRequest", report, false, "EVT_SceneLoadRequest");
|
||
AssignAsset(menuCtrl, "_onSlotConfirmed", report, false, "EVT_SlotConfirmed");
|
||
|
||
// ── 主按钮区域 ────────────────────────────────────────────────────
|
||
GameObject menuPanelGo = GetOrCreateChild(canvasGo.transform, "MenuPanel").gameObject;
|
||
GetOrAddComponent<VerticalLayoutGroup>(menuPanelGo);
|
||
|
||
GameObject btnNewGameGo = GetOrCreateButtonChild(menuPanelGo.transform, "Btn_NewGame", "新游戏");
|
||
GameObject btnContinueGo = GetOrCreateButtonChild(menuPanelGo.transform, "Btn_Continue", "继续");
|
||
GameObject btnSettingsGo = GetOrCreateButtonChild(menuPanelGo.transform, "Btn_Settings", "设置");
|
||
GameObject btnCreditsGo = GetOrCreateButtonChild(menuPanelGo.transform, "Btn_Credits", "制作团队");
|
||
GameObject btnQuitGo = GetOrCreateButtonChild(menuPanelGo.transform, "Btn_Quit", "退出");
|
||
|
||
AssignReference(menuCtrl, "_btnNewGame", btnNewGameGo.GetComponent<Button>());
|
||
AssignReference(menuCtrl, "_btnContinue", btnContinueGo.GetComponent<Button>());
|
||
AssignReference(menuCtrl, "_btnSettings", btnSettingsGo.GetComponent<Button>());
|
||
AssignReference(menuCtrl, "_btnCredits", btnCreditsGo.GetComponent<Button>());
|
||
AssignReference(menuCtrl, "_btnQuit", btnQuitGo.GetComponent<Button>());
|
||
AssignReference(menuCtrl, "_menuPanel", menuPanelGo);
|
||
|
||
// ── SaveSlotPanel ─────────────────────────────────────────────────
|
||
GameObject saveSlotPanelGo = GetOrCreateChild(canvasGo.transform, "SaveSlotPanel").gameObject;
|
||
saveSlotPanelGo.SetActive(false);
|
||
SaveSlotController saveSlotCtrl = GetOrAddComponent<SaveSlotController>(saveSlotPanelGo);
|
||
AssignAsset(saveSlotCtrl, "_onSlotConfirmed", report, false, "EVT_SlotConfirmed");
|
||
AssignReference(menuCtrl, "_saveSlotPanel", saveSlotPanelGo);
|
||
AssignReference(menuCtrl, "_saveSlotController", saveSlotCtrl);
|
||
|
||
// ── SettingsPanel ─────────────────────────────────────────────────
|
||
GameObject settingsPanelGo = GetOrCreateChild(canvasGo.transform, "SettingsPanel").gameObject;
|
||
settingsPanelGo.SetActive(false);
|
||
AssignReference(menuCtrl, "_settingsPanel", settingsPanelGo);
|
||
|
||
// ── CreditsPanel ──────────────────────────────────────────────────
|
||
GameObject creditsPanelGo = GetOrCreateChild(canvasGo.transform, "CreditsPanel").gameObject;
|
||
creditsPanelGo.SetActive(false);
|
||
AssignReference(menuCtrl, "_creditsPanel", creditsPanelGo);
|
||
|
||
report.Add("设置 MainMenuController._firstGameSceneKey 为第一个游戏场景的 Addressable Key(字符串)。");
|
||
report.Add("SaveSlotPanel 需要补充 3 个存档槽 Button 引用(_slot0Btn / _slot1Btn / _slot2Btn)。");
|
||
report.Add("建议为 MenuPanel 添加 RectTransform 入场动画所需的锚点配置,参考 MainMenuController._menuPanel 的偏移量。");
|
||
|
||
MarkDirtyAndLog("Main Menu 场景脚手架", root, report);
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
// Scaffold Game Room
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// 在当前活动场景中生成标准游戏关卡房间的完整层级结构:
|
||
/// [RoomRoot] → [Camera] / [SpawnPoints] / [Environment] / [Transitions]
|
||
/// 可配合 SceneObjectPlacerTool 在层级内快速追加更多对象。
|
||
/// </summary>
|
||
[MenuItem("BaseGames/Tools/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("Ground");
|
||
if (groundLayer >= 0) groundTileGo.layer = groundLayer;
|
||
else report.Add("Layer 'Ground' 不存在,请在 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();
|
||
}
|
||
|
||
// ── Report ─────────────────────────────────────────────────────
|
||
report.Add("在 RoomController._roomId 填写唯一房间 ID(如 \"Room_Forest_01\")。");
|
||
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);
|
||
}
|
||
|
||
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);
|
||
report.Add("AudioManager 已生成 2 个 BGM Source 和 6 个 SFX Source,AudioMixer 仍需手工指定。");
|
||
}
|
||
|
||
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;
|
||
GetOrAddComponent<CanvasScaler>(canvasGo);
|
||
GetOrAddComponent<GraphicRaycaster>(canvasGo);
|
||
return canvasGo;
|
||
}
|
||
|
||
/// <summary>在指定父节点下创建一个带 Button 的菜单按钮子节点(幂等)。文本由美术后续补充。</summary>
|
||
private static GameObject GetOrCreateButtonChild(Transform parent, string name, string label)
|
||
{
|
||
GameObject go = GetOrCreateChild(parent, name).gameObject;
|
||
GetOrAddComponent<Image>(go);
|
||
GetOrAddComponent<Button>(go);
|
||
_ = label; // 占位:文本内容由美术在 Prefab/Scene 中设置
|
||
return go;
|
||
}
|
||
|
||
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;
|
||
}
|
||
}
|
||
|
||
} |