Project_WL/AgentScripts/WL_SpawnerProbe.cs

253 lines
13 KiB
C#

// 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<MobControlMgr>(true);
L("[C01] MobControlMgr " + mgrs.Length + "개");
var listComp = root.GetComponentInChildren<MobControlMgrList>(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<MobControlMgrList>(true);
L("[WL] MobControlMgrList = " + (lst == null ? "없음" : Path(lst.transform, root.transform)));
var mgrs = root.GetComponentsInChildren<MobControlMgr>(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<MapData>();
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.Transform>(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<UnityEngine.AI.NavMeshData>(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<long, int>();
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<UnityEngine.Component>();
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");
}
}