159 lines
10 KiB
C#
159 lines
10 KiB
C#
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
||
|
|
// 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();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|