// PD 지시 #757 — 몬스터 스포너 배치 사전 실측 프로브 (읽기 전용) // 실행: unity command run_script --file AgentScripts/WL_SpawnerProbe.cs --entry WL_SpawnerProbe.<메서드> // CLAUDE.md §1 준수: using 금지 · 네임스페이스 풀어 쓰기 public static class WL_SpawnerProbe { const string kMapC01 = "Assets/Res_Addr/Map/Map_C01.prefab"; const string kWLStart = "Assets/Res_Addr/Map/WL_Start.prefab"; const string kScene = "Assets/Scenes/WL_Start.unity"; // ── A. Map_C01 스포너 템플릿 실측 ──────────────────────────── public static void DumpTemplate() { var root = UnityEditor.PrefabUtility.LoadPrefabContents(kMapC01); try { var mgrs = root.GetComponentsInChildren(true); L("[C01] MobControlMgr " + mgrs.Length + "개"); var listComp = root.GetComponentInChildren(true); L("[C01] MobControlMgrList = " + (listComp == null ? "없음" : Path(listComp.transform, root.transform))); for (int i = 0; i < mgrs.Length; i++) { var m = mgrs[i]; var t = m.transform; L(string.Format("[C01] #{0} path={1} id={2} range={3} pos={4} rot={5} scale={6} layer={7}({8}) tag={9} active={10}", i, Path(t, root.transform), m.SpawnerID, m.MobGenRange, t.position.ToString("F3"), t.eulerAngles.ToString("F1"), t.localScale.ToString("F3"), m.gameObject.layer, UnityEngine.LayerMask.LayerToName(m.gameObject.layer), m.gameObject.tag, m.gameObject.activeSelf)); } // 첫 스포너의 전체 컴포넌트/자식 구조 if (mgrs.Length > 0) DumpDeep(mgrs[0].gameObject, root.transform, " "); // text_lv 참조 대상 확인 for (int i = 0; i < mgrs.Length && i < 3; i++) { var lv = mgrs[i].text_lv; L("[C01] #" + i + " text_lv = " + (lv == null ? "NULL" : Path(lv.transform, root.transform) + " localPos=" + lv.transform.localPosition.ToString("F3") + " localRot=" + lv.transform.localEulerAngles.ToString("F1") + " localScale=" + lv.transform.localScale.ToString("F3") + " fontSize=" + lv.fontSize + " color=" + lv.color + " align=" + lv.alignment + " font=" + (lv.font == null ? "null" : lv.font.name))); } } finally { UnityEditor.PrefabUtility.UnloadPrefabContents(root); } } // ── B. WL_Start 프리팹 구조 ───────────────────────────────── public static void DumpWLStart() { var root = UnityEditor.PrefabUtility.LoadPrefabContents(kWLStart); try { L("[WL] 루트 자식 " + root.transform.childCount + "개:"); for (int i = 0; i < root.transform.childCount; i++) { var c = root.transform.GetChild(i); L(" - " + c.name + " 자식=" + c.childCount + " comps=" + Comps(c.gameObject) + " pos=" + c.position.ToString("F2")); } var lst = root.GetComponentInChildren(true); L("[WL] MobControlMgrList = " + (lst == null ? "없음" : Path(lst.transform, root.transform))); var mgrs = root.GetComponentsInChildren(true); L("[WL] 기존 MobControlMgr = " + mgrs.Length + "개"); var p = FindDeep(root.transform, "Portal_Start"); L("[WL] Portal_Start = " + (p == null ? "없음" : p.position.ToString("F4") + " comps=" + Comps(p.gameObject))); var md = root.GetComponent(); L("[WL] MapData = " + (md == null ? "없음" : "있음")); L("[WL] 루트 컴포넌트: " + Comps(root)); } finally { UnityEditor.PrefabUtility.UnloadPrefabContents(root); } } // ── C. NavMesh 실측 (씬을 열어 삼각분할을 읽는다) ──────────── public static void DumpNavMesh() { var cur = UnityEditor.SceneManagement.EditorSceneManager.GetActiveScene(); L("[Nav] 현재 씬=" + cur.path + " dirty=" + cur.isDirty); if (cur.isDirty) { L("[Nav] 🔴 현재 씬이 dirty — 다이얼로그를 피하려고 중단한다"); return; } var sc = UnityEditor.SceneManagement.EditorSceneManager.OpenScene(kScene, UnityEditor.SceneManagement.OpenSceneMode.Single); L("[Nav] 씬 오픈: " + sc.name + " isLoaded=" + sc.isLoaded); var tri = UnityEngine.AI.NavMesh.CalculateTriangulation(); L("[Nav] 삼각분할 vertices=" + tri.vertices.Length + " indices=" + tri.indices.Length); if (tri.vertices.Length == 0) { L("[Nav] 🔴 에디트 모드에서 NavMesh 가 로드되지 않았다 — NavMeshSurface.AddData 를 강제한다"); ForceAddData(); tri = UnityEngine.AI.NavMesh.CalculateTriangulation(); L("[Nav] 재시도 vertices=" + tri.vertices.Length); } if (tri.vertices.Length == 0) { L("[Nav] 실패 — 종료"); return; } var min = tri.vertices[0]; var max = tri.vertices[0]; for (int i = 1; i < tri.vertices.Length; i++) { min = UnityEngine.Vector3.Min(min, tri.vertices[i]); max = UnityEngine.Vector3.Max(max, tri.vertices[i]); } L("[Nav] bounds min=" + min.ToString("F2") + " max=" + max.ToString("F2") + " size=" + (max - min).ToString("F2")); // 총 보행 면적 double area = 0; for (int i = 0; i + 2 < tri.indices.Length; i += 3) { var a = tri.vertices[tri.indices[i]]; var b = tri.vertices[tri.indices[i + 1]]; var c = tri.vertices[tri.indices[i + 2]]; area += UnityEngine.Vector3.Cross(b - a, c - a).magnitude * 0.5; } L("[Nav] 삼각형 " + (tri.indices.Length / 3) + "개 · 총면적 " + area.ToString("F0") + " m^2"); var portal = UnityEngine.GameObject.Find("Portal_Start"); if (portal == null) { var all = UnityEngine.Object.FindObjectsByType(UnityEngine.FindObjectsSortMode.None); for (int i = 0; i < all.Length; i++) if (all[i].name == "Portal_Start") { portal = all[i].gameObject; break; } } L("[Nav] Portal_Start(씬) = " + (portal == null ? "없음" : portal.transform.position.ToString("F3"))); } // ── D. 베이크된 NavMeshData 에셋을 직접 올려 실측 ────────────── const string kNavAsset = "Assets/WL/Settings/WL_Start_NavMesh.asset"; public static void DumpNavAsset() { var data = UnityEditor.AssetDatabase.LoadAssetAtPath(kNavAsset); if (data == null) { L("[NavA] 에셋 없음: " + kNavAsset); return; } L("[NavA] sourceBounds center=" + data.sourceBounds.center.ToString("F2") + " size=" + data.sourceBounds.size.ToString("F2")); L("[NavA] position=" + data.position.ToString("F2") + " rotation=" + data.rotation.eulerAngles.ToString("F1")); var inst = UnityEngine.AI.NavMesh.AddNavMeshData(data); try { var tri = UnityEngine.AI.NavMesh.CalculateTriangulation(); L("[NavA] vertices=" + tri.vertices.Length + " tris=" + (tri.indices.Length / 3)); if (tri.vertices.Length == 0) { L("[NavA] 🔴 빈 NavMesh"); return; } var min = tri.vertices[0]; var max = tri.vertices[0]; for (int i = 1; i < tri.vertices.Length; i++) { min = UnityEngine.Vector3.Min(min, tri.vertices[i]); max = UnityEngine.Vector3.Max(max, tri.vertices[i]); } L("[NavA] bounds min=" + min.ToString("F2") + " max=" + max.ToString("F2") + " size=" + (max - min).ToString("F2")); double area = 0; for (int i = 0; i + 2 < tri.indices.Length; i += 3) { var a = tri.vertices[tri.indices[i]]; var b = tri.vertices[tri.indices[i + 1]]; var c = tri.vertices[tri.indices[i + 2]]; area += UnityEngine.Vector3.Cross(b - a, c - a).magnitude * 0.5; } L("[NavA] 총 보행면적 = " + area.ToString("F0") + " m^2"); // 스폰 지점 검증 var spawn = new UnityEngine.Vector3(63.441f, -4.641f, -31.815f); UnityEngine.AI.NavMeshHit hit; bool ok = UnityEngine.AI.NavMesh.SamplePosition(spawn, out hit, 5f, UnityEngine.AI.NavMesh.AllAreas); L("[NavA] 스폰 지점 샘플 = " + ok + " -> " + (ok ? hit.position.ToString("F3") : "-")); // 면적 상위 삼각형 20개의 무게중심 (분산 후보 참고용) int n = tri.indices.Length / 3; L("[NavA] 삼각형 개수 " + n); // 20 m 격자로 보행 가능 셀 분포 파악 var cells = new System.Collections.Generic.Dictionary(); for (int i = 0; i + 2 < tri.indices.Length; i += 3) { var a = tri.vertices[tri.indices[i]]; var b = tri.vertices[tri.indices[i + 1]]; var c = tri.vertices[tri.indices[i + 2]]; var ctr = (a + b + c) / 3f; long cx = (long)UnityEngine.Mathf.Floor(ctr.x / 20f), cz = (long)UnityEngine.Mathf.Floor(ctr.z / 20f); long key = cx * 100000 + cz; if (!cells.ContainsKey(key)) cells[key] = 0; cells[key]++; } L("[NavA] 20m 격자 점유 셀 수 = " + cells.Count); } finally { UnityEngine.AI.NavMesh.RemoveNavMeshData(inst); } } static void ForceAddData() { var t = FindType("Unity.AI.Navigation.NavMeshSurface"); if (t == null) { L("[Nav] NavMeshSurface 타입 없음"); return; } var found = UnityEngine.Object.FindObjectsByType(t, UnityEngine.FindObjectsSortMode.None); L("[Nav] NavMeshSurface " + found.Length + "개 발견"); var mi = t.GetMethod("AddData"); for (int i = 0; i < found.Length; i++) { var dataProp = t.GetProperty("navMeshData"); var data = dataProp == null ? null : dataProp.GetValue(found[i]); L("[Nav] surface#" + i + " navMeshData=" + (data == null ? "null" : "있음")); if (mi != null && data != null) mi.Invoke(found[i], null); } } static System.Type FindType(string full) { var asms = System.AppDomain.CurrentDomain.GetAssemblies(); for (int i = 0; i < asms.Length; i++) { var t = asms[i].GetType(full); if (t != null) return t; } return null; } static UnityEngine.Transform FindDeep(UnityEngine.Transform root, string name) { if (root.name == name) return root; for (int i = 0; i < root.childCount; i++) { var r = FindDeep(root.GetChild(i), name); if (r != null) return r; } return null; } static string Path(UnityEngine.Transform t, UnityEngine.Transform root) { var s = t.name; var p = t.parent; while (p != null && p != root) { s = p.name + "/" + s; p = p.parent; } return s; } static string Comps(UnityEngine.GameObject go) { var cs = go.GetComponents(); var sb = new System.Text.StringBuilder(); for (int i = 0; i < cs.Length; i++) { if (sb.Length > 0) sb.Append("+"); sb.Append(cs[i] == null ? "MISSING" : cs[i].GetType().Name); } return sb.ToString(); } static void DumpDeep(UnityEngine.GameObject go, UnityEngine.Transform root, string indent) { L(indent + Path(go.transform, root) + " [" + Comps(go) + "] localPos=" + go.transform.localPosition.ToString("F3") + " localScale=" + go.transform.localScale.ToString("F3") + " layer=" + UnityEngine.LayerMask.LayerToName(go.layer)); for (int i = 0; i < go.transform.childCount; i++) DumpDeep(go.transform.GetChild(i).gameObject, root, indent + " "); } // 콘솔은 스택트레이스가 붙어 토큰을 낭비한다 → 파일로 남긴다 (C57) const string kLogPath = "AgentScripts/staging/WL_Monsters/PROBE_LOG.txt"; static bool s_logInit; static void L(string s) { if (!s_logInit) { var dir = System.IO.Path.GetDirectoryName(kLogPath); if (!System.IO.Directory.Exists(dir)) System.IO.Directory.CreateDirectory(dir); System.IO.File.WriteAllText(kLogPath, ""); s_logInit = true; } System.IO.File.AppendAllText(kLogPath, s + "\n"); } }