Project_WL/AgentScripts/WL813t2_Bake.cs

280 lines
17 KiB
C#
Raw Permalink Normal View History

// 발주서 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<UnityEngine.GameObject>(kMapPrefab);
if (prefab == null) { L("[에러] 프리팹 로드 실패"); return Flush(logPath); }
// 현행(커밋본) 자산 상태
var oldData = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.AI.NavMeshData>(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<Unity.AI.Navigation.NavMeshSurface>(true);
if (sf == null) { L("[에러] NavMeshSurface 없음"); return Flush(logPath); }
var vols = go.GetComponentsInChildren<Unity.AI.Navigation.NavMeshModifierVolume>(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<UnityEngine.Object>(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<UnityEngine.AI.NavMeshData>(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<UnityEngine.GameObject>(kMapPrefab);
Report("검증", data, prefab);
return Flush(kOutDir + "/VERIFY.txt");
}
// ─────────────────────────────────────────────────────────────────────────
/// <summary>NavMeshData 를 잠시 올려 면적·y 대역·격자·연결성·스폰 표집을 잰다.</summary>
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<MobControlMgr>(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.Combat.Boss.WLBossArenaSettings>("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); }
}
/// <summary>원본 Get_RandomPos_onNavMesh_Reachable 규약(스냅 2 m · 50회 · PathComplete)으로 1000점 표집.</summary>
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<int, int>();
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;
}
}