Project_WL/AgentScripts/WL813t2_Probe.cs

319 lines
19 KiB
C#
Raw Permalink 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.

// 발주서 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<UnityEngine.GameObject>(kMapPrefab);
if (root == null) { L("[에러] 맵 프리팹 로드 실패: " + kMapPrefab); return Flush(kLog); }
// ── (a) NavMesh 출처 · 베이크 설정 ───────────────────────────────────
L("");
L("── (a) NavMesh 출처 · 베이크 설정 ──");
var surfaces = root.GetComponentsInChildren<Unity.AI.Navigation.NavMeshSurface>(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<Unity.AI.Navigation.NavMeshModifierVolume>(true).Length);
L(" NavMeshModifier 기존 개수 = " + root.GetComponentsInChildren<Unity.AI.Navigation.NavMeshModifier>(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<int, double>();
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<MobControlMgr>(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);
}
// ─────────────────────────────────────────────────────────────────────────
/// <summary>격자 스캔. 셀 중심 수직선을 1 m 간격으로 훑어 그 (x,z) 위에 있는 NavMesh 높이를 전부 모은다.</summary>
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<int, int>();
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<long, double>();
var cellY = new System.Collections.Generic.Dictionary<long, UnityEngine.Vector2>(); // (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<long>();
var stack = new System.Collections.Generic.Stack<long>();
var results = new System.Collections.Generic.List<System.Collections.Generic.KeyValuePair<double, string>>();
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<double, string>(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<double, string> a, System.Collections.Generic.KeyValuePair<double, string> 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.Combat.Boss.WLBossArenaSettings>("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;
}
}