摄像机区域的架构改动
This commit is contained in:
566
Assets/_Game/Scripts/Editor/Scene/SceneScaffoldTools.cs
Normal file
566
Assets/_Game/Scripts/Editor/Scene/SceneScaffoldTools.cs
Normal file
@@ -0,0 +1,566 @@
|
||||
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.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;
|
||||
GameObject saveManagerGo = GetOrCreateChild(services, "GameSaveManager").gameObject;
|
||||
|
||||
GameServiceRegistrar registrar = GetOrAddComponent<GameServiceRegistrar>(registrarGo);
|
||||
DeathRespawnService deathRespawnService = GetOrAddComponent<DeathRespawnService>(deathRespawnGo);
|
||||
SceneService sceneService = GetOrAddComponent<SceneService>(sceneServiceGo);
|
||||
SceneLoader sceneLoader = GetOrAddComponent<SceneLoader>(sceneLoaderGo);
|
||||
EventChannelRegistry registry = GetOrAddComponent<EventChannelRegistry>(registryGo);
|
||||
SettingsManager settingsManager = GetOrAddComponent<SettingsManager>(settingsGo);
|
||||
GetOrAddComponent<GlobalObjectPool>(poolGo);
|
||||
GameManager gameManager = GetOrAddComponent<GameManager>(gameManagerGo);
|
||||
AudioManager audioManager = GetOrAddComponent<AudioManager>(audioManagerGo);
|
||||
GameSaveManager gameSaveManager = GetOrAddComponent<GameSaveManager>(saveManagerGo);
|
||||
|
||||
GameObject inputHolderGo = GetOrCreateChild(input, "InputReaderHolder").gameObject;
|
||||
Object inputReaderAsset = FindFirstAssetByType<InputReaderSO>("InputReader", "InputReaderSO");
|
||||
if (inputReaderAsset == null)
|
||||
inputReaderAsset = EnsureInputReaderAsset(report);
|
||||
|
||||
InputReaderBootstrap inputBootstrap = GetOrAddComponent<InputReaderBootstrap>(inputHolderGo);
|
||||
AssignReference(inputBootstrap, "_inputReader", inputReaderAsset, report);
|
||||
if (inputReaderAsset != null)
|
||||
{
|
||||
AssignReference(inputReaderAsset, "_onPauseRequested", FindFirstAssetByType<VoidEventChannelSO>("EVT_PauseRequested"), report);
|
||||
AssignReference(inputReaderAsset, "_inputActions", FindFirstAssetWithExtension(".inputactions", "PlayerInputActions", "InputActions"), report);
|
||||
}
|
||||
if (inputReaderAsset == null)
|
||||
report.Add("未找到 InputReaderSO 资产,InputReaderBootstrap 将保持空引用。请补齐 Assets/_Game/Data/Player/Input/InputReader.asset。");
|
||||
|
||||
GameObject mainCameraGo = GetOrCreateChild(camera, "Main Camera").gameObject;
|
||||
UnityEngine.Camera mainCamera = GetOrAddComponent<UnityEngine.Camera>(mainCameraGo);
|
||||
mainCamera.orthographic = false;
|
||||
mainCamera.fieldOfView = 60f;
|
||||
mainCameraGo.tag = "MainCamera";
|
||||
GetOrAddComponent<AudioListener>(mainCameraGo);
|
||||
CinemachineBrain brain = GetOrAddComponent<CinemachineBrain>(mainCameraGo);
|
||||
|
||||
GameObject cameraStateGo = GetOrCreateChild(camera, "CameraStateController").gameObject;
|
||||
CameraStateController cameraStateController = GetOrAddComponent<CameraStateController>(cameraStateGo);
|
||||
CinemachineImpulseSource impulseSource = GetOrAddComponent<CinemachineImpulseSource>(cameraStateGo);
|
||||
|
||||
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(registrar, "_saveManager", gameSaveManager);
|
||||
|
||||
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, "_onFadeInRequest", report, false, "EVT_FadeInRequest");
|
||||
AssignAsset(sceneService, "_onFadeOutRequest", report, false, "EVT_FadeOutRequest");
|
||||
AssignReference(sceneService, "_sceneLoader", sceneLoader);
|
||||
|
||||
AssignAsset(sceneLoader, "_onSceneLoaded", report, false, "EVT_SceneLoaded");
|
||||
|
||||
AssignAsset(deathRespawnService, "_onRespawnStarted", report, false, "EVT_RespawnStarted");
|
||||
AssignAsset(deathRespawnService, "_onRespawnCompleted", report, false, "EVT_RespawnCompleted");
|
||||
AssignAsset(deathRespawnService, "_onDeathScreenConfirmed", report, true, "EVT_DeathScreenConfirmed");
|
||||
|
||||
AssignAsset(settingsManager, "_defaultSettings", report, false, "SET_GlobalSettings");
|
||||
|
||||
AssignAsset(audioManager, "_onPlayerDied", report, false, "EVT_PlayerDied");
|
||||
|
||||
AssignReference(cameraStateController, "_brain", brain);
|
||||
AssignReference(cameraStateController, "_impulseSource", impulseSource);
|
||||
|
||||
AssignReference(uiManager, "_hudRoot", hudRootGo);
|
||||
AssignReference(uiManager, "_pauseMenuRoot", pauseRootGo);
|
||||
AssignReference(uiManager, "_deathScreenRoot", deathRootGo);
|
||||
AssignReference(uiManager, "_settingsRoot", settingsRootGo);
|
||||
AssignReference(uiManager, "_mapRoot", mapRootGo);
|
||||
AssignReference(uiManager, "_shopRoot", shopRootGo);
|
||||
AssignAsset(uiManager, "_onGameStateChanged", report, true, "EVT_GameStateChanged", "EVT_GameState");
|
||||
AssignAsset(uiManager, "_onPauseRequested", report, false, "EVT_PauseRequested");
|
||||
AssignAsset(uiManager, "_onFastTravelOpen", report, false, "EVT_FastTravelOpen");
|
||||
AssignAsset(uiManager, "_onShopOpen", report, false, "EVT_ShopOpen");
|
||||
AssignAsset(uiManager, "_onMapOpen", report, false, "EVT_MapOpen");
|
||||
|
||||
AssignReference(deathScreenController, "_btnRespawn", respawnButton);
|
||||
AssignAsset(deathScreenController, "_onPlayerDied", report, true, "EVT_PlayerDied");
|
||||
AssignAsset(deathScreenController, "_onDeathScreenConfirmed", report, true, "EVT_DeathScreenConfirmed");
|
||||
|
||||
AddScaffoldNote(hudRootGo, "HUDController 已挂载。其内部图片/文本/图标 Prefab 依赖较多,需后续手工补 UI 资源与事件频道。", report);
|
||||
|
||||
MarkDirtyAndLog("Persistent 场景脚手架", root, report);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// Scaffold Game Room
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 在当前活动场景中生成标准游戏关卡房间的完整层级结构:
|
||||
/// [RoomRoot] → [Camera] / [SpawnPoints] / [Environment] / [Transitions]
|
||||
/// 可配合 SceneObjectPlacerTool 在层级内快速追加更多对象。
|
||||
/// </summary>
|
||||
[MenuItem("BaseGames/Tools/Scaffold Game Room", priority = 201)]
|
||||
public static void ScaffoldGameRoom()
|
||||
{
|
||||
var report = new List<string>();
|
||||
|
||||
// ── [RoomRoot] ─────────────────────────────────────────────────
|
||||
GameObject root = GetOrCreateRoot("[RoomRoot]");
|
||||
RoomController roomController = GetOrAddComponent<RoomController>(root);
|
||||
|
||||
// ── [Camera] ───────────────────────────────────────────────────
|
||||
Transform cameraGroup = GetOrCreateChild(root.transform, "[Camera]");
|
||||
|
||||
GameObject roomCameraGo = GetOrCreateChild(cameraGroup, "RoomCamera").gameObject;
|
||||
CinemachineCamera cinemachineCamera = GetOrAddComponent<CinemachineCamera>(roomCameraGo);
|
||||
RoomCamera roomCamera = GetOrAddComponent<RoomCamera>(roomCameraGo);
|
||||
CinemachineConfiner2D confiner = GetOrAddComponent<CinemachineConfiner2D>(roomCameraGo);
|
||||
|
||||
// RoomBoundary — defines visible area and confiner polygon
|
||||
Transform boundaryT = GetOrCreateChild(roomCameraGo.transform, "RoomBoundary");
|
||||
PolygonCollider2D boundaryCollider = GetOrAddComponent<PolygonCollider2D>(boundaryT.gameObject);
|
||||
boundaryCollider.pathCount = 1;
|
||||
boundaryCollider.SetPath(0, new Vector2[]
|
||||
{
|
||||
new Vector2(-12f, -6f), new Vector2(-12f, 6f),
|
||||
new Vector2( 12f, 6f), new Vector2( 12f, -6f),
|
||||
});
|
||||
RoomVisibleArea visibleArea = GetOrAddComponent<RoomVisibleArea>(boundaryT.gameObject);
|
||||
|
||||
AssignReference(roomCamera, "_visibleArea", visibleArea);
|
||||
AssignReference(confiner, "m_BoundingShape2D", boundaryCollider);
|
||||
|
||||
// Disable stray Camera / AudioListener components sometimes added by Cinemachine
|
||||
UnityEngine.Camera staleCam = roomCameraGo.GetComponent<UnityEngine.Camera>();
|
||||
if (staleCam != null) staleCam.enabled = false;
|
||||
AudioListener staleAl = roomCameraGo.GetComponent<AudioListener>();
|
||||
if (staleAl != null) { Undo.DestroyObjectImmediate(staleAl); }
|
||||
|
||||
// ── [SpawnPoints] ──────────────────────────────────────────────
|
||||
Transform spawnGroup = GetOrCreateChild(root.transform, "[SpawnPoints]");
|
||||
|
||||
GameObject defaultSpawnGo = GetOrCreateChild(spawnGroup, "SpawnPoint_Default").gameObject;
|
||||
PlayerSpawnPoint defaultSpawn = GetOrAddComponent<PlayerSpawnPoint>(defaultSpawnGo);
|
||||
AssignString(defaultSpawn, "_transitionId", "default");
|
||||
AssignInt(defaultSpawn, "_facingDirection", 1);
|
||||
|
||||
// ── [Environment] ──────────────────────────────────────────────
|
||||
Transform envGroup = GetOrCreateChild(root.transform, "[Environment]");
|
||||
|
||||
// Ground Tilemap
|
||||
GameObject gridGo = GetOrCreateChild(envGroup, "GroundGrid").gameObject;
|
||||
GetOrAddComponent<Grid>(gridGo);
|
||||
|
||||
GameObject groundTileGo = GetOrCreateChild(gridGo.transform, "Ground").gameObject;
|
||||
int groundLayer = LayerMask.NameToLayer("Ground");
|
||||
if (groundLayer >= 0) groundTileGo.layer = groundLayer;
|
||||
else report.Add("Layer 'Ground' 不存在,请在 Tags and Layers 中创建。");
|
||||
|
||||
GetOrAddComponent<Tilemap>(groundTileGo);
|
||||
GetOrAddComponent<TilemapRenderer>(groundTileGo);
|
||||
TilemapCollider2D tilemapCol = GetOrAddComponent<TilemapCollider2D>(groundTileGo);
|
||||
tilemapCol.usedByComposite = true;
|
||||
Rigidbody2D groundRb = GetOrAddComponent<Rigidbody2D>(groundTileGo);
|
||||
groundRb.bodyType = RigidbodyType2D.Static;
|
||||
GetOrAddComponent<CompositeCollider2D>(groundTileGo);
|
||||
|
||||
// NavSurface for PathBerserker2d
|
||||
GameObject navGo = GetOrCreateChild(envGroup, "NavSurface").gameObject;
|
||||
GetOrAddComponent<NavSurface>(navGo);
|
||||
|
||||
// ── [Transitions] ──────────────────────────────────────────────
|
||||
GetOrCreateChild(root.transform, "[Transitions]");
|
||||
|
||||
// ── Wire RoomController ────────────────────────────────────────
|
||||
AssignReference(roomController, "_roomCamera", roomCamera);
|
||||
|
||||
SerializedObject roomSO = new SerializedObject(roomController);
|
||||
SerializedProperty spawnArrayProp = roomSO.FindProperty("_spawnPoints");
|
||||
if (spawnArrayProp != null && spawnArrayProp.isArray)
|
||||
{
|
||||
spawnArrayProp.arraySize = 1;
|
||||
spawnArrayProp.GetArrayElementAtIndex(0).objectReferenceValue = defaultSpawn;
|
||||
roomSO.ApplyModifiedPropertiesWithoutUndo();
|
||||
}
|
||||
|
||||
// ── Report ─────────────────────────────────────────────────────
|
||||
report.Add("在 RoomController._roomId 填写唯一房间 ID(如 \"Room_Forest_01\")。");
|
||||
report.Add("将 Player/CameraFollowTarget Transform 拖入 CinemachineCamera.Follow 字段以跟随玩家(或使用 BaseGames → Camera → Room Camera Setup 工具批量赋值)。");
|
||||
report.Add("调整 RoomBoundary PolygonCollider2D 顶点以匹配实际房间大小。");
|
||||
report.Add("使用 Tile Palette 在 Ground Tilemap 上绘制地形,然后在 NavSurface Inspector 中点击 Bake。");
|
||||
report.Add("[Transitions] 子节点下使用 BaseGames/Scene/Place/Room Transition 添加过渡点。");
|
||||
|
||||
MarkDirtyAndLog("Game Room 脚手架", root, report);
|
||||
}
|
||||
|
||||
private static void AssignString(Object target, string propertyName, string value, List<string> report = null)
|
||||
{
|
||||
SerializedObject serializedObject = new SerializedObject(target);
|
||||
SerializedProperty property = serializedObject.FindProperty(propertyName);
|
||||
if (property == null)
|
||||
{
|
||||
report?.Add($"{target.GetType().Name}.{propertyName} 字段不存在,未写入字符串值。");
|
||||
return;
|
||||
}
|
||||
property.stringValue = value;
|
||||
serializedObject.ApplyModifiedPropertiesWithoutUndo();
|
||||
}
|
||||
|
||||
private static void AssignInt(Object target, string propertyName, int value, List<string> report = null)
|
||||
{
|
||||
SerializedObject serializedObject = new SerializedObject(target);
|
||||
SerializedProperty property = serializedObject.FindProperty(propertyName);
|
||||
if (property == null)
|
||||
{
|
||||
report?.Add($"{target.GetType().Name}.{propertyName} 字段不存在,未写入整型值。");
|
||||
return;
|
||||
}
|
||||
property.intValue = value;
|
||||
serializedObject.ApplyModifiedPropertiesWithoutUndo();
|
||||
}
|
||||
|
||||
private static void EnsureEventChannelAssets(List<string> report)
|
||||
{
|
||||
bool hasCoreSet =
|
||||
FindFirstAsset("EVT_PlayerDied") != null &&
|
||||
FindFirstAsset("EVT_DeathScreenConfirmed") != null &&
|
||||
(FindFirstAsset("EVT_GameStateChanged") != null || FindFirstAsset("EVT_GameState") != null) &&
|
||||
FindFirstAsset("EVT_PauseRequested") != null &&
|
||||
FindFirstAsset("EVT_SceneLoadRequest") != null;
|
||||
|
||||
if (hasCoreSet)
|
||||
return;
|
||||
|
||||
CreateEventChannelAssets.CreateAll();
|
||||
report?.Add("检测到关键事件频道缺失,已自动执行 Create Event Channel Assets。");
|
||||
}
|
||||
|
||||
|
||||
|
||||
private static void EnsureAudioSources(GameObject audioManagerGo, AudioManager audioManager, List<string> report)
|
||||
{
|
||||
GameObject bgmAGo = GetOrCreateChild(audioManagerGo.transform, "BGM Source A").gameObject;
|
||||
GameObject bgmBGo = GetOrCreateChild(audioManagerGo.transform, "BGM Source B").gameObject;
|
||||
GameObject sfxRootGo = GetOrCreateChild(audioManagerGo.transform, "SFX Sources").gameObject;
|
||||
|
||||
AudioSource bgmA = GetOrAddComponent<AudioSource>(bgmAGo);
|
||||
AudioSource bgmB = GetOrAddComponent<AudioSource>(bgmBGo);
|
||||
bgmA.playOnAwake = false;
|
||||
bgmB.playOnAwake = false;
|
||||
bgmA.loop = true;
|
||||
bgmB.loop = true;
|
||||
|
||||
var sfxSources = new AudioSource[6];
|
||||
for (int i = 0; i < sfxSources.Length; i++)
|
||||
{
|
||||
GameObject sfxGo = GetOrCreateChild(sfxRootGo.transform, $"SFX Source {i + 1}").gameObject;
|
||||
AudioSource sfxSource = GetOrAddComponent<AudioSource>(sfxGo);
|
||||
sfxSource.playOnAwake = false;
|
||||
sfxSources[i] = sfxSource;
|
||||
}
|
||||
|
||||
AssignReference(audioManager, "_bgmSourceA", bgmA);
|
||||
AssignReference(audioManager, "_bgmSourceB", bgmB);
|
||||
AssignArrayReferences(audioManager, "_sfxSources", sfxSources, report);
|
||||
report.Add("AudioManager 已生成 2 个 BGM Source 和 6 个 SFX Source,AudioMixer 仍需手工指定。");
|
||||
}
|
||||
|
||||
private static GameObject GetOrCreateRoot(string name)
|
||||
{
|
||||
Scene scene = SceneManager.GetActiveScene();
|
||||
foreach (GameObject rootObject in scene.GetRootGameObjects())
|
||||
{
|
||||
if (rootObject.name == name)
|
||||
return rootObject;
|
||||
}
|
||||
|
||||
GameObject root = new GameObject(name);
|
||||
Undo.RegisterCreatedObjectUndo(root, $"Create {name}");
|
||||
return root;
|
||||
}
|
||||
|
||||
private static Transform GetOrCreateChild(Transform parent, string name)
|
||||
{
|
||||
Transform child = parent.Find(name);
|
||||
if (child != null)
|
||||
return child;
|
||||
|
||||
GameObject go = new GameObject(name);
|
||||
Undo.RegisterCreatedObjectUndo(go, $"Create {name}");
|
||||
go.transform.SetParent(parent, false);
|
||||
return go.transform;
|
||||
}
|
||||
|
||||
private static T GetOrAddComponent<T>(GameObject go) where T : Component
|
||||
{
|
||||
T component = go.GetComponent<T>();
|
||||
if (component != null)
|
||||
return component;
|
||||
|
||||
return Undo.AddComponent<T>(go);
|
||||
}
|
||||
|
||||
private static GameObject GetOrCreateCanvas(Transform parent, string name, int sortOrder)
|
||||
{
|
||||
GameObject canvasGo = GetOrCreateChild(parent, name).gameObject;
|
||||
Canvas canvas = GetOrAddComponent<Canvas>(canvasGo);
|
||||
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
||||
canvas.sortingOrder = sortOrder;
|
||||
GetOrAddComponent<CanvasScaler>(canvasGo);
|
||||
GetOrAddComponent<GraphicRaycaster>(canvasGo);
|
||||
return canvasGo;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user