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