// ───────────────────────────────────────────────────────────────────────────── // WLDungeonSceneSetup.cs — 던전 씬(아레나 복사본)을 **런타임**에 전투 가능한 맵으로 만든다 // // PD 지시 「던전은 지금 만든 데모 씬을 쓰면 될 것 같아」 + Lead 「WL_ArenaProto 복사본 2개, 원본은 룩 개발용」 // // ■ 아레나 데모 씬에 없는 것(실측) — 전부 여기서 런타임에 채운다. 씬 파일은 룩 그대로 둔다. // ① NavMesh : 룩 씬이라 베이크 데이터가 없다 → `NavMeshSurface` 를 붙여 런타임 베이크 // ② 몬스터 스포너: `StageSpawner` 는 `MobControlMgr.SpawnerID` 로 몹을 찾는다(StageSpawner.cs:20) // → 스테이지 표가 요구하는 SpawnerID 로 스포너를 만든다(테이블 수정 0 · 몹 종류는 MonsterAppear 표가 정한다) // ③ LoadMapMgr : `PCInfo.Make_Actors:24` 가 부른다 → 섬과 같은 파생 노드로 채운다 // ④ 중복 정리 : 데모 리그(ArenaWalkerRig)·여분 카메라/AudioListener 해제 // // ■ 그 다음 `StageDirector.Enter(stageIndex, "dungeon")` — #815 자산을 그대로 쓴다(코드 0줄). // // 🔴 Debug.LogError / Debug.LogException 을 쓰지 않는다. // ───────────────────────────────────────────────────────────────────────────── using System.Collections; using System.Collections.Generic; using TMPro; using Unity.AI.Navigation; using UnityEngine; using UnityEngine.SceneManagement; using WL.Combat.Stage; namespace WL.Island { /// 던전 씬 준비 + 스테이지 입장. 정적. public static class WLDungeonSceneSetup { /// 입장 요청이 걸린 던전(씬 로드 전에 세팅된다). public static WLDungeonDef Pending; /// 지금 돌고 있는 던전(스테이지 이벤트를 우리 것으로 가려내는 기준). public static WLDungeonDef Current; public static int Prepared, NavBakes, SpawnersMade, EnterCalls; public static string LastLog = ""; static readonly List s_made = new List(4); public static IEnumerator Co_Prepare(WLIslandSettings cfg, Scene scene, WLDungeonDef d) { Prepared++; Current = d; Pending = null; yield return null; // 씬 안 오브젝트들의 Awake 를 끝까지 돌린다 WLIslandSceneSetup.EnsureDungeonMapNodes(cfg, scene, d); FixDuplicates(cfg, scene); // ① NavMesh if (cfg.bakeDungeonNavMesh != 0) BakeNavMesh(cfg, scene); // 🔴 단독 Play(씬만 열고 Play = 룩 확인 경로)에는 테이블·ServerData 가 없다. // 스포너·스테이지는 원본 테이블을 타므로 여기서 멈춘다 — 씬 룩만 확인한다. if (InGameInfo.Ins == null) { LastLog = "던전 씬 단독 Play — 룩만 확인(스포너·스테이지 건너뜀)"; WLIslandBridge.Log(cfg, LastLog); yield break; } // ② 스포너 — 스테이지 표가 요구하는 ID 로만 만든다 s_made.Clear(); var def = StageDefOf(d); if (cfg.spawnDungeonSpawners != 0 && def != null) MakeSpawners(cfg, scene, def); // ③ 스포너 로딩(첫 스폰)이 끝나기를 기다린다 — 안 기다리면 리젠 차단이 첫 스폰을 죽인다(StageSpawner.cs 가드) float t = 0f; while (t < 8f && !AllLoaded()) { t += Time.unscaledDeltaTime; yield return null; } // ④ PC 가 생길 때까지 기다린다(Make_Actors 는 Addressables 비동기다) t = 0f; while (t < 8f && !(MyValue.bMyPC && MyValue.MyPC != null)) { t += Time.unscaledDeltaTime; yield return null; } if (cfg.dungeonEnterDelay > 0f) yield return new WaitForSecondsRealtime(cfg.dungeonEnterDelay); EnterCalls++; bool ok = StageDirector.Enter(d.stageIndex, "dungeon " + d.id); LastLog = "던전 준비 완료 — " + d.displayName + " · 스포너 " + s_made.Count + " · NavMesh " + NavBakes + " · Enter(" + d.stageIndex + ")=" + ok; WLIslandBridge.Log(cfg, LastLog); } public static StageDef StageDefOf(WLDungeonDef d) { var t = WLStageTable.Instance; return t != null ? t.Get(d.stageIndex) : null; } // ───────────────────────────────────────────────────────────────── // ① NavMesh 런타임 베이크 // ───────────────────────────────────────────────────────────────── static void BakeNavMesh(WLIslandSettings cfg, Scene scene) { var surf = Object.FindFirstObjectByType(); if (surf == null) { var go = new GameObject("~WL_DungeonNavMesh"); SceneManager.MoveGameObjectToScene(go, scene); surf = go.AddComponent(); } // 🔴 기본값(RenderMeshes)은 메시마다 Read/Write 를 요구해 콘솔 에러 100건이 난다(실측). // 콜라이더 기준으로 베이크하면 임포트 설정을 하나도 건드리지 않고 조용히 끝난다. surf.useGeometry = UnityEngine.AI.NavMeshCollectGeometry.PhysicsColliders; surf.BuildNavMesh(); NavBakes++; UnityEngine.AI.NavMeshHit hit; var def = StageDefOf(WLDungeonSceneSetup.Current); Vector3 probe = def != null ? def.anchor : Vector3.zero; bool onMesh = UnityEngine.AI.NavMesh.SamplePosition(probe, out hit, 6f, UnityEngine.AI.NavMesh.AllAreas); WLIslandBridge.Log(cfg, "던전 NavMesh 런타임 베이크 완료 — 앵커 " + probe.ToString("F1") + " 위 NavMesh " + (onMesh ? ("있음(" + hit.position.ToString("F2") + ")") : "🔴 없음")); } // ───────────────────────────────────────────────────────────────── // ② 스포너 생성 (테이블 수정 0 — MonsterAppear 표의 기존 SpawnerID 를 그대로 쓴다) // ───────────────────────────────────────────────────────────────── static void MakeSpawners(WLIslandSettings cfg, Scene scene, StageDef def) { if (def.waves == null) return; for (int i = 0; i < def.waves.Length; i++) { var w = def.waves[i]; if (w == null || w.spawnerId == 0) continue; if (StageSpawner.Find(w.spawnerId) != null) continue; // 이미 씬에 있으면 그대로 쓴다 var go = new GameObject("~WL_Spawner_" + w.spawnerId); go.SetActive(false); // 🔴 필드부터 채우고 깨운다 SceneManager.MoveGameObjectToScene(go, scene); go.transform.position = def.anchor; var labelGo = new GameObject("lv"); labelGo.transform.SetParent(go.transform, false); labelGo.transform.localPosition = new Vector3(0f, 2.2f, 0f); var tmp = labelGo.AddComponent(); tmp.text = "Lv.???"; tmp.fontSize = 2.4f; tmp.alignment = TextAlignmentOptions.Center; labelGo.SetActive(false); // 던전 바닥에 레벨표가 뜨지 않게 var mgr = go.AddComponent(); mgr.SpawnerID = w.spawnerId; mgr.MobGenRange = cfg.dungeonSpawnerRange; mgr.text_lv = tmp; go.SetActive(true); s_made.Add(mgr); SpawnersMade++; } WLIslandBridge.Log(cfg, "던전 스포너 " + s_made.Count + "개 생성(앵커 " + def.anchor + ")"); } static bool AllLoaded() { for (int i = 0; i < s_made.Count; i++) if (s_made[i] == null || !s_made[i].Get_LoadComplete()) return false; return s_made.Count > 0; } // ───────────────────────────────────────────────────────────────── // ④ 중복 정리 — 데모 리그·여분 카메라 // ───────────────────────────────────────────────────────────────── public static void FixDuplicates(WLIslandSettings cfg, Scene scene) { if (cfg.fixDungeonDuplicates == 0) return; if (InGameInfo.Ins == null) return; // 단독 Play(룩 확인)에서는 데모 리그가 유일한 볼거리다 int rigs = 0, cams = 0, listeners = 0; var walkers = Object.FindObjectsByType(FindObjectsSortMode.None); for (int i = 0; i < walkers.Length; i++) if (walkers[i] != null) { walkers[i].gameObject.SetActive(false); rigs++; } var allCams = Object.FindObjectsByType(FindObjectsSortMode.None); for (int i = 0; i < allCams.Length; i++) { var c = allCams[i]; if (c == null || c.gameObject.scene != scene) continue; if (c == Camera.main) continue; c.enabled = false; cams++; var al = c.GetComponent(); if (al != null && al.enabled) { al.enabled = false; listeners++; } } WLIslandBridge.Log(cfg, "던전 중복 정리 — 데모 리그 " + rigs + " · 카메라 " + cams + " · 리스너 " + listeners); } } }