// ───────────────────────────────────────────────────────────────────────────── // 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()); } /// 테스트 씬을 연다(Single). 현재 씬이 저장 안 된 상태면 열지 않고 알린다(K-WL-114). 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; } /// 테스트 씬이 열려 있으면 **저장 없이** 빈 씬으로 바꾼다(다시 만들기 전 · 저장 안 된 변경이 있으면 거절). 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.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(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().sharedMaterial = mat; GameObjectUtility.SetStaticEditorFlags(ground, StaticEditorFlags.BatchingStatic); // NavMesh 는 부트가 콜라이더 기준 런타임 베이크 // 매니저(Title 의 Mgr 과 같은 구성) var mgr = new GameObject("Mgr"); mgr.AddComponent(); mgr.AddComponent(); mgr.AddComponent(); var es = new GameObject("EventSystem"); es.AddComponent(); es.AddComponent(); // Title 이 Addressables 로 올리던 프리팹 → 씬에 직접 foreach (var n in TitlePrefabs) { var p = AssetDatabase.LoadAssetAtPath(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(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(); new GameObject("DungeonInfo").AddComponent(); new GameObject("DropItemInfo").AddComponent(); var gv = new GameObject("Global Volume"); gv.AddComponent(); var vol = gv.AddComponent(); vol.isGlobal = true; vol.enabled = false; // InGame.unity 실측: Volume 컴포넌트 꺼짐 var profPath = AssetDatabase.GUIDToAssetPath(VolumeProfileGuid); if (!string.IsNullOrEmpty(profPath)) vol.sharedProfile = AssetDatabase.LoadAssetAtPath(profPath); // ProjectileInfo 는 씬에 두지 않는다 — 부트가 InGameInfo 뒤에 원본 프리팹으로 만든다(1차 Play: 예열 투사체가 InGameInfo.Ins null 로 NRE 127건) // 부트 var bootGo = new GameObject("WLSkillTestBoot"); var boot = bootGo.AddComponent(); boot.inGameInfoPrefab = AssetDatabase.LoadAssetAtPath(InGameInfoPrefab); boot.pcInfoPrefab = AssetDatabase.LoadAssetAtPath(PCInfoPrefab); boot.projectileInfoPrefab = AssetDatabase.LoadAssetAtPath(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(); } } }