Project_WL/Assets/WL/SkillTest/Editor/WLSkillTestSceneBuilder.cs

159 lines
10 KiB
C#
Raw Normal View History

// ─────────────────────────────────────────────────────────────────────────────
// WLSkillTestSceneBuilder.cs — 테스트 씬(Assets/WL/SkillTest/WLSkillTest.unity)을 코드로 조립·저장한다 (PD #947 · 2026-10-19)
//
// ■ 왜 코드로: 씬에 무엇이 들어가는지가 글로 남고, 깨지면 메뉴 한 번으로 다시 만든다(에디터 손작업 0 · 열린 씬(Title)은 건드리지 않는다).
// ■ 들어가는 것 (= Title 이 Addressables 로 올리던 매니저 + InGame 씬 루트 + 원본 카메라 프리팹 + 평지 + 부트)
// Directional Light · Ground(Plane · URP Lit 재질 자산 · 부트가 런타임 NavMesh 베이크) · Mgr(AddrResourceMgr·CoroutineMgr·TimeMgr) · EventSystem
// · ResWork/UIPrefabs/Title/{Tables, SoundInfo, UIAtlasMgr, SortOrder_5, ActorInfo}.prefab · ResWork/Prefabs/Ingame/Main Camera.prefab(RealCamera)
// · InGame 씬 루트 4종(IngameTouchInfo · DungeonInfo · DropItemInfo · Global Volume(GlobalVolumeMgr + 꺼진 Volume · 같은 프로필)) · ProjectileInfo
// · WLSkillTestBoot(로그인 뒤 InGameInfo·PCInfo 프리팹 생성)
// ■ 메뉴 WL/Skill Test/Build Scene · CLI: unity command eval --code 'return WL.SkillTest.Editor.WLSkillTestSceneBuilder.Build();'
// ─────────────────────────────────────────────────────────────────────────────
using System.Text;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.Rendering;
using UnityEngine.SceneManagement;
namespace WL.SkillTest.Editor
{
public static class WLSkillTestSceneBuilder
{
public const string Folder = "Assets/WL/SkillTest";
public const string ScenePath = Folder + "/WLSkillTest.unity";
public const string GroundMatPath = Folder + "/WLSkillTestGround.mat";
static readonly string[] TitlePrefabs = { "Tables", "SoundInfo", "UIAtlasMgr", "SortOrder_5", "ActorInfo" };
const string TitlePrefabFolder = "Assets/ResWork/UIPrefabs/Title/";
const string MainCameraPrefab = "Assets/ResWork/Prefabs/Ingame/Main Camera.prefab";
const string InGameInfoPrefab = "Assets/ResWork/Prefabs/Ingame/InGameInfo.prefab";
const string PCInfoPrefab = "Assets/ResWork/Prefabs/Ingame/PCInfo.prefab";
const string ProjectileInfoPrefab = "Assets/ResWork/Prefabs/Ingame/ProjectileInfo.prefab";
const string VolumeProfileGuid = "96e4f5994a1f2d948a293a2d5f2bd1b3"; // InGame.unity 의 Global Volume 프로필(실측)
[MenuItem("WL/Skill Test/Build Scene (다시 만들기)")]
public static void BuildMenu() { Debug.Log(Build()); }
[MenuItem("WL/Skill Test/Open Scene")]
public static void OpenMenu() { Debug.Log(Open()); }
/// <summary>테스트 씬을 연다(Single). 현재 씬이 저장 안 된 상태면 열지 않고 알린다(K-WL-114).</summary>
public static string Open()
{
var cur = SceneManager.GetActiveScene();
if (cur.isDirty) return "현재 씬 " + cur.name + " 이 저장되지 않음 — 저장 없이 다시 열거나 저장한 뒤 다시";
if (!System.IO.File.Exists(ScenePath)) return "씬 없음 " + ScenePath + " — Build() 먼저";
EditorSceneManager.OpenScene(ScenePath, OpenSceneMode.Single);
return "열림 " + ScenePath;
}
static bool IsTestSceneOpen()
{
for (int i = 0; i < SceneManager.sceneCount; i++) if (SceneManager.GetSceneAt(i).path == ScenePath) return true;
return false;
}
/// <summary>테스트 씬이 열려 있으면 **저장 없이** 빈 씬으로 바꾼다(다시 만들기 전 · 저장 안 된 변경이 있으면 거절).</summary>
public static string CloseTestScene()
{
if (Application.isPlaying) return "Play 중";
if (!IsTestSceneOpen()) return "테스트 씬 안 열림";
var a = SceneManager.GetActiveScene();
if (a.path == ScenePath && a.isDirty) return "테스트 씬이 저장되지 않음 — 저장 없이 다시 열거나 저장한 뒤";
EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
return "빈 씬으로 바꿈";
}
public static string Build()
{
if (Application.isPlaying) return "Play 중에는 만들지 않는다";
var sb = new StringBuilder();
var prevActive = SceneManager.GetActiveScene();
if (!AssetDatabase.IsValidFolder(Folder)) { AssetDatabase.CreateFolder("Assets/WL", "SkillTest"); }
var cfg = WLSkillTestBoot.Cfg;
// 테스트 씬이 열려 있으면 그 경로로 저장할 수 없다(Unity: "Overwriting the same path as another open scene is not allowed")
// → 같은 호출 안에서 씬을 바꾸면 파이프라인 실행이 끊긴다(실측) · 먼저 CloseTestScene() 을 따로 부른 뒤 Build()
if (IsTestSceneOpen()) return "테스트 씬이 열려 있음 — 먼저 CloseTestScene()(빈 씬으로) 을 부른 뒤 Build()";
var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Additive);
SceneManager.SetActiveScene(scene);
// 빛
var lightGo = new GameObject("Directional Light");
var light = lightGo.AddComponent<Light>();
light.type = LightType.Directional; light.intensity = 1.1f; light.shadows = LightShadows.Soft; light.color = Color.white;
lightGo.transform.rotation = Quaternion.Euler(50f, -30f, 0f);
// 평지(콜라이더 = NavMesh 베이크 기준)
var ground = GameObject.CreatePrimitive(PrimitiveType.Plane);
ground.name = "Ground";
float size = cfg != null && cfg.groundSize > 1f ? cfg.groundSize : 60f;
ground.transform.localScale = new Vector3(size / 10f, 1f, size / 10f);
var mat = AssetDatabase.LoadAssetAtPath<Material>(GroundMatPath);
if (mat == null)
{
var sh = Shader.Find("Universal Render Pipeline/Lit");
mat = new Material(sh != null ? sh : Shader.Find("Standard"));
AssetDatabase.CreateAsset(mat, GroundMatPath);
}
var gc = cfg != null && cfg.groundColor != null && cfg.groundColor.Length >= 3 ? new Color(cfg.groundColor[0], cfg.groundColor[1], cfg.groundColor[2]) : new Color(0.45f, 0.62f, 0.35f);
if (mat.HasProperty("_BaseColor")) mat.SetColor("_BaseColor", gc); else mat.color = gc;
EditorUtility.SetDirty(mat);
ground.GetComponent<Renderer>().sharedMaterial = mat;
GameObjectUtility.SetStaticEditorFlags(ground, StaticEditorFlags.BatchingStatic); // NavMesh 는 부트가 콜라이더 기준 런타임 베이크
// 매니저(Title 의 Mgr 과 같은 구성)
var mgr = new GameObject("Mgr");
mgr.AddComponent<AddrResourceMgr>(); mgr.AddComponent<CoroutineMgr>(); mgr.AddComponent<TimeMgr>();
var es = new GameObject("EventSystem");
es.AddComponent<EventSystem>(); es.AddComponent<StandaloneInputModule>();
// Title 이 Addressables 로 올리던 프리팹 → 씬에 직접
foreach (var n in TitlePrefabs)
{
var p = AssetDatabase.LoadAssetAtPath<GameObject>(TitlePrefabFolder + n + ".prefab");
if (p == null) { sb.Append("🔴 프리팹 없음 ").Append(n).Append('\n'); continue; }
var go = PrefabUtility.InstantiatePrefab(p, scene) as GameObject;
if (go != null) go.name = n;
}
// 원본 인게임 카메라(RealCamera · 태그 MainCamera)
var camP = AssetDatabase.LoadAssetAtPath<GameObject>(MainCameraPrefab);
if (camP != null) { var cam = PrefabUtility.InstantiatePrefab(camP, scene) as GameObject; if (cam != null) cam.name = "Main Camera"; }
else sb.Append("🔴 Main Camera 프리팹 없음\n");
// InGame 씬 루트 4종
new GameObject("IngameTouchInfo").AddComponent<InGameTouchInfo>();
new GameObject("DungeonInfo").AddComponent<DungeonInfo>();
new GameObject("DropItemInfo").AddComponent<DropItemInfo>();
var gv = new GameObject("Global Volume");
gv.AddComponent<GlobalVolumeMgr>();
var vol = gv.AddComponent<Volume>();
vol.isGlobal = true; vol.enabled = false; // InGame.unity 실측: Volume 컴포넌트 꺼짐
var profPath = AssetDatabase.GUIDToAssetPath(VolumeProfileGuid);
if (!string.IsNullOrEmpty(profPath)) vol.sharedProfile = AssetDatabase.LoadAssetAtPath<VolumeProfile>(profPath);
// ProjectileInfo 는 씬에 두지 않는다 — 부트가 InGameInfo 뒤에 원본 프리팹으로 만든다(1차 Play: 예열 투사체가 InGameInfo.Ins null 로 NRE 127건)
// 부트
var bootGo = new GameObject("WLSkillTestBoot");
var boot = bootGo.AddComponent<WLSkillTestBoot>();
boot.inGameInfoPrefab = AssetDatabase.LoadAssetAtPath<GameObject>(InGameInfoPrefab);
boot.pcInfoPrefab = AssetDatabase.LoadAssetAtPath<GameObject>(PCInfoPrefab);
boot.projectileInfoPrefab = AssetDatabase.LoadAssetAtPath<GameObject>(ProjectileInfoPrefab);
if (boot.inGameInfoPrefab == null) sb.Append("🔴 InGameInfo 프리팹 없음\n");
if (boot.pcInfoPrefab == null) sb.Append("🔴 PCInfo 프리팹 없음\n");
if (boot.projectileInfoPrefab == null) sb.Append("🔴 ProjectileInfo 프리팹 없음\n");
bool saved = EditorSceneManager.SaveScene(scene, ScenePath);
sb.Append(saved ? "저장 " : "🔴 저장 실패 ").Append(ScenePath).Append(" · 루트 ").Append(scene.rootCount).Append('\n');
// 열려 있던 씬으로 되돌리고 새 씬은 닫는다(Title 무변경)
if (prevActive.IsValid() && prevActive.isLoaded) SceneManager.SetActiveScene(prevActive);
EditorSceneManager.CloseScene(scene, true);
AssetDatabase.SaveAssets();
return sb.ToString();
}
}
}