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

228 lines
16 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ─────────────────────────────────────────────────────────────────────────────
// WLSkillTestSceneBuilder.cs — 독립 스킬 테스트 씬(Assets/WL/SkillTest/WLSkillTest.unity)을 코드로 조립·저장 (PD #947 · 2026-10-20 재지시판)
//
// ■ 들어가는 것(= 원본 StatTest 하네스 구성 + WL 치비·더미·평지) — 로그인·HUD·섬 없음
// Directional Light · Ground(Plane · URP Lit 재질 자산 · 하네스가 런타임 NavMesh 베이크) · Mgr(AddrResourceMgr·CoroutineMgr·TimeMgr) · EventSystem · DungeonInfo(InGame.unity 씬 객체 · 웨이브 스탯 표)
// · ResWork/UIPrefabs/Title/{Tables, SoundInfo, SortOrder_5(OptionInfo·Popup), ActorInfo}.prefab
// · ResWork/Prefabs/Ingame/{InGameInfo(이펙트·투사체 풀), ProjectileInfo, Main Camera(RealCamera)}.prefab
// · PCPos/치비 PC 프리팹(WLCharacterSwapSettings.pcPrefabName · 없으면 Ai01) · MobPos/더미 몹 프리팹(Res_Addr/Mobs/WL/RPG_Bat)
// · Canvas(스케일 = 원본 NewGameUI 와 동일) + 원본 가상패드(Floating Joystick 중첩 프리팹 · RectTransform 값 = NewGameUI 의 것) + 글자 4(원본 Set_Dmglabel 이 쓰는 피해 로그)
// · Ground 의 NavMeshSurface(사전 베이크 · 데이터 에셋 WLSkillTestNavMesh.asset) · WLSkillTestHarness(actors · labels · pcActor · dummyActor)
// ■ 메뉴 WL/Skill Test/Build Scene · CLI: 먼저 CloseTestScene() 또는 Title 을 연 뒤 Build()
// ─────────────────────────────────────────────────────────────────────────────
using System.Text;
using TMPro;
using Unity.AI.Navigation;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.AI;
using UnityEngine.EventSystems;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
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", "SortOrder_5", "ActorInfo" };
const string TitlePrefabFolder = "Assets/ResWork/UIPrefabs/Title/";
static readonly string[] IngamePrefabs = { "InGameInfo", "ProjectileInfo", "Main Camera" };
const string IngamePrefabFolder = "Assets/ResWork/Prefabs/Ingame/";
const string PcPrefabFolder = "Assets/Res_Addr/PC/";
const string DummyPrefab = "Assets/Res_Addr/Mobs/WL/RPG_Bat.prefab";
const string NewGameUiPrefab = "Assets/Res_Addr/MainUI/NewGameUI.prefab"; // 원본 HUD — 가상패드(Floating Joystick)·캔버스 스케일을 여기서 읽어 복제한다
public const string NavMeshPath = Folder + "/WLSkillTestNavMesh.asset";
[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()); }
public static string Open()
{
var cur = SceneManager.GetActiveScene();
if (cur.path == ScenePath) return "열림 " + ScenePath;
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 "빈 씬으로 바꿈";
}
static string PcPrefabPath()
{
var s = WL.Character.WLCharacterSwapSettings.Instance;
string name = s != null && !string.IsNullOrEmpty(s.pcPrefabName) ? s.pcPrefabName : "Ai01";
string p = PcPrefabFolder + name + ".prefab";
if (AssetDatabase.LoadAssetAtPath<GameObject>(p) == null) p = PcPrefabFolder + "Ai01.prefab";
return p;
}
/// <summary>원본 HUD(NewGameUI)의 가상패드를 그대로 — 캔버스 스케일(원본 1920×1080 Expand)과 Floating Joystick 중첩 프리팹 · RectTransform 값을 원본 프리팹에서 읽어 복제한다
/// (PD 2026-10-20 #947 「원본과 동일한 가상패드 조작 + 키 입력」 · 값을 코드에 적지 않는다 C45).</summary>
static string AddOriginalJoystick(GameObject canvasGo, Scene scene)
{
var hud = AssetDatabase.LoadAssetAtPath<GameObject>(NewGameUiPrefab);
if (hud == null) return "🔴 NewGameUI 프리팹 없음 " + NewGameUiPrefab + "\n";
var srcScaler = hud.GetComponentInChildren<CanvasScaler>(true);
var dstScaler = canvasGo.GetComponent<CanvasScaler>();
if (srcScaler != null && dstScaler != null)
{
dstScaler.uiScaleMode = srcScaler.uiScaleMode; dstScaler.referenceResolution = srcScaler.referenceResolution;
dstScaler.screenMatchMode = srcScaler.screenMatchMode; dstScaler.matchWidthOrHeight = srcScaler.matchWidthOrHeight;
dstScaler.referencePixelsPerUnit = srcScaler.referencePixelsPerUnit;
}
var srcJoy = hud.GetComponentInChildren<FloatingJoystick>(true);
if (srcJoy == null) return "🔴 NewGameUI 안에 FloatingJoystick 없음\n";
var srcPrefab = PrefabUtility.GetCorrespondingObjectFromOriginalSource(srcJoy.gameObject); // 중첩 프리팹의 원본(Joystick Pack · Floating Joystick.prefab)
var joy = srcPrefab != null ? PrefabUtility.InstantiatePrefab(srcPrefab, scene) as GameObject : Object.Instantiate(srcJoy.gameObject);
joy.name = srcJoy.gameObject.name;
joy.transform.SetParent(canvasGo.transform, false);
joy.transform.SetAsFirstSibling();
var s = srcJoy.GetComponent<RectTransform>(); var d = joy.GetComponent<RectTransform>();
d.anchorMin = s.anchorMin; d.anchorMax = s.anchorMax; d.pivot = s.pivot; d.sizeDelta = s.sizeDelta; d.anchoredPosition = s.anchoredPosition; d.localScale = s.localScale;
return "";
}
static TextMeshProUGUI MakeLabel(Transform canvas, string name, Vector2 anchor, Vector2 pos, Vector2 size, TextAlignmentOptions align)
{
var go = new GameObject(name, typeof(RectTransform));
go.transform.SetParent(canvas, false);
var rt = go.GetComponent<RectTransform>();
rt.anchorMin = rt.anchorMax = rt.pivot = anchor; rt.anchoredPosition = pos; rt.sizeDelta = size;
var t = go.AddComponent<TextMeshProUGUI>();
t.fontSize = 16; t.alignment = align; t.color = Color.white; t.text = "";
return t;
}
public static string Build()
{
if (Application.isPlaying) return "Play 중에는 만들지 않는다";
if (IsTestSceneOpen()) return "테스트 씬이 열려 있음 — 먼저 CloseTestScene() 또는 Title 을 연 뒤 Build()";
var sb = new StringBuilder();
var prevActive = SceneManager.GetActiveScene();
if (!AssetDatabase.IsValidFolder(Folder)) AssetDatabase.CreateFolder("Assets/WL", "SkillTest");
var cfg = WLSkillTestHarness.Cfg;
// 현재 씬이 저장 안 된 Untitled(CloseTestScene() 직후)면 Additive 생성이 거절된다(InvalidOperationException · #947 실측) → 그 자리에 Single 로 만든다
bool untitled = string.IsNullOrEmpty(prevActive.path);
if (untitled && prevActive.isDirty) return "저장 안 된 Untitled 씬이 열려 있음 — Title 을 연 뒤 Build()";
var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, untitled ? NewSceneMode.Single : 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;
lightGo.transform.rotation = Quaternion.Euler(50f, -30f, 0f);
var ground = GameObject.CreatePrimitive(PrimitiveType.Plane);
ground.name = "Ground";
float size = cfg != null && cfg.groundSize > 1f ? cfg.groundSize : 100f;
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 사전 베이크 — 런타임 베이크(#947 실측 5.3 s)를 없앤다. 데이터는 씬 옆 에셋 · 하네스는 데이터가 있으면 베이크를 건너뛴다(NavMeshSurface.OnEnable 이 등록)
var surf = ground.AddComponent<NavMeshSurface>();
surf.useGeometry = NavMeshCollectGeometry.PhysicsColliders;
surf.collectObjects = CollectObjects.All;
surf.BuildNavMesh();
if (surf.navMeshData != null)
{
if (AssetDatabase.LoadAssetAtPath<NavMeshData>(NavMeshPath) != null) AssetDatabase.DeleteAsset(NavMeshPath);
AssetDatabase.CreateAsset(surf.navMeshData, NavMeshPath);
}
else sb.Append("🔴 NavMesh 베이크 실패(런타임 베이크로 대체)\n");
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>();
// InGame.unity 의 씬 객체(직렬화 참조 0 · 몬스터 웨이브 스탯 표) — 원본 ActorStatInfo.Reset 이 DungeonInfo.Ins.dic_WaveStat 를 읽어 없으면 PC.Set 이 NRE(#947 1차 Play 실측)
var dg = new GameObject("DungeonInfo"); dg.AddComponent<DungeonInfo>();
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;
}
foreach (var n in IngamePrefabs)
{
var p = AssetDatabase.LoadAssetAtPath<GameObject>(IngamePrefabFolder + 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;
}
// PC(치비) · 더미 — 씬에 직접(StatTest 의 PCPos/MobPos 와 같은 꼴)
Vector3 spawn = cfg != null && cfg.spawnPos != null && cfg.spawnPos.Length >= 3 ? new Vector3(cfg.spawnPos[0], cfg.spawnPos[1], cfg.spawnPos[2]) : Vector3.zero;
var pcPos = new GameObject("PCPos"); pcPos.transform.position = spawn;
var pcPrefab = AssetDatabase.LoadAssetAtPath<GameObject>(PcPrefabPath());
Actor pcActor = null;
// 🔴 PC·더미는 꺼 둔 채 저장 — 하네스가 Set 직전에 켠다. 켜진 채 두면 Set(Init_CC) 전에 MobActor.Update→IsCC 가 매 프레임 KeyNotFound(Freeze) 예외(#947 1차 Play 1000건)
if (pcPrefab != null) { var pc = PrefabUtility.InstantiatePrefab(pcPrefab, scene) as GameObject; pc.transform.SetParent(pcPos.transform, false); pcActor = pc.GetComponent<Actor>(); pc.SetActive(false); }
else sb.Append("🔴 PC 프리팹 없음\n");
var mobPos = new GameObject("MobPos"); mobPos.transform.position = spawn + new Vector3(2f, 0f, -1.5f);
var mobPrefab = AssetDatabase.LoadAssetAtPath<GameObject>(DummyPrefab);
MobActor mob = null;
if (mobPrefab != null) { var m = PrefabUtility.InstantiatePrefab(mobPrefab, scene) as GameObject; m.transform.SetParent(mobPos.transform, false); mob = m.GetComponent<MobActor>(); m.SetActive(false); }
else sb.Append("🔴 더미 프리팹 없음 " + DummyPrefab + "\n");
if (pcActor == null) sb.Append("🔴 PC 프리팹에 Actor 없음\n");
if (mob == null) sb.Append("🔴 더미 프리팹에 MobActor 없음\n");
// 글자 4(원본 분기 Set_Dmglabel 이 labels[2] 에 피해를 적는다)
var canvasGo = new GameObject("WLSkillTestCanvas");
var canvas = canvasGo.AddComponent<Canvas>(); canvas.renderMode = RenderMode.ScreenSpaceOverlay; canvas.sortingOrder = 50;
canvasGo.AddComponent<CanvasScaler>(); canvasGo.AddComponent<GraphicRaycaster>();
var labels = new TextMeshProUGUI[4];
labels[0] = MakeLabel(canvasGo.transform, "pcstat", new Vector2(0f, 0f), new Vector2(10f, 10f), new Vector2(360f, 220f), TextAlignmentOptions.BottomLeft);
labels[1] = MakeLabel(canvasGo.transform, "mobstat", new Vector2(1f, 0f), new Vector2(-10f, 10f), new Vector2(360f, 220f), TextAlignmentOptions.BottomRight);
labels[2] = MakeLabel(canvasGo.transform, "dmg", new Vector2(1f, 1f), new Vector2(-10f, -10f), new Vector2(420f, 300f), TextAlignmentOptions.TopRight);
labels[3] = MakeLabel(canvasGo.transform, "classinfo", new Vector2(0.5f, 0f), new Vector2(0f, 10f), new Vector2(600f, 40f), TextAlignmentOptions.Bottom);
foreach (var l in labels) l.raycastTarget = false; // 글자가 가상패드 터치를 가리지 않게
sb.Append(AddOriginalJoystick(canvasGo, scene));
var hGo = new GameObject("WLSkillTestHarness");
var h = hGo.AddComponent<WLSkillTestHarness>();
h.pcActor = pcActor; h.dummyActor = mob;
h.actors = new Actor[] { pcActor, mob };
h.labels = labels;
bool saved = EditorSceneManager.SaveScene(scene, ScenePath);
sb.Append(saved ? "저장 " : "🔴 저장 실패 ").Append(ScenePath).Append(" · 루트 ").Append(scene.rootCount).Append(" · PC ").Append(PcPrefabPath()).Append('\n');
if (!untitled)
{ // Title 등에서 불렀으면 원래 씬으로 돌아가고 테스트 씬은 닫는다(Untitled 에서 불렀으면 저장된 테스트 씬이 그대로 열려 있다)
if (prevActive.IsValid() && prevActive.isLoaded) SceneManager.SetActiveScene(prevActive);
EditorSceneManager.CloseScene(scene, true);
}
AssetDatabase.SaveAssets();
return sb.ToString();
}
}
}