Files
zeling_v2/Assets/_Game/Scripts/Editor/Scene/SceneScaffoldTools.cs
Joywayer 520f84999b Add enemy respawner and related components for room lifecycle management
- Implemented EnemyRespawner to manage enemy spawning and respawning within rooms.
- Added IRoomLifecycle interface for room activation and dormancy handling.
- Created supporting classes and metadata for enemy perception and threat assessment.
- Established streaming system components for room state management and transitions.
- Added necessary metadata files for new scripts to ensure proper integration with Unity.
2026-05-23 21:23:09 +08:00

831 lines
47 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
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);
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";
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);
// CinemachinePositionComposerBody 阶段组件必须存在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 绑定 _progressBarSlider和 _loadingPanelGameObject。");
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");
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, "_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);
// ── 流式加载系统 ──────────────────────────────────────────────────
ScaffoldStreamingSystem(services, report);
MarkDirtyAndLog("Persistent 场景脚手架", root, report);
}
// ─────────────────────────────────────────────────────────────────────
// 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);
// ── 主菜单控制器 ──────────────────────────────────────────────────
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/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);
report.Add("AudioManager 已生成 2 个 BGM Source 和 6 个 SFX SourceAudioMixer 仍需手工指定。");
}
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;
}
}
}