diff --git a/AgentScripts/WL813t2_Bake.cs b/AgentScripts/WL813t2_Bake.cs new file mode 100644 index 000000000..3a91cb745 --- /dev/null +++ b/AgentScripts/WL813t2_Bake.cs @@ -0,0 +1,279 @@ +// 발주서 WL-813t2 — WL_Nature NavMesh 재베이크 (언덕 = 이동 불가) +// +// 실행 (CLAUDE.md §1: using 금지 · 네임스페이스 풀어 쓰기) +// 대조군(현행 프리팹 그대로 재베이크 · 저장 0) : +// unity command --project-path E:\NerdNavis\WL_wt\systems run_script --file AgentScripts/WL813t2_Bake.cs --entry WL813t2_Bake.Control --timeout 1800 +// 본 베이크(WL 볼륨 노드 포함 · 자산 갱신) : +// … --entry WL813t2_Bake.Apply --timeout 1800 +// 검증(자산 재로드 후 면적·연결성·스폰 격자) : +// … --entry WL813t2_Bake.Verify --timeout 1800 +// +// 🔴 이 스크립트는 씬을 새로 만들고(EmptyScene) 프리팹을 임시 인스턴스화한 뒤 베이크한다. 씬은 저장하지 않는다. +// 자산 갱신은 GUID 보존을 위해 「임시 자산 생성 → 파일 바이트 덮어쓰기 → 임시 삭제 → 재임포트」로 한다. + +public static class WL813t2_Bake +{ + const string kMapPrefab = "Assets/Res_Addr/Map/WL_Nature.prefab"; + const string kNavAsset = "Assets/WL/Settings/WL_Nature_NavMesh.asset"; + const string kTmpAsset = "Assets/WL/Settings/WL_Nature_NavMesh_813t2TMP.asset"; + const string kOutDir = "AgentScripts/staging/WL813t2"; + + static System.Text.StringBuilder s; + static void L(string t) { s.AppendLine(t); UnityEngine.Debug.Log("[WL813t2] " + t); } + + static string Abs(string rel) + { + return System.IO.Path.Combine(System.IO.Directory.GetCurrentDirectory(), + rel.Replace('/', System.IO.Path.DirectorySeparatorChar)); + } + + static string Flush(string path) + { + try + { + System.IO.Directory.CreateDirectory(Abs(kOutDir)); + System.IO.File.WriteAllText(Abs(path), s.ToString(), new System.Text.UTF8Encoding(true)); + } + catch (System.Exception e) { UnityEngine.Debug.LogWarning("[WL813t2] 로그 저장 실패: " + e.Message); } + return s.ToString(); + } + + // ───────────────────────────────────────────────────────────────────────── + public static string Control() { return Run(false, kOutDir + "/BAKE_CONTROL.txt"); } + public static string Apply() { return Run(true, kOutDir + "/BAKE_APPLY.txt"); } + + static string Run(bool save, string logPath) + { + s = new System.Text.StringBuilder(); + L("===== WL-813t2 베이크 " + (save ? "APPLY(자산 갱신)" : "CONTROL(저장 0)") + " · " + System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " ====="); + + UnityEditor.AssetDatabase.ImportAsset(kMapPrefab, UnityEditor.ImportAssetOptions.ForceUpdate); + var prefab = UnityEditor.AssetDatabase.LoadAssetAtPath(kMapPrefab); + if (prefab == null) { L("[에러] 프리팹 로드 실패"); return Flush(logPath); } + + // 현행(커밋본) 자산 상태 + var oldData = UnityEditor.AssetDatabase.LoadAssetAtPath(kNavAsset); + long oldBytes = 0; + try { oldBytes = new System.IO.FileInfo(Abs(kNavAsset)).Length; } catch { } + L("현행 자산 = " + kNavAsset + " · " + oldBytes + " bytes · " + (oldData != null ? "로드 OK" : "로드 실패")); + if (oldData != null) Report("현행(커밋본)", oldData, null); + + // ── 임시 씬 + 프리팹 인스턴스 ──────────────────────────────────────── + var scene = UnityEditor.SceneManagement.EditorSceneManager.NewScene( + UnityEditor.SceneManagement.NewSceneSetup.EmptyScene, UnityEditor.SceneManagement.NewSceneMode.Single); + var go = UnityEditor.PrefabUtility.InstantiatePrefab(prefab) as UnityEngine.GameObject; + if (go == null) { L("[에러] 인스턴스화 실패"); return Flush(logPath); } + L("인스턴스 root transform pos=" + go.transform.position.ToString("F3") + + " rot=" + go.transform.rotation.eulerAngles.ToString("F1") + " scale=" + go.transform.localScale.ToString("F3")); + + var sf = go.GetComponentInChildren(true); + if (sf == null) { L("[에러] NavMeshSurface 없음"); return Flush(logPath); } + + var vols = go.GetComponentsInChildren(true); + L("NavMeshModifierVolume " + vols.Length + "개"); + for (int i = 0; i < vols.Length; i++) + { + var v = vols[i]; + var wc = v.transform.TransformPoint(v.center); + L(" [" + i + "] " + Path(v.transform) + " · area=" + v.area + "(" + UnityEngine.AI.NavMesh.GetAreaNames()[UnityEngine.Mathf.Clamp(v.area, 0, 31)] + ")" + + " · center(world)=" + wc.ToString("F1") + " size=" + v.size.ToString("F1") + + " · Y[" + (wc.y - v.size.y * 0.5f).ToString("F2") + ", " + (wc.y + v.size.y * 0.5f).ToString("F2") + "]" + + " · active=" + v.gameObject.activeInHierarchy + " enabled=" + v.enabled + + " · affectsAgent0=" + v.AffectsAgentType(0) + " layer=" + v.gameObject.layer); + } + + // ── 베이크 ────────────────────────────────────────────────────────── + var sw = System.Diagnostics.Stopwatch.StartNew(); + var keep = sf.navMeshData; + sf.navMeshData = null; // 새 데이터로 굽는다(기존 자산을 덮어쓰지 않게) + sf.BuildNavMesh(); + sw.Stop(); + var built = sf.navMeshData; + L("베이크 " + (sw.ElapsedMilliseconds / 1000.0).ToString("F1") + " s · 결과 " + (built != null ? "OK" : "null")); + if (built == null) { sf.navMeshData = keep; return Flush(logPath); } + + sf.RemoveData(); // BuildNavMesh 가 이미 AddData 했다 — Report 가 다시 올리므로 중복(면적 2배)을 막는다 + Report("재베이크", built, go); + + if (save) + { + // GUID 보존 저장: 임시 자산 → 파일 바이트 덮어쓰기 → 임시 삭제 → 재임포트 + if (UnityEditor.AssetDatabase.LoadAssetAtPath(kTmpAsset) != null) + UnityEditor.AssetDatabase.DeleteAsset(kTmpAsset); + UnityEditor.AssetDatabase.CreateAsset(built, kTmpAsset); + UnityEditor.AssetDatabase.SaveAssets(); + System.IO.File.Copy(Abs(kTmpAsset), Abs(kNavAsset), true); + UnityEditor.AssetDatabase.DeleteAsset(kTmpAsset); + UnityEditor.AssetDatabase.ImportAsset(kNavAsset, UnityEditor.ImportAssetOptions.ForceUpdate); + long newBytes = 0; + try { newBytes = new System.IO.FileInfo(Abs(kNavAsset)).Length; } catch { } + L("자산 저장 완료 · " + oldBytes + " → " + newBytes + " bytes (" + (newBytes - oldBytes) + ")"); + L("GUID = " + UnityEditor.AssetDatabase.AssetPathToGUID(kNavAsset) + " (기대 ef300913c80dbda47b476b9261fdd786)"); + } + else L("저장 안 함(대조군)."); + + sf.navMeshData = keep; + UnityEngine.Object.DestroyImmediate(go); + L("씬 정리 완료(저장 0) · scene=" + scene.name); + return Flush(logPath); + } + + // ───────────────────────────────────────────────────────────────────────── + public static string Verify() + { + s = new System.Text.StringBuilder(); + L("===== WL-813t2 검증 (" + System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + ") ====="); + UnityEditor.AssetDatabase.ImportAsset(kNavAsset, UnityEditor.ImportAssetOptions.ForceUpdate); + var data = UnityEditor.AssetDatabase.LoadAssetAtPath(kNavAsset); + if (data == null) { L("[에러] 자산 로드 실패"); return Flush(kOutDir + "/VERIFY.txt"); } + long bytes = 0; try { bytes = new System.IO.FileInfo(Abs(kNavAsset)).Length; } catch { } + L("자산 " + kNavAsset + " · " + bytes + " bytes"); + var prefab = UnityEditor.AssetDatabase.LoadAssetAtPath(kMapPrefab); + Report("검증", data, prefab); + return Flush(kOutDir + "/VERIFY.txt"); + } + + // ───────────────────────────────────────────────────────────────────────── + /// NavMeshData 를 잠시 올려 면적·y 대역·격자·연결성·스폰 표집을 잰다. + static void Report(string tag, UnityEngine.AI.NavMeshData data, UnityEngine.GameObject mapRoot) + { + var inst = UnityEngine.AI.NavMesh.AddNavMeshData(data); + try + { + var tri = UnityEngine.AI.NavMesh.CalculateTriangulation(); + int n = tri.indices.Length / 3; + double total = 0.0, a15 = 0.0, a20 = 0.0, a25 = 0.0, a30 = 0.0, a35 = 0.0, a50 = 0.0; + float yMin = 1e9f, yMax = -1e9f; + for (int t = 0; t < n; t++) + { + var a = tri.vertices[tri.indices[t * 3 + 0]]; + var b = tri.vertices[tri.indices[t * 3 + 1]]; + var c = tri.vertices[tri.indices[t * 3 + 2]]; + double ar = UnityEngine.Vector3.Cross(b - a, c - a).magnitude * 0.5; + float cy = (a.y + b.y + c.y) / 3f; + float hy = UnityEngine.Mathf.Max(a.y, UnityEngine.Mathf.Max(b.y, c.y)); + total += ar; + if (cy > 1.5f) a15 += ar; if (cy > 2.0f) a20 += ar; if (cy > 2.5f) a25 += ar; + if (cy > 3.0f) a30 += ar; if (cy > 3.5f) a35 += ar; if (cy > 5.0f) a50 += ar; + if (cy < yMin) yMin = cy; if (hy > yMax) yMax = hy; + } + L("[" + tag + "] 삼각형 " + n + " · 정점 " + tri.vertices.Length + " · 총 면적 " + total.ToString("F1") + + " m^2 · y " + yMin.ToString("F2") + " ~ " + yMax.ToString("F2")); + L("[" + tag + "] y>1.5 " + a15.ToString("F1") + " · y>2.0 " + a20.ToString("F1") + " · y>2.5 " + a25.ToString("F1") + + " · **y>3.0 " + a30.ToString("F1") + "** · y>3.5 " + a35.ToString("F1") + " · y>5.0 " + a50.ToString("F1") + " m^2"); + + if (mapRoot != null) + { + // 연결성 — 시작점 → 스포너 23 · 아레나 + var portal = FindByName(mapRoot.transform, "Portal_Start"); + UnityEngine.Vector3 startPos = portal != null ? portal.position : new UnityEngine.Vector3(115f, 0f, 10f); + UnityEngine.AI.NavMeshHit sh; + if (!UnityEngine.AI.NavMesh.SamplePosition(startPos, out sh, 5f, UnityEngine.AI.NavMesh.AllAreas)) + { L("[" + tag + "] [에러] 시작 지점이 NavMesh 밖"); } + else + { + var mgrs = mapRoot.GetComponentsInChildren(true); + int comp = 0, part = 0, none = 0; + for (int i = 0; i < mgrs.Length; i++) + { + var p = mgrs[i].transform.position; + UnityEngine.AI.NavMeshHit h; + if (!UnityEngine.AI.NavMesh.SamplePosition(p, out h, 5f, UnityEngine.AI.NavMesh.AllAreas)) { none++; L(" #" + mgrs[i].SpawnerID + " NavMesh 밖 " + p.ToString("F1")); continue; } + var path = new UnityEngine.AI.NavMeshPath(); + UnityEngine.AI.NavMesh.CalculatePath(sh.position, h.position, UnityEngine.AI.NavMesh.AllAreas, path); + if (path.status == UnityEngine.AI.NavMeshPathStatus.PathComplete) comp++; + else { part++; L(" #" + mgrs[i].SpawnerID + " " + path.status + " " + p.ToString("F1")); } + } + L("[" + tag + "] 시작→스포너 " + mgrs.Length + " : PathComplete " + comp + " · Partial/Invalid " + part + " · 밖 " + none); + + var arenaCfg = UnityEngine.Resources.Load("WL/WLBossArenaSettings"); + if (arenaCfg != null) + { + UnityEngine.AI.NavMeshHit ah; + bool on = UnityEngine.AI.NavMesh.SamplePosition(arenaCfg.arenaCenter, out ah, 5f, UnityEngine.AI.NavMesh.AllAreas); + string st = "밖"; + if (on) + { + var p2 = new UnityEngine.AI.NavMeshPath(); + UnityEngine.AI.NavMesh.CalculatePath(sh.position, ah.position, UnityEngine.AI.NavMesh.AllAreas, p2); + st = ah.position.ToString("F2") + " · 시작→아레나 " + p2.status; + } + L("[" + tag + "] 아레나 " + arenaCfg.arenaCenter.ToString("F1") + " → " + st); + + // 아레나 원주 12방향에서 아레나 중심으로 경로(아레나 내부 연결성) + int okr = 0; + for (int i = 0; i < 12 && on; i++) + { + float ang = i * UnityEngine.Mathf.PI * 2f / 12f; + var rp = arenaCfg.arenaCenter + new UnityEngine.Vector3(UnityEngine.Mathf.Cos(ang), 0f, UnityEngine.Mathf.Sin(ang)) * (arenaCfg.arenaRadius * 0.8f); + UnityEngine.AI.NavMeshHit rh; + if (!UnityEngine.AI.NavMesh.SamplePosition(rp, out rh, 5f, UnityEngine.AI.NavMesh.AllAreas)) continue; + var p3 = new UnityEngine.AI.NavMeshPath(); + UnityEngine.AI.NavMesh.CalculatePath(rh.position, ah.position, UnityEngine.AI.NavMesh.AllAreas, p3); + if (p3.status == UnityEngine.AI.NavMeshPathStatus.PathComplete) okr++; + } + L("[" + tag + "] 아레나 원주 12점 → 중심 PathComplete " + okr + "/12"); + } + + // 주요 앵커 y + string[] anchors = { "Portal_Start", "Reincarnation_Start" }; + for (int i = 0; i < anchors.Length; i++) + { + var tr = FindByName(mapRoot.transform, anchors[i]); + if (tr == null) { L("[" + tag + "] 앵커 " + anchors[i] + " 없음"); continue; } + UnityEngine.AI.NavMeshHit hh; + bool onA = UnityEngine.AI.NavMesh.SamplePosition(tr.position, out hh, 5f, UnityEngine.AI.NavMesh.AllAreas); + L("[" + tag + "] 앵커 " + anchors[i] + " " + tr.position.ToString("F2") + " → " + (onA ? hh.position.ToString("F2") : "NavMesh 밖")); + } + + // 스폰 표집 — 스포너별 랜덤 표집(원본 DSUtil 규약 재현) 1000점의 y 분포 + SpawnProbe(tag, mgrs, sh.position); + } + } + } + finally { UnityEngine.AI.NavMesh.RemoveNavMeshData(inst); } + } + + /// 원본 Get_RandomPos_onNavMesh_Reachable 규약(스냅 2 m · 50회 · PathComplete)으로 1000점 표집. + static void SpawnProbe(string tag, MobControlMgr[] mgrs, UnityEngine.Vector3 startOnMesh) + { + if (mgrs.Length == 0) return; + UnityEngine.Random.InitState(813); + int samples = 1000, hi = 0, offMesh = 0, fellBack = 0; + var band = new System.Collections.Generic.SortedDictionary(); + for (int i = 0; i < samples; i++) + { + var mgr = mgrs[i % mgrs.Length]; + var origin = mgr.transform.position; + var p = DSUtil.Get_RandomPos_onNavMesh_Reachable(origin, 0f, mgr.MobGenRange, 2f, 50); + if ((p - origin).sqrMagnitude < 1e-6f) fellBack++; + UnityEngine.AI.NavMeshHit h; + if (!UnityEngine.AI.NavMesh.SamplePosition(p, out h, 1f, UnityEngine.AI.NavMesh.AllAreas)) offMesh++; + float y = h.hit ? h.position.y : p.y; + if (y > 3.0f) hi++; + int k = UnityEngine.Mathf.FloorToInt(y); int c; band.TryGetValue(k, out c); band[k] = c + 1; + } + L("[" + tag + "] 스폰 표집 " + samples + "점 · **y>3.0 " + hi + "** · NavMesh 밖 " + offMesh + " · 원점 폴백 " + fellBack); + var sb = new System.Text.StringBuilder(" y 분포:"); + foreach (var kv in band) sb.Append(" [" + kv.Key + "]=" + kv.Value); + L(sb.ToString()); + } + + static string Path(UnityEngine.Transform t) + { + string p = t.name; + while (t.parent != null) { t = t.parent; p = t.name + "/" + p; } + return p; + } + + static UnityEngine.Transform FindByName(UnityEngine.Transform t, string name) + { + if (t.name == name) return t; + for (int i = 0; i < t.childCount; i++) + { + var r = FindByName(t.GetChild(i), name); + if (r != null) return r; + } + return null; + } +} diff --git a/AgentScripts/WL813t2_Probe.cs b/AgentScripts/WL813t2_Probe.cs new file mode 100644 index 000000000..75b83a7a4 --- /dev/null +++ b/AgentScripts/WL813t2_Probe.cs @@ -0,0 +1,318 @@ +// 발주서 WL-813t2 — 언덕 지형 이동 불가: 실측 프로브 (NavMesh 출처·베이크 설정·고지대 면적·스포너 겹침) +// +// 실행 (CLAUDE.md §1: using 금지 · 네임스페이스 풀어 쓰기) +// unity command --project-path E:\NerdNavis\WL_wt\systems run_script --file AgentScripts/WL813t2_Probe.cs --entry WL813t2_Probe.Measure --timeout 900 +// +// 재는 것 — 전부 AssetDatabase 읽기 전용(에셋 수정 0): +// (a) WL_Nature 가 쓰는 NavMesh 출처 = NavMeshSurface.m_NavMeshData 자산 경로 · 베이크 설정(agent radius/height/slope/climb) +// (b) NavMesh 삼각형 면적을 y 대역별로 집계 → 「언덕 위 NavMesh 면적」 +// (c) 샘플 격자(2 m) — 표면 바운즈 안 (x,z) 마다 SamplePosition → on/off · y +// (d) 고지대 삼각형을 4 m 격자로 묶어 연결 성분(클러스터) → AABB + 면적 (= NavMeshModifierVolume 배치 후보) +// (e) 스포너 노드(MobControlMgr) 좌표 · y · 고지대 여부 +// (f) 경로 연결성 — 시작점 · 존 앵커 · 아레나 사이 CalculatePath +// +// 수치는 전부 프로젝트 데이터에서 읽는다(C45). 프로브 상수는 "어디를 보나"(대역 폭 · 격자 간격)뿐이다. + +public static class WL813t2_Probe +{ + const string kMapPrefab = "Assets/Res_Addr/Map/WL_Nature.prefab"; + const string kOutDir = "AgentScripts/staging/WL813t2"; + const string kLog = kOutDir + "/MEASURE.txt"; + + static System.Text.StringBuilder s; + static void L(string t) { s.AppendLine(t); UnityEngine.Debug.Log("[WL813t2] " + t); } + + static string Abs(string rel) + { + return System.IO.Path.Combine(System.IO.Directory.GetCurrentDirectory(), + rel.Replace('/', System.IO.Path.DirectorySeparatorChar)); + } + + static string Flush(string path) + { + try + { + System.IO.Directory.CreateDirectory(Abs(kOutDir)); + System.IO.File.WriteAllText(Abs(path), s.ToString(), new System.Text.UTF8Encoding(true)); + } + catch (System.Exception e) { UnityEngine.Debug.LogWarning("[WL813t2] 로그 저장 실패: " + e.Message); } + return s.ToString(); + } + + // ───────────────────────────────────────────────────────────────────────── + public static string Measure() + { + s = new System.Text.StringBuilder(); + L("===== WL-813t2 실측 (" + System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + ") ====="); + + var root = UnityEditor.AssetDatabase.LoadAssetAtPath(kMapPrefab); + if (root == null) { L("[에러] 맵 프리팹 로드 실패: " + kMapPrefab); return Flush(kLog); } + + // ── (a) NavMesh 출처 · 베이크 설정 ─────────────────────────────────── + L(""); + L("── (a) NavMesh 출처 · 베이크 설정 ──"); + var surfaces = root.GetComponentsInChildren(true); + L(" NavMeshSurface 개수 = " + surfaces.Length); + UnityEngine.AI.NavMeshData navData = null; + UnityEngine.Bounds volBounds = new UnityEngine.Bounds(UnityEngine.Vector3.zero, UnityEngine.Vector3.one); + for (int i = 0; i < surfaces.Length; i++) + { + var sf = surfaces[i]; + string dataPath = sf.navMeshData != null ? UnityEditor.AssetDatabase.GetAssetPath(sf.navMeshData) : "(null)"; + L(" [" + i + "] " + sf.name + " · agentTypeID=" + sf.agentTypeID + " · collectObjects=" + sf.collectObjects + + " · size=" + sf.size.ToString("F0") + " · center=" + sf.center.ToString("F0") + + " · layerMask=" + sf.layerMask.value + " · useGeometry=" + sf.useGeometry + + " · defaultArea=" + sf.defaultArea + " · minRegionArea=" + sf.minRegionArea + + " · overrideVoxel=" + sf.overrideVoxelSize + " voxel=" + sf.voxelSize.ToString("F5") + + " · overrideTile=" + sf.overrideTileSize + " tile=" + sf.tileSize); + L(" 데이터 = " + dataPath + (sf.navMeshData != null ? " · 원점=" + sf.navMeshData.position.ToString("F1") : "")); + if (i == 0) + { + navData = sf.navMeshData; + volBounds = new UnityEngine.Bounds(sf.transform.TransformPoint(sf.center), sf.size); + } + } + var bs = UnityEngine.AI.NavMesh.GetSettingsByID(0); + L(" Agent 0 (" + UnityEngine.AI.NavMesh.GetSettingsNameFromID(0) + ") radius=" + bs.agentRadius + + " height=" + bs.agentHeight + " slope=" + bs.agentSlope + " climb=" + bs.agentClimb + + " minRegionArea=" + bs.minRegionArea + " voxel=" + bs.voxelSize.ToString("F5")); + L(" NavMeshModifierVolume 기존 개수 = " + root.GetComponentsInChildren(true).Length); + L(" NavMeshModifier 기존 개수 = " + root.GetComponentsInChildren(true).Length); + if (navData == null) { L("[에러] NavMeshData 없음"); return Flush(kLog); } + + // ── 기준 지면 · 앵커 ──────────────────────────────────────────────── + var portal = FindByName(root.transform, "Portal_Start"); + UnityEngine.Vector3 startPos = portal != null ? portal.position : new UnityEngine.Vector3(115f, 0f, 10f); + L(" Portal_Start = " + startPos.ToString("F2")); + + var inst = UnityEngine.AI.NavMesh.AddNavMeshData(navData); + try + { + var tri = UnityEngine.AI.NavMesh.CalculateTriangulation(); + int triCount = tri.indices.Length / 3; + L(" NavMesh 삼각형 " + triCount + " · 정점 " + tri.vertices.Length); + + // ── (b) y 대역별 면적 ──────────────────────────────────────────── + L(""); + L("── (b) NavMesh 면적 y 대역 분포 (삼각형 3D 면적 합 · m^2) ──"); + double total = 0.0; + double yMin = 1e9, yMax = -1e9; + var band = new System.Collections.Generic.SortedDictionary(); + for (int t = 0; t < triCount; t++) + { + var a = tri.vertices[tri.indices[t * 3 + 0]]; + var b = tri.vertices[tri.indices[t * 3 + 1]]; + var c = tri.vertices[tri.indices[t * 3 + 2]]; + double ar = UnityEngine.Vector3.Cross(b - a, c - a).magnitude * 0.5; + total += ar; + float cy = (a.y + b.y + c.y) / 3f; + if (cy < yMin) yMin = cy; if (cy > yMax) yMax = cy; + int k = UnityEngine.Mathf.FloorToInt(cy); // 1 m 대역 + double cur; band.TryGetValue(k, out cur); band[k] = cur + ar; + } + L(" 총 면적 = " + total.ToString("F1") + " m^2 · y 범위 " + yMin.ToString("F2") + " ~ " + yMax.ToString("F2")); + double cum = 0.0; + foreach (var kv in band) + { + cum += kv.Value; + L(" y [" + kv.Key + ", " + (kv.Key + 1) + ") 면적 " + kv.Value.ToString("F1") + + " (" + (kv.Value / total * 100.0).ToString("F2") + " %) · 누적 " + (cum / total * 100.0).ToString("F2") + " %"); + } + for (int i = 0; i < 6; i++) + { + float thr = 1.5f + i * 1.0f; + double hi = 0.0; + for (int t = 0; t < triCount; t++) + { + var a = tri.vertices[tri.indices[t * 3 + 0]]; + var b = tri.vertices[tri.indices[t * 3 + 1]]; + var c = tri.vertices[tri.indices[t * 3 + 2]]; + float cy = (a.y + b.y + c.y) / 3f; + if (cy > thr) hi += UnityEngine.Vector3.Cross(b - a, c - a).magnitude * 0.5; + } + L(" y > " + thr.ToString("F1") + " m 면적 = " + hi.ToString("F1") + " m^2 (" + (hi / total * 100.0).ToString("F2") + " %)"); + } + + // ── (c) 샘플 격자 ──────────────────────────────────────────────── + L(""); + L("── (c) 샘플 격자 2 m (셀 중심 수직선 1 m 간격 · SamplePosition 반경 1 m) ──"); + GridScan(volBounds, 2f, 1f, (float)yMin - 1f, (float)yMax + 1f); + + // ── (d) 고지대 클러스터 (4 m 셀 · 8이웃 연결 성분) ─────────────── + L(""); + L("── (d) 고지대 NavMesh 클러스터 (셀 4 m · y > 2.0 m 삼각형) ──"); + Clusters(tri, triCount, 2.0f, 4f); + L(""); + L("── (d2) 고지대 NavMesh 클러스터 (셀 4 m · y > 1.5 m 삼각형) ──"); + Clusters(tri, triCount, 1.5f, 4f); + + // ── (e) 스포너 노드 ────────────────────────────────────────────── + L(""); + L("── (e) 스포너 노드 (MobControlMgr) ──"); + var mgrs = root.GetComponentsInChildren(true); + L(" 스포너 " + mgrs.Length + "기"); + for (int i = 0; i < mgrs.Length; i++) + { + var p = mgrs[i].transform.position; + UnityEngine.AI.NavMeshHit h; + bool on = UnityEngine.AI.NavMesh.SamplePosition(p, out h, 2f, UnityEngine.AI.NavMesh.AllAreas); + L(" #" + mgrs[i].SpawnerID + " " + mgrs[i].name + " pos=" + p.ToString("F2") + + " range=" + mgrs[i].MobGenRange + + " · NavMesh " + (on ? "y=" + h.position.y.ToString("F2") + " d=" + UnityEngine.Vector3.Distance(p, h.position).ToString("F2") : "밖")); + } + + // ── (f) 경로 연결성 ────────────────────────────────────────────── + L(""); + L("── (f) 경로 연결성 (시작점 ↔ 스포너 원점 · 아레나) ──"); + Connectivity(root, mgrs, startPos); + } + finally { UnityEngine.AI.NavMesh.RemoveNavMeshData(inst); } + + L(""); + L("===== 끝 ====="); + return Flush(kLog); + } + + // ───────────────────────────────────────────────────────────────────────── + /// 격자 스캔. 셀 중심 수직선을 1 m 간격으로 훑어 그 (x,z) 위에 있는 NavMesh 높이를 전부 모은다. + public static void GridScan(UnityEngine.Bounds b, float step, float snap, float yLo, float yHi) + { + int on = 0, off = 0, hiCells = 0; + var band = new System.Collections.Generic.SortedDictionary(); + float x0 = b.min.x, x1 = b.max.x, z0 = b.min.z, z1 = b.max.z; + for (float x = x0; x <= x1; x += step) + for (float z = z0; z <= z1; z += step) + { + bool found = false; float best = -1e9f; + for (float y = yLo; y <= yHi; y += 1f) + { + UnityEngine.AI.NavMeshHit h; + if (!UnityEngine.AI.NavMesh.SamplePosition(new UnityEngine.Vector3(x, y, z), out h, snap, UnityEngine.AI.NavMesh.AllAreas)) continue; + if (UnityEngine.Mathf.Abs(h.position.x - x) > snap || UnityEngine.Mathf.Abs(h.position.z - z) > snap) continue; + found = true; if (h.position.y > best) best = h.position.y; + } + if (found) + { + on++; int k = UnityEngine.Mathf.FloorToInt(best); int c; band.TryGetValue(k, out c); band[k] = c + 1; + if (best > 2.0f) hiCells++; + } + else off++; + } + L(" 격자 " + (on + off) + " 점(간격 " + step + " m) · NavMesh 위 " + on + " · 밖 " + off + " · **y > 2.0 셀 " + hiCells + "**"); + foreach (var kv in band) L(" y [" + kv.Key + ", " + (kv.Key + 1) + ") " + kv.Value + " 점"); + } + + // 고지대 삼각형을 셀로 묶어 8이웃 연결 성분을 만든다. + static void Clusters(UnityEngine.AI.NavMeshTriangulation tri, int triCount, float thr, float cell) + { + var cells = new System.Collections.Generic.Dictionary(); + var cellY = new System.Collections.Generic.Dictionary(); // (minY, maxY) + for (int t = 0; t < triCount; t++) + { + var a = tri.vertices[tri.indices[t * 3 + 0]]; + var b = tri.vertices[tri.indices[t * 3 + 1]]; + var c = tri.vertices[tri.indices[t * 3 + 2]]; + float cy = (a.y + b.y + c.y) / 3f; + if (cy <= thr) continue; + double ar = UnityEngine.Vector3.Cross(b - a, c - a).magnitude * 0.5; + float cx = (a.x + b.x + c.x) / 3f, cz = (a.z + b.z + c.z) / 3f; + int ix = UnityEngine.Mathf.FloorToInt(cx / cell), iz = UnityEngine.Mathf.FloorToInt(cz / cell); + long key = ((long)(ix + 100000) << 20) | (long)(iz + 100000); + double cur; cells.TryGetValue(key, out cur); cells[key] = cur + ar; + UnityEngine.Vector2 yy; + float lo = UnityEngine.Mathf.Min(a.y, UnityEngine.Mathf.Min(b.y, c.y)); + float hi = UnityEngine.Mathf.Max(a.y, UnityEngine.Mathf.Max(b.y, c.y)); + if (cellY.TryGetValue(key, out yy)) cellY[key] = new UnityEngine.Vector2(UnityEngine.Mathf.Min(yy.x, lo), UnityEngine.Mathf.Max(yy.y, hi)); + else cellY[key] = new UnityEngine.Vector2(lo, hi); + } + L(" 고지대 셀 " + cells.Count + "개 (셀 " + cell + " m)"); + + var seen = new System.Collections.Generic.HashSet(); + var stack = new System.Collections.Generic.Stack(); + var results = new System.Collections.Generic.List>(); + int cid = 0; + foreach (var kv in cells) + { + if (seen.Contains(kv.Key)) continue; + seen.Add(kv.Key); stack.Clear(); stack.Push(kv.Key); + double area = 0.0; int n = 0; + float minX = 1e9f, maxX = -1e9f, minZ = 1e9f, maxZ = -1e9f, minY = 1e9f, maxY = -1e9f; + while (stack.Count > 0) + { + long k = stack.Pop(); + int ix = (int)((k >> 20) & 0xFFFFF) - 100000; + int iz = (int)(k & 0xFFFFF) - 100000; + area += cells[k]; n++; + float cx0 = ix * cell, cz0 = iz * cell; + if (cx0 < minX) minX = cx0; if (cx0 + cell > maxX) maxX = cx0 + cell; + if (cz0 < minZ) minZ = cz0; if (cz0 + cell > maxZ) maxZ = cz0 + cell; + var yy = cellY[k]; + if (yy.x < minY) minY = yy.x; if (yy.y > maxY) maxY = yy.y; + for (int dx = -1; dx <= 1; dx++) + for (int dz = -1; dz <= 1; dz++) + { + if (dx == 0 && dz == 0) continue; + long nk = ((long)(ix + dx + 100000) << 20) | (long)(iz + dz + 100000); + if (cells.ContainsKey(nk) && !seen.Contains(nk)) { seen.Add(nk); stack.Push(nk); } + } + } + cid++; + results.Add(new System.Collections.Generic.KeyValuePair(area, + string.Format(" 셀 {0,4} · 면적 {1,9:F1} m^2 · X[{2,7:F1},{3,7:F1}] Z[{4,7:F1},{5,7:F1}] Y[{6,5:F2},{7,5:F2}] · 중심 ({8:F1}, {9:F1}) · 크기 {10:F1}×{11:F1}", + n, area, minX, maxX, minZ, maxZ, minY, maxY, (minX + maxX) * 0.5f, (minZ + maxZ) * 0.5f, maxX - minX, maxZ - minZ))); + } + results.Sort(delegate (System.Collections.Generic.KeyValuePair a, System.Collections.Generic.KeyValuePair b) + { return b.Key.CompareTo(a.Key); }); + for (int i = 0; i < results.Count && i < 60; i++) L(" C" + (i + 1) + results[i].Value); + if (results.Count > 60) L(" … 외 " + (results.Count - 60) + "개 클러스터"); + L(" 클러스터 총 " + cid + "개"); + } + + static void Connectivity(UnityEngine.GameObject root, MobControlMgr[] mgrs, UnityEngine.Vector3 startPos) + { + UnityEngine.AI.NavMeshHit sh; + if (!UnityEngine.AI.NavMesh.SamplePosition(startPos, out sh, 5f, UnityEngine.AI.NavMesh.AllAreas)) + { L(" [에러] 시작 지점이 NavMesh 밖"); return; } + L(" 시작 스냅 = " + sh.position.ToString("F2")); + int comp = 0, part = 0, none = 0; + for (int i = 0; i < mgrs.Length; i++) + { + var p = mgrs[i].transform.position; + UnityEngine.AI.NavMeshHit h; + if (!UnityEngine.AI.NavMesh.SamplePosition(p, out h, 5f, UnityEngine.AI.NavMesh.AllAreas)) { none++; L(" #" + mgrs[i].SpawnerID + " NavMesh 밖"); continue; } + var path = new UnityEngine.AI.NavMeshPath(); + UnityEngine.AI.NavMesh.CalculatePath(sh.position, h.position, UnityEngine.AI.NavMesh.AllAreas, path); + if (path.status == UnityEngine.AI.NavMeshPathStatus.PathComplete) comp++; + else { part++; L(" #" + mgrs[i].SpawnerID + " " + path.status + " " + p.ToString("F1")); } + } + L(" 스포너 " + mgrs.Length + " → PathComplete " + comp + " · Partial/Invalid " + part + " · NavMesh 밖 " + none); + + // 아레나 중심(813e = 139, 0.8, −20) 은 SO 에서 읽는다. + var arenaCfg = UnityEngine.Resources.Load("WL/WLBossArenaSettings"); + if (arenaCfg != null) + { + var ac = arenaCfg.arenaCenter; + UnityEngine.AI.NavMeshHit ah; + bool on = UnityEngine.AI.NavMesh.SamplePosition(ac, out ah, 5f, UnityEngine.AI.NavMesh.AllAreas); + L(" 아레나 중심(SO) = " + ac.ToString("F2") + " r=" + arenaCfg.arenaRadius + " · NavMesh " + (on ? ah.position.ToString("F2") : "밖")); + if (on) + { + var path = new UnityEngine.AI.NavMeshPath(); + UnityEngine.AI.NavMesh.CalculatePath(sh.position, ah.position, UnityEngine.AI.NavMesh.AllAreas, path); + L(" 시작 → 아레나 = " + path.status); + } + } + else L(" [미확인] WLBossArenaSettings 로드 실패"); + } + + static UnityEngine.Transform FindByName(UnityEngine.Transform t, string name) + { + if (t.name == name) return t; + for (int i = 0; i < t.childCount; i++) + { + var r = FindByName(t.GetChild(i), name); + if (r != null) return r; + } + return null; + } +} diff --git a/Assets/Res_Addr/Map/WL_Nature.prefab b/Assets/Res_Addr/Map/WL_Nature.prefab index ca7629f54..f6ce781a1 100644 --- a/Assets/Res_Addr/Map/WL_Nature.prefab +++ b/Assets/Res_Addr/Map/WL_Nature.prefab @@ -2654,6 +2654,7 @@ Transform: - {fileID: 5625234380910883685} - {fileID: 8168532711777316576} - {fileID: 8130050000000000002} + - {fileID: 8132000000000000002} m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!114 &5695711385560286233 @@ -69610,3 +69611,53 @@ MonoBehaviour: m_hasFontAssetChanged: 0 m_renderer: {fileID: 8130240000000000006} m_maskType: 0 +--- !u!1 &8132000000000000001 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 8132000000000000002} + - component: {fileID: 8132000000000000003} + m_Layer: 0 + m_Name: WL813t2_HillNotWalkable + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &8132000000000000002 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8132000000000000001} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 115, y: 24, z: 10} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 4502284306717827522} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &8132000000000000003 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8132000000000000001} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 35e95dc5ff2b64380880dd7ac5922847, type: 3} + m_Name: + m_EditorClassIdentifier: Unity.AI.Navigation::Unity.AI.Navigation.NavMeshModifierVolume + m_SerializedVersion: 0 + m_Size: {x: 260, y: 42, z: 260} + m_Center: {x: 0, y: 0, z: 0} + m_Area: 1 + m_AffectedAgents: + - -1 diff --git a/Assets/WL/Settings/WL_Nature_NavMesh.asset b/Assets/WL/Settings/WL_Nature_NavMesh.asset index e9cd3dff7..ea5fc2656 100644 Binary files a/Assets/WL/Settings/WL_Nature_NavMesh.asset and b/Assets/WL/Settings/WL_Nature_NavMesh.asset differ