多轮审查和修复

This commit is contained in:
2026-05-12 15:34:08 +08:00
parent f55d2a57c3
commit ebbbb7332e
805 changed files with 838724 additions and 1905 deletions

View File

@@ -0,0 +1,904 @@
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.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);
}
}
}