Files
zeling_v2/Assets/Scripts/Editor/SceneScaffoldTools.cs
2026-05-12 21:50:49 +08:00

905 lines
46 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 System.Reflection;
using BaseGames.Audio;
using BaseGames.Camera;
using BaseGames.Combat;
using BaseGames.Core;
using BaseGames.Core.Events;
using BaseGames.Core.Pool;
using BaseGames.Enemies;
using BaseGames.Input;
using BaseGames.Player;
using BaseGames.Player.States;
using BaseGames.Skills;
using BaseGames.UI;
using BaseGames.UI.HUD;
using BaseGames.UI.Menus;
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;
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);
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/Data/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);
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);
EnsureAudioSources(audioManagerGo, audioManager, report);
AssignReference(registrar, "_deathRespawnService", deathRespawnService);
AssignReference(registrar, "_sceneService", sceneService);
AssignReference(registrar, "_eventChannelRegistry", registry);
AssignReference(gameManager, "_settingsManager", settingsManager);
AssignReference(gameManager, "_deathRespawnService", deathRespawnService);
AssignReference(gameManager, "_sceneService", sceneService);
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, "_onSceneLoaded", report, false, "EVT_SceneLoaded");
AssignAsset(sceneService, "_onFadeInRequest", report, false, "EVT_FadeInRequest");
AssignAsset(sceneService, "_onFadeOutRequest", report, false, "EVT_FadeOutRequest");
AssignAsset(sceneLoader, "_onSceneLoadRequest", report, false, "EVT_SceneLoadRequest");
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(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);
}
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。");
}
[MenuItem("BaseGames/Tools/Scaffold Test Room")]
public static void ScaffoldTestRoom()
{
var report = new List<string>();
EnsureEventChannelAssets(report);
Object inputReaderAsset = FindFirstAssetByType<InputReaderSO>("InputReader", "InputReaderSO");
if (inputReaderAsset == null)
inputReaderAsset = EnsureInputReaderAsset(report);
GameObject root = GetOrCreateRoot("[TestRoom]");
Transform environment = GetOrCreateChild(root.transform, "[Environment]");
Transform playerRoot = GetOrCreateChild(root.transform, "[Player]");
Transform enemyRoot = GetOrCreateChild(root.transform, "[Enemy]");
Transform savePointRoot = GetOrCreateChild(root.transform, "[SavePoint]");
Transform cameraRoot = GetOrCreateChild(root.transform, "[Camera]");
DisableRenderCamerasUnderRoot(root.transform, report);
Object playerStatsConfigAsset = EnsurePlayerStatsConfigAsset(report);
Object playerMovementConfigAsset = EnsurePlayerMovementConfigAsset(report);
Object playerAnimationConfigAsset = EnsurePlayerAnimationConfigAsset(report);
GameObject groundGridGo = GetOrCreateChild(environment, "GroundGrid").gameObject;
GetOrAddComponent<Grid>(groundGridGo);
GameObject groundGo = GetOrCreateChild(groundGridGo.transform, "Ground").gameObject;
TilemapCollider2D tilemapCollider = GetOrAddComponent<TilemapCollider2D>(groundGo);
tilemapCollider.usedByComposite = true;
GetOrAddComponent<Tilemap>(groundGo);
GetOrAddComponent<TilemapRenderer>(groundGo);
Rigidbody2D groundBody = GetOrAddComponent<Rigidbody2D>(groundGo);
groundBody.bodyType = RigidbodyType2D.Static;
GetOrAddComponent<CompositeCollider2D>(groundGo);
SetLayer(groundGo, "Ground", report);
GameObject fallbackFloorGo = GetOrCreateChild(environment, "GroundFallback").gameObject;
fallbackFloorGo.transform.position = new Vector3(0f, -2f, 0f);
BoxCollider2D fallbackFloorCollider = GetOrAddComponent<BoxCollider2D>(fallbackFloorGo);
fallbackFloorCollider.size = new Vector2(40f, 1f);
Rigidbody2D fallbackFloorBody = GetOrAddComponent<Rigidbody2D>(fallbackFloorGo);
fallbackFloorBody.bodyType = RigidbodyType2D.Static;
SetLayer(fallbackFloorGo, "Ground", report);
EnsureVisualSprite(fallbackFloorGo.transform, "Visual", new Color(0.25f, 0.7f, 0.3f, 1f), new Vector2(40f, 1f), -10);
AddScaffoldNote(fallbackFloorGo, "保底地板用于测试:若 Tilemap 资源未提供碰撞形状,角色仍不会穿地。", report);
GameObject navSurfaceGo = GetOrCreateChild(environment, "NavSurfaceRoot").gameObject;
GetOrAddComponent<NavSurface>(navSurfaceGo);
AddScaffoldNote(navSurfaceGo, "NavSurface 已创建,仍需在 Unity Inspector 中点击 Bake。", report);
GameObject playerGo = GetOrCreateChild(playerRoot, "Player").gameObject;
RemoveMissingScripts(playerGo, recursive: true, report);
playerGo.transform.position = new Vector3(0f, 1f, 0f);
playerGo.tag = "Player";
SetLayer(playerGo, "Player", report);
PlayerController playerController = GetOrAddComponent<PlayerController>(playerGo);
InputBuffer inputBuffer = GetOrAddComponent<InputBuffer>(playerGo);
PlayerMovement playerMovement = GetOrAddComponent<PlayerMovement>(playerGo);
PlayerStats playerStats = GetOrAddComponent<PlayerStats>(playerGo);
PlayerCombat playerCombat = GetOrAddComponent<PlayerCombat>(playerGo);
FormController formController = GetOrAddComponent<FormController>(playerGo);
WeaponManager weaponManager = GetOrAddComponent<WeaponManager>(playerGo);
SkillManager skillManager = GetOrAddComponent<SkillManager>(playerGo);
SpringSystem springSystem = GetOrAddComponent<SpringSystem>(playerGo);
Component parrySystem = AddComponentIfTypeExists(playerGo, "BaseGames.Parry.ParrySystem", report);
ShieldComponent shieldComponent = GetOrAddComponent<ShieldComponent>(playerGo);
Rigidbody2D playerBody = GetOrAddComponent<Rigidbody2D>(playerGo);
playerBody.bodyType = RigidbodyType2D.Dynamic;
playerBody.gravityScale = 2f;
playerBody.constraints = RigidbodyConstraints2D.FreezeRotation;
GetOrAddComponent<CapsuleCollider2D>(playerGo);
Animancer.AnimancerComponent playerAnimancer = GetOrAddComponent<Animancer.AnimancerComponent>(playerGo);
EnsureVisualSprite(playerGo.transform, "Visual", new Color(0.3f, 0.7f, 1f, 1f), new Vector2(0.8f, 1.6f), 20);
GameObject groundCheckGo = GetOrCreateChild(playerGo.transform, "GroundCheck").gameObject;
groundCheckGo.transform.localPosition = new Vector3(0f, -0.5f, 0f);
GameObject hitBoxGroundGo = GetOrCreateChild(playerGo.transform, "HitBoxGround").gameObject;
BoxCollider2D hitBoxGroundCollider = GetOrAddComponent<BoxCollider2D>(hitBoxGroundGo);
hitBoxGroundCollider.isTrigger = true;
HitBox hitBoxGround = GetOrAddComponent<HitBox>(hitBoxGroundGo);
SetLayer(hitBoxGroundGo, "PlayerHitBox", report);
GameObject hitBoxUpGo = GetOrCreateChild(playerGo.transform, "HitBoxUp").gameObject;
BoxCollider2D hitBoxUpCollider = GetOrAddComponent<BoxCollider2D>(hitBoxUpGo);
hitBoxUpCollider.isTrigger = true;
HitBox hitBoxUp = GetOrAddComponent<HitBox>(hitBoxUpGo);
SetLayer(hitBoxUpGo, "PlayerHitBox", report);
GameObject hitBoxDownGo = GetOrCreateChild(playerGo.transform, "HitBoxDown").gameObject;
BoxCollider2D hitBoxDownCollider = GetOrAddComponent<BoxCollider2D>(hitBoxDownGo);
hitBoxDownCollider.isTrigger = true;
HitBox hitBoxDown = GetOrAddComponent<HitBox>(hitBoxDownGo);
SetLayer(hitBoxDownGo, "PlayerHitBox", report);
GameObject hitBoxAirGo = GetOrCreateChild(playerGo.transform, "HitBoxAir").gameObject;
BoxCollider2D hitBoxAirCollider = GetOrAddComponent<BoxCollider2D>(hitBoxAirGo);
hitBoxAirCollider.isTrigger = true;
HitBox hitBoxAir = GetOrAddComponent<HitBox>(hitBoxAirGo);
SetLayer(hitBoxAirGo, "PlayerHitBox", report);
GameObject playerHurtBoxGo = GetOrCreateChild(playerGo.transform, "HurtBox").gameObject;
CapsuleCollider2D playerHurtCollider = GetOrAddComponent<CapsuleCollider2D>(playerHurtBoxGo);
playerHurtCollider.isTrigger = true;
HurtBox playerHurtBox = GetOrAddComponent<HurtBox>(playerHurtBoxGo);
SetLayer(playerHurtBoxGo, "PlayerHurtBox", report);
GameObject enemyGo = GetOrCreateChild(enemyRoot, "BasicEnemy").gameObject;
SetLayer(enemyGo, "Enemy", report);
EnemyBase enemyBase = GetOrAddComponent<EnemyBase>(enemyGo);
EnemyStats enemyStats = GetOrAddComponent<EnemyStats>(enemyGo);
GetOrAddComponent<Rigidbody2D>(enemyGo).bodyType = RigidbodyType2D.Dynamic;
GetOrAddComponent<CapsuleCollider2D>(enemyGo);
GetOrAddComponent<Animancer.AnimancerComponent>(enemyGo);
EnsureVisualSprite(enemyGo.transform, "Visual", new Color(1f, 0.45f, 0.45f, 1f), new Vector2(0.8f, 1.4f), 15);
AddComponentIfTypeExists(enemyGo, "PathBerserker2d.NavAgent", report);
GameObject enemyHurtBoxGo = GetOrCreateChild(enemyGo.transform, "HurtBox").gameObject;
CapsuleCollider2D enemyHurtCollider = GetOrAddComponent<CapsuleCollider2D>(enemyHurtBoxGo);
enemyHurtCollider.isTrigger = true;
HurtBox enemyHurtBox = GetOrAddComponent<HurtBox>(enemyHurtBoxGo);
SetLayer(enemyHurtBoxGo, "EnemyHurtBox", report);
GameObject savePointGo = GetOrCreateChild(savePointRoot, "SavePointObject").gameObject;
SavePoint savePoint = GetOrAddComponent<SavePoint>(savePointGo);
BoxCollider2D savePointCollider = GetOrAddComponent<BoxCollider2D>(savePointGo);
savePointCollider.isTrigger = true;
SetLayer(savePointGo, "TriggerZone", report);
EnsureVisualSprite(savePointGo.transform, "Visual", new Color(1f, 0.9f, 0.3f, 0.9f), new Vector2(0.8f, 1.2f), 10);
AssignAsset(savePoint, "_onSavePointActivated", report, false, "EVT_SavePointActivated");
AssignAsset(savePoint, "_onFastTravelOpen", report, false, "EVT_FastTravelOpen");
GameObject roomCameraGo = GetOrCreateChild(cameraRoot, "RoomCamera").gameObject;
CinemachineCamera roomCameraComponent = GetOrAddComponent<CinemachineCamera>(roomCameraGo);
RoomCamera roomCamera = GetOrAddComponent<RoomCamera>(roomCameraGo);
UnityEngine.Camera roomRenderCamera = roomCameraGo.GetComponent<UnityEngine.Camera>();
if (roomRenderCamera != null)
{
roomRenderCamera.orthographic = false;
roomRenderCamera.fieldOfView = 60f;
roomRenderCamera.enabled = false;
report.Add("RoomCamera 上的 Unity Camera 已禁用Additive 测试时仅保留 Persistent/Main Camera 渲染)。");
}
AudioListener roomAudioListener = roomCameraGo.GetComponent<AudioListener>();
if (roomAudioListener != null)
{
Undo.DestroyObjectImmediate(roomAudioListener);
report.Add("RoomCamera 上的 AudioListener 已移除Additive 测试时由 Persistent/Main Camera 保留唯一监听器)。");
}
GameObject roomBoundaryGo = GetOrCreateChild(roomCameraGo.transform, "RoomBoundary").gameObject;
RemoveMissingScripts(roomBoundaryGo, recursive: true, report);
RoomVisibleArea roomVisibleArea = GetOrAddComponent<RoomVisibleArea>(roomBoundaryGo);
CinemachineConfiner2D confiner2D = GetOrAddComponent<CinemachineConfiner2D>(roomCameraGo);
AssignReference(roomCamera, "_visibleArea", roomVisibleArea);
AssignReferenceByCandidates(roomCameraComponent, playerGo.transform, report, "Follow", "m_Follow");
AssignReferenceByCandidates(confiner2D, roomVisibleArea.Collider, report, "BoundingShape2D", "m_BoundingShape2D");
AddScaffoldNote(roomBoundaryGo, "RoomBoundary 已挂 RoomVisibleArea需要手工编辑 PolygonCollider2D 顶点定义房间边界。", report);
if (inputReaderAsset != null)
{
AssignReference(playerController, "_inputReader", inputReaderAsset, report);
AssignReference(inputBuffer, "_inputReader", inputReaderAsset, report);
}
else
{
report.Add("未找到 InputReader 资产PlayerController/InputBuffer 的 _inputReader 未能绑定。");
}
AssignReference(playerController, "_movement", playerMovement, report);
AssignReference(playerController, "_animancer", playerAnimancer, report);
AssignReference(playerController, "_combat", playerCombat, report);
AssignReference(playerController, "_formController", formController, report);
AssignReference(playerController, "_weaponManager", weaponManager, report);
AssignReference(playerController, "_skillManager", skillManager, report);
AssignReference(playerController, "_springSystem", springSystem, report);
AssignReference(playerController, "_parrySystem", parrySystem, report);
AssignReference(playerController, "_shield", shieldComponent, report);
AssignReference(playerController, "_statsConfig", playerStatsConfigAsset, report);
AssignReference(playerController, "_movementConfig", playerMovementConfigAsset, report);
AssignReference(playerController, "_animConfig", playerAnimationConfigAsset, report);
AssignReference(playerStats, "_config", playerStatsConfigAsset, report);
AssignReference(playerMovement, "_config", playerMovementConfigAsset, report);
AssignReference(playerMovement, "_groundCheck", groundCheckGo.transform, report);
AssignLayerMask(playerMovement, "_groundLayer", "Ground", report);
AssignReference(playerCombat, "_weaponManager", weaponManager, report);
AssignReference(playerCombat, "_hitBoxGround", hitBoxGround, report);
AssignReference(playerCombat, "_hitBoxUp", hitBoxUp, report);
AssignReference(playerCombat, "_hitBoxDown", hitBoxDown, report);
AssignReference(playerCombat, "_hitBoxAir", hitBoxAir, report);
AssignAsset(playerController, "_onPlayerDied", report, false, "EVT_PlayerDied");
AssignAsset(playerController, "_onHPChanged", report, false, "EVT_HPChanged");
AssignReference(playerController, "_stats", playerStats);
AssignReference(playerController, "_hurtBox", playerHurtBox);
AssignAsset(playerStats, "_onHPChanged", report, false, "EVT_HPChanged");
AssignAsset(playerStats, "_onMaxHPChanged", report, false, "EVT_MaxHPChanged");
AssignAsset(playerStats, "_onSoulPowerChanged", report, false, "EVT_SoulPowerChanged");
AssignAsset(playerStats, "_onSpiritPowerChanged", report, false, "EVT_SpiritPowerChanged");
AssignAsset(playerStats, "_onSpringChargesChanged", report, false, "EVT_SpringChargesChanged");
AssignAsset(playerStats, "_onGeoChanged", report, false, "EVT_GeoChanged");
AssignAsset(playerStats, "_onAbilityUnlocked", report, false, "EVT_AbilityUnlocked");
AssignAsset(playerStats, "_onPlayerDied", report, false, "EVT_PlayerDied");
AssignAsset(playerHurtBox, "_onDamageDealt", report, false, "EVT_DamageDealt");
AssignAsset(playerHurtBox, "_onHitConfirmed", report, false, "EVT_HitConfirmed");
AssignAsset(enemyHurtBox, "_onDamageDealt", report, false, "EVT_DamageDealt");
AssignAsset(enemyHurtBox, "_onHitConfirmed", report, false, "EVT_HitConfirmed");
AssignReference(enemyBase, "_stats", enemyStats);
AssignAsset(enemyBase, "_onEnemyDied", report, false, "EVT_EnemyDied");
AssignAsset(enemyBase, "_statsSO", report, false, "BasicEnemyStats", "EnemyStatsSO");
AddScaffoldNote(playerGo, "Player 已生成基础控制节点。PlayerMovement、Combat、Form、Weapon、Skill 等复杂依赖需按实际 prefab/配置继续补齐。", report);
AddScaffoldNote(enemyGo, "Enemy 已生成基础节点。行为树、导航参数、动画配置和战斗组件仍需手工配置。", report);
MarkDirtyAndLog("TestRoom 场景脚手架", root, report);
}
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 Component AddComponentIfTypeExists(GameObject go, string typeName, List<string> report)
{
System.Type type = FindType(typeName);
if (type == null)
{
report.Add($"未找到类型 {typeName},已跳过对应组件挂载。");
return null;
}
Component component = go.GetComponent(type);
if (component != null)
return component;
return Undo.AddComponent(go, type);
}
private static System.Type FindType(string typeName)
{
foreach (var assembly in System.AppDomain.CurrentDomain.GetAssemblies())
{
System.Type type = assembly.GetType(typeName);
if (type != null)
return type;
}
return null;
}
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;
}
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 AssignReferenceByCandidates(Object target, Object value, List<string> report, params string[] candidates)
{
if (target == null || candidates == null || candidates.Length == 0)
return;
foreach (string candidate in candidates)
{
if (TryAssignSerializedReference(target, candidate, value))
return;
if (TryAssignMemberReference(target, candidate, value))
return;
}
report?.Add($"{target.GetType().Name} 未找到可写引用字段: {string.Join(" / ", candidates)}");
}
private static bool TryAssignSerializedReference(Object target, string propertyName, Object value)
{
SerializedObject serializedObject = new SerializedObject(target);
SerializedProperty property = serializedObject.FindProperty(propertyName);
if (property == null || property.propertyType != SerializedPropertyType.ObjectReference)
return false;
property.objectReferenceValue = value;
serializedObject.ApplyModifiedPropertiesWithoutUndo();
return true;
}
private static bool TryAssignMemberReference(Object target, string memberName, Object value)
{
System.Type targetType = target.GetType();
PropertyInfo property = targetType.GetProperty(memberName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (property != null && property.CanWrite && typeof(Object).IsAssignableFrom(property.PropertyType))
{
if (value == null || property.PropertyType.IsAssignableFrom(value.GetType()))
{
property.SetValue(target, value);
EditorUtility.SetDirty(target);
return true;
}
}
FieldInfo field = targetType.GetField(memberName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (field != null && typeof(Object).IsAssignableFrom(field.FieldType))
{
if (value == null || field.FieldType.IsAssignableFrom(value.GetType()))
{
field.SetValue(target, value);
EditorUtility.SetDirty(target);
return true;
}
}
return false;
}
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/Data/Input";
if (!AssetDatabase.IsValidFolder("Assets/Data"))
AssetDatabase.CreateFolder("Assets", "Data");
if (!AssetDatabase.IsValidFolder(inputFolder))
AssetDatabase.CreateFolder("Assets/Data", "Input");
const string assetPath = "Assets/Data/Input/InputReader.asset";
InputReaderSO created = ScriptableObject.CreateInstance<InputReaderSO>();
AssetDatabase.CreateAsset(created, assetPath);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
report?.Add("未找到 InputReaderSO已自动创建 Assets/Data/Input/InputReader.asset。");
return created;
}
private static Object EnsurePlayerStatsConfigAsset(List<string> report)
{
Object existing = FindFirstAssetByType<PlayerStatsSO>("PlayerStats", "PLY_PlayerStats", "PlayerStatsSO");
if (existing != null)
return existing;
const string folder = "Assets/Data/Player";
EnsureFolder(folder);
const string assetPath = "Assets/Data/Player/PLY_PlayerStats.asset";
PlayerStatsSO created = ScriptableObject.CreateInstance<PlayerStatsSO>();
AssetDatabase.CreateAsset(created, assetPath);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
report?.Add("未找到 PlayerStatsSO已自动创建 Assets/Data/Player/PLY_PlayerStats.asset。");
return created;
}
private static Object EnsurePlayerMovementConfigAsset(List<string> report)
{
Object existing = FindFirstAssetByType<PlayerMovementConfigSO>("PlayerMovementConfig", "PLY_PlayerMovementConfig", "PlayerMovementConfigSO");
if (existing != null)
return existing;
const string folder = "Assets/Data/Player";
EnsureFolder(folder);
const string assetPath = "Assets/Data/Player/PLY_PlayerMovementConfig.asset";
PlayerMovementConfigSO created = ScriptableObject.CreateInstance<PlayerMovementConfigSO>();
AssetDatabase.CreateAsset(created, assetPath);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
report?.Add("未找到 PlayerMovementConfigSO已自动创建 Assets/Data/Player/PLY_PlayerMovementConfig.asset。");
return created;
}
private static Object EnsurePlayerAnimationConfigAsset(List<string> report)
{
Object existing = FindFirstAssetByType<PlayerAnimationConfigSO>("PlayerAnimationConfig", "PLY_PlayerAnimationConfig", "PlayerAnimationConfigSO");
if (existing != null)
return existing;
const string folder = "Assets/Data/Player";
EnsureFolder(folder);
const string assetPath = "Assets/Data/Player/PLY_PlayerAnimationConfig.asset";
PlayerAnimationConfigSO created = ScriptableObject.CreateInstance<PlayerAnimationConfigSO>();
AssetDatabase.CreateAsset(created, assetPath);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
report?.Add("未找到 PlayerAnimationConfigSO已自动创建 Assets/Data/Player/PLY_PlayerAnimationConfig.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 RemoveMissingScripts(GameObject go, bool recursive, List<string> report)
{
if (go == null)
return;
int removed = 0;
if (!recursive)
{
removed = RemoveMissingScriptsOnSingleObject(go);
}
else
{
var stack = new Stack<Transform>();
stack.Push(go.transform);
while (stack.Count > 0)
{
Transform current = stack.Pop();
removed += RemoveMissingScriptsOnSingleObject(current.gameObject);
foreach (Transform child in current)
stack.Push(child);
}
}
if (removed > 0)
report?.Add($"{go.name}: 已清理 Missing Behaviour x{removed}。");
}
private static int RemoveMissingScriptsOnSingleObject(GameObject go)
{
int before = GameObjectUtility.GetMonoBehavioursWithMissingScriptCount(go);
if (before <= 0)
return 0;
Undo.RegisterCompleteObjectUndo(go, "Remove Missing Scripts");
GameObjectUtility.RemoveMonoBehavioursWithMissingScript(go);
int after = GameObjectUtility.GetMonoBehavioursWithMissingScriptCount(go);
return before - after;
}
private static void SetLayer(GameObject go, string layerName, List<string> report)
{
int layer = LayerMask.NameToLayer(layerName);
if (layer < 0)
{
report.Add($"Layer '{layerName}' 不存在,{go.name} 保持默认 Layer。");
return;
}
go.layer = layer;
}
private static void AssignLayerMask(Object target, string propertyName, string layerName, List<string> report)
{
int layer = LayerMask.NameToLayer(layerName);
if (layer < 0)
{
report?.Add($"Layer '{layerName}' 不存在,{target.GetType().Name}.{propertyName} 无法写入。");
return;
}
SerializedObject serializedObject = new SerializedObject(target);
SerializedProperty property = serializedObject.FindProperty(propertyName);
if (property == null)
{
report?.Add($"{target.GetType().Name}.{propertyName} 字段不存在,未写入 LayerMask。");
return;
}
property.intValue = 1 << layer;
serializedObject.ApplyModifiedPropertiesWithoutUndo();
}
private static void EnsureVisualSprite(Transform parent, string childName, Color color, Vector2 size, int sortingOrder)
{
Transform visualTransform = GetOrCreateChild(parent, childName);
SpriteRenderer renderer = GetOrAddComponent<SpriteRenderer>(visualTransform.gameObject);
renderer.sprite = GetBuiltinDefaultSprite();
renderer.color = color;
renderer.drawMode = SpriteDrawMode.Sliced;
renderer.size = size;
renderer.sortingOrder = sortingOrder;
visualTransform.localPosition = Vector3.zero;
visualTransform.localRotation = Quaternion.identity;
visualTransform.localScale = Vector3.one;
}
private static Sprite GetBuiltinDefaultSprite()
=> AssetDatabase.GetBuiltinExtraResource<Sprite>("UI/Skin/UISprite.psd");
private static void AddScaffoldNote(GameObject go, string message)
{
AddScaffoldNote(go, message, null);
}
private static void DisableRenderCamerasUnderRoot(Transform root, List<string> report)
{
if (root == null)
return;
UnityEngine.Camera[] cameras = root.GetComponentsInChildren<UnityEngine.Camera>(true);
foreach (UnityEngine.Camera camera in cameras)
{
if (camera == null)
continue;
if (camera.enabled)
{
camera.enabled = false;
report?.Add($"已禁用 TestRoom 内渲染相机: {camera.gameObject.name}");
}
}
}
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);
}
}
}