260 lines
16 KiB
C#
260 lines
16 KiB
C#
|
|
// 발주서 WL-813t2 — 지형 가드 · 스폰 위치 검증 프로브 (에디트 모드 · Play 0 · 로그인 0)
|
|||
|
|
//
|
|||
|
|
// 실행 (CLAUDE.md §1: using 금지 · 네임스페이스 풀어 쓰기)
|
|||
|
|
// SO 생성 : unity command --project-path E:\NerdNavis\WL_wt\systems run_script --file AgentScripts/WL813t2_Guard.cs --entry WL813t2_Guard.MakeSettings --timeout 300
|
|||
|
|
// 프로브 : … --entry WL813t2_Guard.Probe --timeout 900
|
|||
|
|
//
|
|||
|
|
// 프로브가 재는 것 — 전부 읽기 전용(에셋 수정 0):
|
|||
|
|
// ① SO 실값(에셋에서 읽는다 · 코드 상수 대조 0)
|
|||
|
|
// ② 언덕 격자 1000점 — 재베이크 뒤 walkable 0 인지(SamplePosition)
|
|||
|
|
// ③ 되돌릴 지점 찾기 — D-48 실좌표(아레나 139/−20 위 y 3.66 · 5.48)에서 FindGround → y ≤ 2.96
|
|||
|
|
// ④ 스폰 보정 경로 — 언덕 위 가짜 스폰점 200개에 §1-4 규약(재표집 3회 → 스포너 노드) 적용
|
|||
|
|
// ⑤ C8 — enabled=0 · terrainGuardEnabled=0 · spawnGuardEnabled=0 · RuntimeDisabled 진리표
|
|||
|
|
// ⑥ GC — 정상 상태 틱 1000회의 GC 증가분
|
|||
|
|
|
|||
|
|
public static class WL813t2_Guard
|
|||
|
|
{
|
|||
|
|
const string kSoPath = "Assets/WL/Combat/Settings/Resources/WL/WLTerrainGuardSettings.asset";
|
|||
|
|
const string kNavAsset = "Assets/WL/Settings/WL_Nature_NavMesh.asset";
|
|||
|
|
const string kMapPrefab= "Assets/Res_Addr/Map/WL_Nature.prefab";
|
|||
|
|
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 MakeSettings()
|
|||
|
|
{
|
|||
|
|
s = new System.Text.StringBuilder();
|
|||
|
|
var existing = UnityEditor.AssetDatabase.LoadAssetAtPath<WL.Combat.Nav.WLTerrainGuardSettings>(kSoPath);
|
|||
|
|
if (existing != null) { L("이미 있음: " + kSoPath); return Flush(kOutDir + "/SO.txt"); }
|
|||
|
|
|
|||
|
|
var so = UnityEngine.ScriptableObject.CreateInstance<WL.Combat.Nav.WLTerrainGuardSettings>();
|
|||
|
|
// 값은 클래스 초기값(= 발주서 §1-3 · §1-4 값)을 그대로 굳힌다 — 여기서 새 수치를 만들지 않는다.
|
|||
|
|
UnityEditor.AssetDatabase.CreateAsset(so, kSoPath);
|
|||
|
|
UnityEditor.AssetDatabase.SaveAssets();
|
|||
|
|
UnityEditor.AssetDatabase.ImportAsset(kSoPath, UnityEditor.ImportAssetOptions.ForceUpdate);
|
|||
|
|
L("생성: " + kSoPath);
|
|||
|
|
return Flush(kOutDir + "/SO.txt");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|||
|
|
public static string Probe()
|
|||
|
|
{
|
|||
|
|
s = new System.Text.StringBuilder();
|
|||
|
|
L("===== WL-813t2 가드 프로브 (" + System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + ") =====");
|
|||
|
|
|
|||
|
|
WL.Combat.Nav.WLTerrainGuardSettings.ClearCache();
|
|||
|
|
var cfg = UnityEngine.Resources.Load<WL.Combat.Nav.WLTerrainGuardSettings>(
|
|||
|
|
WL.Combat.Nav.WLTerrainGuardSettings.ResourcesPath);
|
|||
|
|
if (cfg == null) { L("[에러] SO 로드 실패 (" + kSoPath + ")"); return Flush(kOutDir + "/PROBE.txt"); }
|
|||
|
|
|
|||
|
|
// ① SO 실값
|
|||
|
|
L("");
|
|||
|
|
L("── ① SO 실값 (에셋에서 읽음) ──");
|
|||
|
|
L(" enabled=" + B(cfg.enabled) + " verboseLog=" + B(cfg.verboseLog)
|
|||
|
|
+ " check=" + cfg.checkSeconds + "s rescan=" + cfg.rescanSeconds + "s");
|
|||
|
|
L(" [지형] terrainGuardEnabled=" + B(cfg.terrainGuardEnabled) + " maxWalkableY=" + cfg.maxWalkableY
|
|||
|
|
+ " offNavMeshTolerance=" + cfg.offNavMeshTolerance + " dwell=" + cfg.dwellSeconds + "s cool=" + cfg.warpCooldownSeconds + "s"
|
|||
|
|
+ " near=" + cfg.warpNearRadius + " far=" + cfg.warpFarRadius);
|
|||
|
|
L(" [지형] guardPlayer=" + B(cfg.guardPlayer) + " guardMobs=" + B(cfg.guardMobs) + " guardBoss=" + B(cfg.guardBoss)
|
|||
|
|
+ " warpSnapsCameraLead=" + B(cfg.warpSnapsCameraLead));
|
|||
|
|
L(" [범위] heightGuardMapIds=[" + Join(cfg.heightGuardMapIds) + "] allMaps=" + B(cfg.heightGuardAllMaps)
|
|||
|
|
+ " whenUnknown=" + B(cfg.heightGuardWhenMapUnknown) + " (WL_Nature = BattleMapConfig n_MapID -1)");
|
|||
|
|
L(" [스폰] spawnGuardEnabled=" + B(cfg.spawnGuardEnabled) + " check=" + cfg.spawnCheckSeconds + "s"
|
|||
|
|
+ " sampleRadius=" + cfg.spawnSampleRadius + " retries=" + cfg.spawnRetries
|
|||
|
|
+ " fallbackToNode=" + B(cfg.spawnFallbackToSpawnerNode));
|
|||
|
|
|
|||
|
|
var navData = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.AI.NavMeshData>(kNavAsset);
|
|||
|
|
var root = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.GameObject>(kMapPrefab);
|
|||
|
|
if (navData == null || root == null) { L("[에러] NavMesh/프리팹 로드 실패"); return Flush(kOutDir + "/PROBE.txt"); }
|
|||
|
|
var inst = UnityEngine.AI.NavMesh.AddNavMeshData(navData);
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
// ② 언덕 격자 1000점
|
|||
|
|
L("");
|
|||
|
|
L("── ② 언덕 격자 — 재베이크 뒤 walkable 0 인지 ──");
|
|||
|
|
// 813t2 실측 고지대 클러스터 C1(X 96~160 · Z −100~−16 · 최고 y 20.46)이 D-48 현장(아레나 139/−20 옆)이다.
|
|||
|
|
UnityEngine.Random.InitState(8132);
|
|||
|
|
int n = 1000, walkable = 0, aboveMax = 0;
|
|||
|
|
float hiY = -1e9f;
|
|||
|
|
for (int i = 0; i < n; i++)
|
|||
|
|
{
|
|||
|
|
float x = UnityEngine.Random.Range(96f, 160f);
|
|||
|
|
float z = UnityEngine.Random.Range(-100f, -16f);
|
|||
|
|
float y = UnityEngine.Random.Range(3.0f, 20.0f); // 언덕 높이대
|
|||
|
|
UnityEngine.AI.NavMeshHit h;
|
|||
|
|
if (!UnityEngine.AI.NavMesh.SamplePosition(new UnityEngine.Vector3(x, y, z), out h, 1.0f, UnityEngine.AI.NavMesh.AllAreas)) continue;
|
|||
|
|
walkable++;
|
|||
|
|
if (h.position.y > hiY) hiY = h.position.y;
|
|||
|
|
if (h.position.y > cfg.maxWalkableY) aboveMax++;
|
|||
|
|
}
|
|||
|
|
L(" 클러스터 C1(X96~160 · Z−100~−16 · y 3~20) 1000점 · 반경 1 m 스냅 성공 " + walkable
|
|||
|
|
+ " · maxWalkableY(" + cfg.maxWalkableY + ") 초과 " + aboveMax
|
|||
|
|
+ (walkable > 0 ? " · 최고 스냅 y " + hiY.ToString("F2") : ""));
|
|||
|
|
|
|||
|
|
// 맵 전역 y>3 walkable 이 정말 0 인지 한 번 더(삼각형 기준은 Bake 프로브가 이미 0.0 을 실측)
|
|||
|
|
var tri = UnityEngine.AI.NavMesh.CalculateTriangulation();
|
|||
|
|
float top = -1e9f;
|
|||
|
|
for (int i = 0; i < tri.vertices.Length; i++) if (tri.vertices[i].y > top) top = tri.vertices[i].y;
|
|||
|
|
L(" NavMesh 정점 최고 y = " + top.ToString("F2") + " (maxWalkableY " + cfg.maxWalkableY + " 아래여야 한다)");
|
|||
|
|
|
|||
|
|
// ③ 되돌릴 지점 찾기 — D-48 실좌표
|
|||
|
|
L("");
|
|||
|
|
L("── ③ FindGround — D-48 실좌표에서 되돌릴 지점 ──");
|
|||
|
|
UnityEngine.Vector3[] cases = {
|
|||
|
|
new UnityEngine.Vector3(139f, 3.66f, -20f), // D-48 R2 t=295.46
|
|||
|
|
new UnityEngine.Vector3(139f, 5.48f, -20f), // D-48 R2 t=309.33
|
|||
|
|
new UnityEngine.Vector3(139f, 5.27f, -20f), // D-48 결과창 방치 74 s
|
|||
|
|
new UnityEngine.Vector3(128f, 8.00f, -58f), // 클러스터 C1 중심 상공
|
|||
|
|
new UnityEngine.Vector3(147f, 6.00f, 30f), // 존2 상공
|
|||
|
|
};
|
|||
|
|
for (int i = 0; i < cases.Length; i++)
|
|||
|
|
{
|
|||
|
|
UnityEngine.Vector3 got;
|
|||
|
|
bool ok = WL.Combat.Nav.TerrainGuard.FindGround(cfg, cases[i], out got);
|
|||
|
|
L(" " + cases[i].ToString("F2") + " → " + (ok ? got.ToString("F2") + " · Δy " + (cases[i].y - got.y).ToString("F2")
|
|||
|
|
+ " · 수평 " + UnityEngine.Vector2.Distance(new UnityEngine.Vector2(cases[i].x, cases[i].z), new UnityEngine.Vector2(got.x, got.z)).ToString("F2") + " m"
|
|||
|
|
+ (got.y <= cfg.maxWalkableY ? " · PASS" : " · **FAIL(언덕 위)**")
|
|||
|
|
: "실패(반경 " + cfg.warpFarRadius + " m 안에 NavMesh 없음)"));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ④ 스폰 보정 경로 — 실제 SpawnPositionGuard.ResolveSpawnPoint 를 그대로 부른다(로직 복제 0)
|
|||
|
|
L("");
|
|||
|
|
L("── ④ 스폰 보정 경로 — 언덕 위 가짜 스폰점 200개 (SpawnPositionGuard.ResolveSpawnPoint 실호출) ──");
|
|||
|
|
var mgrs = root.GetComponentsInChildren<MobControlMgr>(true);
|
|||
|
|
for (int i = 0; i < mgrs.Length; i++) // 에디트 모드에는 씬 인스턴스가 없다 → 노드 캐시를 직접 심는다
|
|||
|
|
WL.Combat.Nav.SpawnPositionGuard.SeedSpawnerNodeForProbe(mgrs[i].SpawnerID, mgrs[i].transform.position);
|
|||
|
|
UnityEngine.Random.InitState(8133);
|
|||
|
|
int okStep1 = 0, okRetry = 0, okNode = 0, fail = 0, resultAbove = 0;
|
|||
|
|
for (int i = 0; i < 200; i++)
|
|||
|
|
{
|
|||
|
|
var mgr = mgrs[i % mgrs.Length];
|
|||
|
|
var origin = mgr.transform.position;
|
|||
|
|
// 스포너 반경 안의 랜덤 XZ 를 언덕 높이(y 6)로 들어올린 가짜 스폰점
|
|||
|
|
var off = UnityEngine.Random.insideUnitCircle * mgr.MobGenRange;
|
|||
|
|
var p = new UnityEngine.Vector3(origin.x + off.x, 6f, origin.z + off.y);
|
|||
|
|
|
|||
|
|
UnityEngine.Vector3 res;
|
|||
|
|
int how = WL.Combat.Nav.SpawnPositionGuard.ResolveSpawnPoint(cfg, p, mgr.SpawnerID, out res);
|
|||
|
|
if (how == WL.Combat.Nav.SpawnPositionGuard.ResultValid) okStep1++;
|
|||
|
|
else if (how == WL.Combat.Nav.SpawnPositionGuard.ResultNode) okNode++;
|
|||
|
|
else if (how == WL.Combat.Nav.SpawnPositionGuard.ResultFailed) { fail++; continue; }
|
|||
|
|
else okRetry++;
|
|||
|
|
if (res.y > cfg.maxWalkableY) resultAbove++;
|
|||
|
|
}
|
|||
|
|
L(" 200점 · 원위치 유효 " + okStep1 + " · 재표집 " + okRetry + " · 스포너 노드 폴백 " + okNode
|
|||
|
|
+ " · 보정 실패 " + fail + " · **보정 결과가 언덕 위 " + resultAbove + "**");
|
|||
|
|
|
|||
|
|
// 스포너 노드 자체가 전부 walkable 이고 언덕 아래인지
|
|||
|
|
int nodeOk = 0, nodeHigh = 0;
|
|||
|
|
for (int i = 0; i < mgrs.Length; i++)
|
|||
|
|
{
|
|||
|
|
UnityEngine.AI.NavMeshHit h;
|
|||
|
|
if (!UnityEngine.AI.NavMesh.SamplePosition(mgrs[i].transform.position, out h, cfg.spawnSampleRadius, UnityEngine.AI.NavMesh.AllAreas)) continue;
|
|||
|
|
nodeOk++;
|
|||
|
|
if (h.position.y > cfg.maxWalkableY) { nodeHigh++; L(" [경고] 스포너 " + mgrs[i].SpawnerID + " 노드가 언덕 위 y=" + h.position.y.ToString("F2")); }
|
|||
|
|
}
|
|||
|
|
L(" 스포너 노드 " + mgrs.Length + "기 · walkable " + nodeOk + " · 언덕 위 " + nodeHigh + " (0 이어야 폴백이 안전하다)");
|
|||
|
|
}
|
|||
|
|
finally { UnityEngine.AI.NavMesh.RemoveNavMeshData(inst); }
|
|||
|
|
|
|||
|
|
// ⑤ C8 진리표 — 임시 SO 로만 실험한다(실에셋 무수정)
|
|||
|
|
L("");
|
|||
|
|
L("── ⑤ C8 진리표 (임시 SO · 실에셋 무수정) ──");
|
|||
|
|
var real = cfg;
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
L(" 실에셋 → TerrainGuard.Active=" + B(WL.Combat.Nav.TerrainGuard.Active)
|
|||
|
|
+ " · SpawnPositionGuard.Active=" + B(WL.Combat.Nav.SpawnPositionGuard.Active));
|
|||
|
|
var tmp = UnityEngine.ScriptableObject.CreateInstance<WL.Combat.Nav.WLTerrainGuardSettings>();
|
|||
|
|
WL.Combat.Nav.WLTerrainGuardSettings.SetInstanceForProbe(tmp);
|
|||
|
|
tmp.enabled = false;
|
|||
|
|
L(" enabled=0 → " + B(WL.Combat.Nav.TerrainGuard.Active) + " / " + B(WL.Combat.Nav.SpawnPositionGuard.Active) + " (기대 false/false)");
|
|||
|
|
tmp.enabled = true; tmp.terrainGuardEnabled = false;
|
|||
|
|
L(" terrainGuard=0 → " + B(WL.Combat.Nav.TerrainGuard.Active) + " / " + B(WL.Combat.Nav.SpawnPositionGuard.Active) + " (기대 false/true)");
|
|||
|
|
tmp.terrainGuardEnabled = true; tmp.spawnGuardEnabled = false;
|
|||
|
|
L(" spawnGuard=0 → " + B(WL.Combat.Nav.TerrainGuard.Active) + " / " + B(WL.Combat.Nav.SpawnPositionGuard.Active) + " (기대 true/false)");
|
|||
|
|
tmp.spawnGuardEnabled = true; WL.Combat.Nav.WLTerrainGuardSettings.RuntimeDisabled = true;
|
|||
|
|
L(" RuntimeDisabled → " + B(WL.Combat.Nav.TerrainGuard.Active) + " / " + B(WL.Combat.Nav.SpawnPositionGuard.Active) + " (기대 false/false)");
|
|||
|
|
WL.Combat.Nav.WLTerrainGuardSettings.RuntimeDisabled = false;
|
|||
|
|
WL.Combat.Nav.WLTerrainGuardSettings.SetInstanceForProbe(null);
|
|||
|
|
L(" 에셋 없음 → " + B(WL.Combat.Nav.TerrainGuard.Active) + " / " + B(WL.Combat.Nav.SpawnPositionGuard.Active) + " (기대 false/false)");
|
|||
|
|
UnityEngine.Object.DestroyImmediate(tmp);
|
|||
|
|
|
|||
|
|
// ⑥ GC — 정상 상태 틱 + 실제 판정 경로(FindGround · ResolveSpawnPoint) 각 1000회
|
|||
|
|
L("");
|
|||
|
|
L("── ⑥ GC — 틱 1000회 + 판정 경로 1000회 ──");
|
|||
|
|
var g = UnityEngine.ScriptableObject.CreateInstance<WL.Combat.Nav.WLTerrainGuardSettings>();
|
|||
|
|
g.rescanSeconds = 100000f; g.spawnCheckSeconds = 100000f;
|
|||
|
|
WL.Combat.Nav.WLTerrainGuardSettings.SetInstanceForProbe(g);
|
|||
|
|
WL.Combat.Nav.TerrainGuard.ResetDiagnostics();
|
|||
|
|
WL.Combat.Nav.SpawnPositionGuard.ResetDiagnostics();
|
|||
|
|
WL.Combat.Nav.TerrainGuard.ForceTick(); // 1회는 재수집(할당) — 이후가 정상 상태
|
|||
|
|
WL.Combat.Nav.SpawnPositionGuard.ForceTick();
|
|||
|
|
|
|||
|
|
var navInst2 = UnityEngine.AI.NavMesh.AddNavMeshData(navData);
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
var from = new UnityEngine.Vector3(139f, 5.48f, -20f);
|
|||
|
|
UnityEngine.Vector3 t1, t2;
|
|||
|
|
WL.Combat.Nav.TerrainGuard.FindGround(g, from, out t1); // JIT 워밍
|
|||
|
|
WL.Combat.Nav.SpawnPositionGuard.ResolveSpawnPoint(g, from, 813001, out t2);
|
|||
|
|
System.GC.Collect(); System.GC.WaitForPendingFinalizers(); System.GC.Collect();
|
|||
|
|
long before = System.GC.GetTotalMemory(false);
|
|||
|
|
for (int i = 0; i < 1000; i++)
|
|||
|
|
{
|
|||
|
|
WL.Combat.Nav.TerrainGuard.ProbeTick();
|
|||
|
|
WL.Combat.Nav.SpawnPositionGuard.ProbeTick();
|
|||
|
|
WL.Combat.Nav.TerrainGuard.FindGround(g, from, out t1);
|
|||
|
|
WL.Combat.Nav.SpawnPositionGuard.ResolveSpawnPoint(g, from, 813001, out t2);
|
|||
|
|
}
|
|||
|
|
long after = System.GC.GetTotalMemory(false);
|
|||
|
|
L(" 틱 1000 + FindGround 1000 + ResolveSpawnPoint 1000 · GC 총량 " + before + " → " + after
|
|||
|
|
+ " (Δ " + (after - before) + " B)"
|
|||
|
|
+ " · 재수집 " + WL.Combat.Nav.TerrainGuard.Scans + " · Spawn 스캔 " + WL.Combat.Nav.SpawnPositionGuard.Scans);
|
|||
|
|
}
|
|||
|
|
finally { UnityEngine.AI.NavMesh.RemoveNavMeshData(navInst2); }
|
|||
|
|
UnityEngine.Object.DestroyImmediate(g);
|
|||
|
|
}
|
|||
|
|
finally
|
|||
|
|
{
|
|||
|
|
WL.Combat.Nav.WLTerrainGuardSettings.RuntimeDisabled = false;
|
|||
|
|
WL.Combat.Nav.WLTerrainGuardSettings.ClearCache();
|
|||
|
|
WL.Combat.Nav.TerrainGuard.ResetDiagnostics();
|
|||
|
|
WL.Combat.Nav.SpawnPositionGuard.ResetDiagnostics();
|
|||
|
|
L(" 정리: 캐시 초기화 · RuntimeDisabled=false · 실에셋 재로드 = " + (UnityEngine.Resources.Load<WL.Combat.Nav.WLTerrainGuardSettings>(WL.Combat.Nav.WLTerrainGuardSettings.ResourcesPath) != null));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
L("");
|
|||
|
|
L("===== 끝 =====");
|
|||
|
|
return Flush(kOutDir + "/PROBE.txt");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static string B(bool b) { return b ? "true" : "false"; }
|
|||
|
|
static string Join(int[] a)
|
|||
|
|
{
|
|||
|
|
if (a == null) return "";
|
|||
|
|
var sb = new System.Text.StringBuilder();
|
|||
|
|
for (int i = 0; i < a.Length; i++) { if (i > 0) sb.Append(", "); sb.Append(a[i]); }
|
|||
|
|
return sb.ToString();
|
|||
|
|
}
|
|||
|
|
}
|