[WL-813t2] 언덕 이동 불가: NavMesh 재구성 · 지형 가드 · 스폰 위치 검증 (#813)
- TerrainGuard(신규 · 폴링 · 원본 훅 0): PC/몹/보스가 NavMesh 밖 또는 y > maxWalkableY 에 dwell(1 s) 넘게 있으면 원본 Actor.Set_Warp 으로 가장 가까운 walkable 지점 복귀(쿨 3 s · 카메라 리드 스냅) - SpawnPositionGuard(같은 파일): 새로 나타난 몹의 스폰 지점 검증 → 재표집 3회 → 스포너 노드 폴백 - WLTerrainGuardSettings(SO · 값 22개 전부 에셋 · C8 롤백 enabled=0) - 프로브 PASS: 언덕 격자 1000점 walkable 0 · FindGround 5/5 · 스폰 200점 언덕 위 0 · C8 6행 · GC 0 B Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
99af7c8eea
commit
b2e86190f9
|
|
@ -0,0 +1,259 @@
|
||||||
|
// 발주서 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,228 @@
|
||||||
|
===== WL-813t2 프로브 산출물 (커밋본) =====
|
||||||
|
|
||||||
|
########## 1. 실측 · 재베이크 후 (WL813t2_Probe.Measure) ##########
|
||||||
|
===== WL-813t2 실측 (2026-09-10 00:53:47) =====
|
||||||
|
|
||||||
|
── (a) NavMesh 출처 · 베이크 설정 ──
|
||||||
|
NavMeshSurface 개수 = 1
|
||||||
|
[0] WL_Nature · agentTypeID=0 · collectObjects=Volume · size=(260, 80, 260) · center=(115, 5, 10) · layerMask=-1 · useGeometry=PhysicsColliders · defaultArea=0 · minRegionArea=15 · overrideVoxel=False voxel=0.16667 · overrideTile=False tile=256
|
||||||
|
데이터 = Assets/WL/Settings/WL_Nature_NavMesh.asset · 원점=(0.0, 0.0, 0.0)
|
||||||
|
Agent 0 (Humanoid) radius=0.5 height=2 slope=45 climb=0.75 minRegionArea=2 voxel=0.16667
|
||||||
|
NavMeshModifierVolume 기존 개수 = 1
|
||||||
|
NavMeshModifier 기존 개수 = 0
|
||||||
|
Portal_Start = (115.00, 0.00, 9.98)
|
||||||
|
NavMesh 삼각형 4969 · 정점 11151
|
||||||
|
|
||||||
|
── (b) NavMesh 면적 y 대역 분포 (삼각형 3D 면적 합 · m^2) ──
|
||||||
|
총 면적 = 61215.0 m^2 · y 범위 -6.21 ~ 2.85
|
||||||
|
y [-7, -6) 면적 12.2 (0.02 %) · 누적 0.02 %
|
||||||
|
y [-6, -5) 면적 162.1 (0.26 %) · 누적 0.28 %
|
||||||
|
y [-5, -4) 면적 643.4 (1.05 %) · 누적 1.34 %
|
||||||
|
y [-4, -3) 면적 463.3 (0.76 %) · 누적 2.09 %
|
||||||
|
y [-3, -2) 면적 107.3 (0.18 %) · 누적 2.27 %
|
||||||
|
y [-2, -1) 면적 1824.6 (2.98 %) · 누적 5.25 %
|
||||||
|
y [-1, 0) 면적 14014.8 (22.89 %) · 누적 28.14 %
|
||||||
|
y [0, 1) 면적 38504.5 (62.90 %) · 누적 91.04 %
|
||||||
|
y [1, 2) 면적 4920.3 (8.04 %) · 누적 99.08 %
|
||||||
|
y [2, 3) 면적 562.5 (0.92 %) · 누적 100.00 %
|
||||||
|
y > 1.5 m 면적 = 1926.4 m^2 (3.15 %)
|
||||||
|
y > 2.5 m 면적 = 104.0 m^2 (0.17 %)
|
||||||
|
y > 3.5 m 면적 = 0.0 m^2 (0.00 %)
|
||||||
|
y > 4.5 m 면적 = 0.0 m^2 (0.00 %)
|
||||||
|
y > 5.5 m 면적 = 0.0 m^2 (0.00 %)
|
||||||
|
y > 6.5 m 면적 = 0.0 m^2 (0.00 %)
|
||||||
|
|
||||||
|
── (c) 샘플 격자 2 m (셀 중심 수직선 1 m 간격 · SamplePosition 반경 1 m) ──
|
||||||
|
격자 17161 점(간격 2 m) · NavMesh 위 15710 · 밖 1451 · **y > 2.0 셀 336**
|
||||||
|
y [-5, -4) 12 점
|
||||||
|
y [-4, -3) 30 점
|
||||||
|
y [-3, -2) 76 점
|
||||||
|
y [-2, -1) 1233 점
|
||||||
|
y [-1, 0) 1485 점
|
||||||
|
y [0, 1) 11174 점
|
||||||
|
y [1, 2) 1364 점
|
||||||
|
y [2, 3) 336 점
|
||||||
|
|
||||||
|
── (d) 고지대 NavMesh 클러스터 (셀 4 m · y > 2.0 m 삼각형) ──
|
||||||
|
고지대 셀 64개 (셀 4 m)
|
||||||
|
C1 셀 1 · 면적 166.9 m^2 · X[ 136.0, 140.0] Z[ 132.0, 136.0] Y[ 1.21, 2.79] · 중심 (138.0, 134.0) · 크기 4.0×4.0
|
||||||
|
C2 셀 1 · 면적 111.3 m^2 · X[ 148.0, 152.0] Z[ 128.0, 132.0] Y[ 1.21, 2.79] · 중심 (150.0, 130.0) · 크기 4.0×4.0
|
||||||
|
C3 셀 1 · 면적 72.9 m^2 · X[ 144.0, 148.0] Z[ 136.0, 140.0] Y[ 2.38, 2.79] · 중심 (146.0, 138.0) · 크기 4.0×4.0
|
||||||
|
C4 셀 5 · 면적 52.1 m^2 · X[ 216.0, 224.0] Z[ -92.0, -76.0] Y[ 0.63, 2.88] · 중심 (220.0, -84.0) · 크기 8.0×16.0
|
||||||
|
C5 셀 8 · 면적 27.1 m^2 · X[ 144.0, 156.0] Z[ -48.0, -24.0] Y[ 0.71, 2.79] · 중심 (150.0, -36.0) · 크기 12.0×24.0
|
||||||
|
C6 셀 2 · 면적 25.1 m^2 · X[ 144.0, 152.0] Z[ 20.0, 28.0] Y[ 1.46, 2.54] · 중심 (148.0, 24.0) · 크기 8.0×8.0
|
||||||
|
C7 셀 8 · 면적 24.5 m^2 · X[ 100.0, 124.0] Z[ -80.0, -64.0] Y[ 1.38, 2.88] · 중심 (112.0, -72.0) · 크기 24.0×16.0
|
||||||
|
C8 셀 5 · 면적 18.4 m^2 · X[ 44.0, 60.0] Z[ -24.0, -12.0] Y[ 0.88, 2.79] · 중심 (52.0, -18.0) · 크기 16.0×12.0
|
||||||
|
C9 셀 2 · 면적 12.6 m^2 · X[ 240.0, 248.0] Z[ 32.0, 40.0] Y[ 1.04, 2.88] · 중심 (244.0, 36.0) · 크기 8.0×8.0
|
||||||
|
C10 셀 2 · 면적 9.9 m^2 · X[ 136.0, 140.0] Z[ 20.0, 28.0] Y[ 1.46, 2.54] · 중심 (138.0, 24.0) · 크기 4.0×8.0
|
||||||
|
C11 셀 5 · 면적 8.9 m^2 · X[ 236.0, 248.0] Z[ -68.0, -60.0] Y[ 1.96, 2.88] · 중심 (242.0, -64.0) · 크기 12.0×8.0
|
||||||
|
C12 셀 1 · 면적 5.2 m^2 · X[ 224.0, 228.0] Z[ -104.0, -100.0] Y[ 2.63, 2.79] · 중심 (226.0, -102.0) · 크기 4.0×4.0
|
||||||
|
C13 셀 1 · 면적 4.6 m^2 · X[ 200.0, 204.0] Z[ -8.0, -4.0] Y[ 1.29, 2.79] · 중심 (202.0, -6.0) · 크기 4.0×4.0
|
||||||
|
C14 셀 1 · 면적 3.8 m^2 · X[ 108.0, 112.0] Z[ -44.0, -40.0] Y[ 1.88, 2.79] · 중심 (110.0, -42.0) · 크기 4.0×4.0
|
||||||
|
C15 셀 3 · 면적 3.8 m^2 · X[ 156.0, 164.0] Z[ 128.0, 136.0] Y[ 2.79, 2.88] · 중심 (160.0, 132.0) · 크기 8.0×8.0
|
||||||
|
C16 셀 2 · 면적 3.7 m^2 · X[ 72.0, 76.0] Z[ 4.0, 12.0] Y[ 1.04, 2.71] · 중심 (74.0, 8.0) · 크기 4.0×8.0
|
||||||
|
C17 셀 2 · 면적 2.4 m^2 · X[ 120.0, 124.0] Z[ -92.0, -84.0] Y[ 2.54, 2.79] · 중심 (122.0, -88.0) · 크기 4.0×8.0
|
||||||
|
C18 셀 1 · 면적 1.4 m^2 · X[ 232.0, 236.0] Z[ -120.0, -116.0] Y[ 2.71, 2.88] · 중심 (234.0, -118.0) · 크기 4.0×4.0
|
||||||
|
C19 셀 2 · 면적 1.3 m^2 · X[ 100.0, 104.0] Z[ 36.0, 44.0] Y[ 1.96, 2.13] · 중심 (102.0, 40.0) · 크기 4.0×8.0
|
||||||
|
C20 셀 1 · 면적 1.2 m^2 · X[ 140.0, 144.0] Z[ -88.0, -84.0] Y[ 1.54, 2.79] · 중심 (142.0, -86.0) · 크기 4.0×4.0
|
||||||
|
C21 셀 1 · 면적 0.9 m^2 · X[ 236.0, 240.0] Z[ 24.0, 28.0] Y[ 1.96, 2.13] · 중심 (238.0, 26.0) · 크기 4.0×4.0
|
||||||
|
C22 셀 1 · 면적 0.7 m^2 · X[ 180.0, 184.0] Z[ 124.0, 128.0] Y[ 2.71, 2.88] · 중심 (182.0, 126.0) · 크기 4.0×4.0
|
||||||
|
C23 셀 1 · 면적 0.7 m^2 · X[ 236.0, 240.0] Z[ 16.0, 20.0] Y[ 2.29, 2.63] · 중심 (238.0, 18.0) · 크기 4.0×4.0
|
||||||
|
C24 셀 1 · 면적 0.6 m^2 · X[ 132.0, 136.0] Z[ -100.0, -96.0] Y[ 2.21, 2.63] · 중심 (134.0, -98.0) · 크기 4.0×4.0
|
||||||
|
C25 셀 1 · 면적 0.6 m^2 · X[ 60.0, 64.0] Z[ -36.0, -32.0] Y[ 2.04, 2.71] · 중심 (62.0, -34.0) · 크기 4.0×4.0
|
||||||
|
C26 셀 1 · 면적 0.6 m^2 · X[ 164.0, 168.0] Z[ 120.0, 124.0] Y[ 2.71, 2.88] · 중심 (166.0, 122.0) · 크기 4.0×4.0
|
||||||
|
C27 셀 1 · 면적 0.4 m^2 · X[ 56.0, 60.0] Z[ 12.0, 16.0] Y[ 1.63, 2.63] · 중심 (58.0, 14.0) · 크기 4.0×4.0
|
||||||
|
C28 셀 1 · 면적 0.4 m^2 · X[ 244.0, 248.0] Z[ 16.0, 20.0] Y[ 2.71, 2.96] · 중심 (246.0, 18.0) · 크기 4.0×4.0
|
||||||
|
C29 셀 1 · 면적 0.3 m^2 · X[ 132.0, 136.0] Z[ -68.0, -64.0] Y[ 2.04, 2.63] · 중심 (134.0, -66.0) · 크기 4.0×4.0
|
||||||
|
C30 셀 1 · 면적 0.3 m^2 · X[ 132.0, 136.0] Z[ -20.0, -16.0] Y[ 1.46, 2.46] · 중심 (134.0, -18.0) · 크기 4.0×4.0
|
||||||
|
클러스터 총 30개
|
||||||
|
|
||||||
|
── (d2) 고지대 NavMesh 클러스터 (셀 4 m · y > 1.5 m 삼각형) ──
|
||||||
|
고지대 셀 109개 (셀 4 m)
|
||||||
|
C1 셀 6 · 면적 228.5 m^2 · X[ 132.0, 144.0] Z[ 16.0, 32.0] Y[ 1.38, 2.54] · 중심 (138.0, 24.0) · 크기 12.0×16.0
|
||||||
|
C2 셀 1 · 면적 166.9 m^2 · X[ 136.0, 140.0] Z[ 132.0, 136.0] Y[ 1.21, 2.79] · 중심 (138.0, 134.0) · 크기 4.0×4.0
|
||||||
|
C3 셀 4 · 면적 155.6 m^2 · X[ 152.0, 164.0] Z[ 112.0, 120.0] Y[ 0.04, 2.79] · 중심 (158.0, 116.0) · 크기 12.0×8.0
|
||||||
|
C4 셀 1 · 면적 152.1 m^2 · X[ 220.0, 224.0] Z[ -112.0, -108.0] Y[ 0.04, 2.71] · 중심 (222.0, -110.0) · 크기 4.0×4.0
|
||||||
|
C5 셀 12 · 면적 142.1 m^2 · X[ 144.0, 160.0] Z[ 20.0, 40.0] Y[ 0.46, 2.54] · 중심 (152.0, 30.0) · 크기 16.0×20.0
|
||||||
|
C6 셀 3 · 면적 137.7 m^2 · X[ 220.0, 232.0] Z[ -64.0, -56.0] Y[-0.12, 2.88] · 중심 (226.0, -60.0) · 크기 12.0×8.0
|
||||||
|
C7 셀 1 · 면적 111.3 m^2 · X[ 148.0, 152.0] Z[ 128.0, 132.0] Y[ 1.21, 2.79] · 중심 (150.0, 130.0) · 크기 4.0×4.0
|
||||||
|
C8 셀 4 · 면적 96.4 m^2 · X[ 180.0, 192.0] Z[ 120.0, 132.0] Y[ 0.04, 2.88] · 중심 (186.0, 126.0) · 크기 12.0×12.0
|
||||||
|
C9 셀 11 · 면적 95.3 m^2 · X[ 144.0, 164.0] Z[ -48.0, -24.0] Y[ 0.71, 2.79] · 중심 (154.0, -36.0) · 크기 20.0×24.0
|
||||||
|
C10 셀 6 · 면적 89.8 m^2 · X[ 216.0, 224.0] Z[ -92.0, -76.0] Y[ 0.63, 2.88] · 중심 (220.0, -84.0) · 크기 8.0×16.0
|
||||||
|
C11 셀 1 · 면적 86.7 m^2 · X[ 224.0, 228.0] Z[ -120.0, -116.0] Y[ 0.04, 2.71] · 중심 (226.0, -118.0) · 크기 4.0×4.0
|
||||||
|
C12 셀 1 · 면적 84.9 m^2 · X[ 240.0, 244.0] Z[ -56.0, -52.0] Y[ 0.04, 2.79] · 중심 (242.0, -54.0) · 크기 4.0×4.0
|
||||||
|
C13 셀 1 · 면적 72.9 m^2 · X[ 144.0, 148.0] Z[ 136.0, 140.0] Y[ 2.38, 2.79] · 중심 (146.0, 138.0) · 크기 4.0×4.0
|
||||||
|
C14 셀 1 · 면적 56.1 m^2 · X[ 188.0, 192.0] Z[ 136.0, 140.0] Y[ 0.04, 2.79] · 중심 (190.0, 138.0) · 크기 4.0×4.0
|
||||||
|
C15 셀 1 · 면적 44.8 m^2 · X[ 176.0, 180.0] Z[ 116.0, 120.0] Y[ 0.13, 2.88] · 중심 (178.0, 118.0) · 크기 4.0×4.0
|
||||||
|
C16 셀 9 · 면적 31.6 m^2 · X[ 96.0, 124.0] Z[ -80.0, -64.0] Y[ 0.71, 2.88] · 중심 (110.0, -72.0) · 크기 28.0×16.0
|
||||||
|
C17 셀 2 · 면적 31.2 m^2 · X[ 152.0, 156.0] Z[ -76.0, -68.0] Y[ 1.21, 1.88] · 중심 (154.0, -72.0) · 크기 4.0×8.0
|
||||||
|
C18 셀 7 · 면적 29.5 m^2 · X[ 236.0, 248.0] Z[ 12.0, 40.0] Y[ 0.54, 2.88] · 중심 (242.0, 26.0) · 크기 12.0×28.0
|
||||||
|
C19 셀 6 · 면적 20.1 m^2 · X[ 44.0, 60.0] Z[ -24.0, -12.0] Y[ 0.79, 2.79] · 중심 (52.0, -18.0) · 크기 16.0×12.0
|
||||||
|
C20 셀 1 · 면적 18.5 m^2 · X[ 144.0, 148.0] Z[ -72.0, -68.0] Y[ 1.46, 1.88] · 중심 (146.0, -70.0) · 크기 4.0×4.0
|
||||||
|
C21 셀 1 · 면적 13.9 m^2 · X[ 244.0, 248.0] Z[ 8.0, 12.0] Y[-0.87, 2.96] · 중심 (246.0, 10.0) · 크기 4.0×4.0
|
||||||
|
C22 셀 5 · 면적 10.8 m^2 · X[ 236.0, 248.0] Z[ -68.0, -60.0] Y[ 1.63, 2.88] · 중심 (242.0, -64.0) · 크기 12.0×8.0
|
||||||
|
C23 셀 2 · 면적 9.1 m^2 · X[ 200.0, 204.0] Z[ -8.0, 0.0] Y[ 1.13, 2.79] · 중심 (202.0, -4.0) · 크기 4.0×8.0
|
||||||
|
C24 셀 3 · 면적 7.0 m^2 · X[ 68.0, 76.0] Z[ 4.0, 12.0] Y[-0.04, 2.71] · 중심 (72.0, 8.0) · 크기 8.0×8.0
|
||||||
|
C25 셀 2 · 면적 6.7 m^2 · X[ 100.0, 104.0] Z[ 36.0, 44.0] Y[ 1.29, 2.13] · 중심 (102.0, 40.0) · 크기 4.0×8.0
|
||||||
|
C26 셀 1 · 면적 5.2 m^2 · X[ 224.0, 228.0] Z[ -104.0, -100.0] Y[ 2.63, 2.79] · 중심 (226.0, -102.0) · 크기 4.0×4.0
|
||||||
|
C27 셀 1 · 면적 3.8 m^2 · X[ 108.0, 112.0] Z[ -44.0, -40.0] Y[ 1.88, 2.79] · 중심 (110.0, -42.0) · 크기 4.0×4.0
|
||||||
|
C28 셀 3 · 면적 3.8 m^2 · X[ 156.0, 164.0] Z[ 128.0, 136.0] Y[ 2.79, 2.88] · 중심 (160.0, 132.0) · 크기 8.0×8.0
|
||||||
|
C29 셀 2 · 면적 2.4 m^2 · X[ 120.0, 124.0] Z[ -92.0, -84.0] Y[ 2.54, 2.79] · 중심 (122.0, -88.0) · 크기 4.0×8.0
|
||||||
|
C30 셀 1 · 면적 2.3 m^2 · X[ 132.0, 136.0] Z[ -100.0, -96.0] Y[ 0.96, 2.63] · 중심 (134.0, -98.0) · 크기 4.0×4.0
|
||||||
|
C31 셀 1 · 면적 2.1 m^2 · X[ 132.0, 136.0] Z[ -68.0, -64.0] Y[ 1.29, 2.63] · 중심 (134.0, -66.0) · 크기 4.0×4.0
|
||||||
|
C32 셀 1 · 면적 1.7 m^2 · X[ 60.0, 64.0] Z[ -36.0, -32.0] Y[ 1.21, 2.71] · 중심 (62.0, -34.0) · 크기 4.0×4.0
|
||||||
|
C33 셀 1 · 면적 1.4 m^2 · X[ 232.0, 236.0] Z[ -120.0, -116.0] Y[ 2.71, 2.88] · 중심 (234.0, -118.0) · 크기 4.0×4.0
|
||||||
|
C34 셀 2 · 면적 1.3 m^2 · X[ 140.0, 144.0] Z[ -92.0, -84.0] Y[ 1.29, 2.79] · 중심 (142.0, -88.0) · 크기 4.0×8.0
|
||||||
|
C35 셀 1 · 면적 1.0 m^2 · X[ 132.0, 136.0] Z[ -20.0, -16.0] Y[ 1.46, 2.46] · 중심 (134.0, -18.0) · 크기 4.0×4.0
|
||||||
|
C36 셀 1 · 면적 0.9 m^2 · X[ 56.0, 60.0] Z[ 12.0, 16.0] Y[ 1.21, 2.63] · 중심 (58.0, 14.0) · 크기 4.0×4.0
|
||||||
|
C37 셀 1 · 면적 0.6 m^2 · X[ 164.0, 168.0] Z[ 120.0, 124.0] Y[ 2.71, 2.88] · 중심 (166.0, 122.0) · 크기 4.0×4.0
|
||||||
|
C38 셀 1 · 면적 0.4 m^2 · X[ 244.0, 248.0] Z[ 16.0, 20.0] Y[ 2.71, 2.96] · 중심 (246.0, 18.0) · 크기 4.0×4.0
|
||||||
|
클러스터 총 38개
|
||||||
|
|
||||||
|
── (e) 스포너 노드 (MobControlMgr) ──
|
||||||
|
스포너 23기
|
||||||
|
#813001 1 pos=(115.00, 0.39, -22.00) range=10 · NavMesh y=0.34 d=0.05
|
||||||
|
#813002 2 pos=(147.00, 2.59, 30.00) range=10 · NavMesh y=2.54 d=0.05
|
||||||
|
#813003 3 pos=(71.00, -0.29, 18.00) range=10 · NavMesh y=-0.34 d=0.05
|
||||||
|
#813004 4 pos=(159.00, 0.88, -22.00) range=10 · NavMesh y=0.83 d=0.05
|
||||||
|
#813005 5 pos=(103.00, 0.22, 74.00) range=10 · NavMesh y=0.17 d=0.05
|
||||||
|
#6 6 pos=(55.00, 0.22, -38.00) range=10 · NavMesh y=0.17 d=0.05
|
||||||
|
#7 7 pos=(191.00, 0.34, 42.00) range=10 · NavMesh y=0.29 d=0.05
|
||||||
|
#8 8 pos=(199.00, 0.66, -2.00) range=10 · NavMesh y=0.61 d=0.05
|
||||||
|
#9 9 pos=(47.00, 0.07, 62.00) range=10 · NavMesh y=0.02 d=0.05
|
||||||
|
#10 10 pos=(155.00, 1.34, -70.00) range=10 · NavMesh y=1.29 d=0.05
|
||||||
|
#11 11 pos=(19.00, 0.20, 10.00) range=10 · NavMesh y=0.15 d=0.05
|
||||||
|
#12 12 pos=(195.00, 0.26, -50.00) range=10 · NavMesh y=0.21 d=0.05
|
||||||
|
#13 13 pos=(143.00, 0.38, 110.00) range=10 · NavMesh y=0.29 d=0.09
|
||||||
|
#14 14 pos=(91.00, 0.20, -90.00) range=10 · NavMesh y=0.15 d=0.05
|
||||||
|
#15 15 pos=(67.00, 0.09, 110.00) range=10 · NavMesh y=0.04 d=0.05
|
||||||
|
#16 16 pos=(195.00, 0.12, 90.00) range=10 · NavMesh y=0.07 d=0.05
|
||||||
|
#17 17 pos=(19.00, -0.62, -90.00) range=10 · NavMesh y=-0.67 d=0.05
|
||||||
|
#18 18 pos=(15.00, 0.12, 110.00) range=10 · NavMesh y=0.07 d=0.05
|
||||||
|
#19 19 pos=(215.00, 1.00, -90.00) range=10 · NavMesh y=0.91 d=0.09
|
||||||
|
#813021 e1 pos=(115.00, 0.39, -22.00) range=6 · NavMesh y=0.34 d=0.05
|
||||||
|
#813022 e2 pos=(147.00, 2.59, 30.00) range=6 · NavMesh y=2.54 d=0.05
|
||||||
|
#813023 e3 pos=(71.00, -0.29, 18.00) range=6 · NavMesh y=-0.34 d=0.05
|
||||||
|
#813024 e4 pos=(159.00, 0.88, -22.00) range=6 · NavMesh y=0.83 d=0.05
|
||||||
|
|
||||||
|
── (f) 경로 연결성 (시작점 ↔ 스포너 원점 · 아레나) ──
|
||||||
|
시작 스냅 = (115.00, 0.11, 9.98)
|
||||||
|
스포너 23 → PathComplete 23 · Partial/Invalid 0 · NavMesh 밖 0
|
||||||
|
아레나 중심(SO) = (139.00, 0.80, -20.00) r=20 · NavMesh (139.00, 0.81, -20.00)
|
||||||
|
시작 → 아레나 = PathComplete
|
||||||
|
|
||||||
|
===== 끝 =====
|
||||||
|
|
||||||
|
########## 2. 베이크 전 기준선 (WL813t2_Bake.Verify · 커밋본 NavMesh) ##########
|
||||||
|
===== WL-813t2 검증 (2026-09-10 00:43:36) =====
|
||||||
|
자산 Assets/WL/Settings/WL_Nature_NavMesh.asset · 667268 bytes
|
||||||
|
[검증] 삼각형 5504 · 정점 12364 · 총 면적 67536.0 m^2 · y -6.21 ~ 22.79
|
||||||
|
[검증] y>1.5 9602.4 · y>2.0 8487.1 · y>2.5 7465.1 · **y>3.0 6044.6** · y>3.5 5590.4 · y>5.0 3428.5 m^2
|
||||||
|
[검증] 시작→스포너 23 : PathComplete 23 · Partial/Invalid 0 · 밖 0
|
||||||
|
[검증] 아레나 (139.0, 0.8, -20.0) → (139.00, 0.81, -20.00) · 시작→아레나 PathComplete
|
||||||
|
[검증] 아레나 원주 12점 → 중심 PathComplete 8/12
|
||||||
|
[검증] 앵커 Portal_Start (115.00, 0.00, 9.98) → (115.00, 0.11, 9.98)
|
||||||
|
[검증] 앵커 Reincarnation_Start (115.00, 0.00, 9.98) → (115.00, 0.11, 9.98)
|
||||||
|
[검증] 스폰 표집 1000점 · **y>3.0 8** · NavMesh 밖 0 · 원점 폴백 0
|
||||||
|
y 분포: [-2]=89 [-1]=53 [0]=668 [1]=93 [2]=89 [3]=1 [4]=7
|
||||||
|
|
||||||
|
########## 3. 베이크 적용 (WL813t2_Bake.Apply) ##########
|
||||||
|
===== WL-813t2 베이크 APPLY(자산 갱신) · 2026-09-10 00:43:46 =====
|
||||||
|
현행 자산 = Assets/WL/Settings/WL_Nature_NavMesh.asset · 667268 bytes · 로드 OK
|
||||||
|
[현행(커밋본)] 삼각형 5504 · 정점 12364 · 총 면적 67536.0 m^2 · y -6.21 ~ 22.79
|
||||||
|
[현행(커밋본)] y>1.5 9602.4 · y>2.0 8487.1 · y>2.5 7465.1 · **y>3.0 6044.6** · y>3.5 5590.4 · y>5.0 3428.5 m^2
|
||||||
|
인스턴스 root transform pos=(0.000, 0.000, 0.000) rot=(0.0, 0.0, 0.0) scale=(1.000, 1.000, 1.000)
|
||||||
|
NavMeshModifierVolume 1개
|
||||||
|
[0] WL_Nature/WL813t2_HillNotWalkable · area=1(Not Walkable) · center(world)=(115.0, 24.0, 10.0) size=(260.0, 42.0, 260.0) · Y[3.00, 45.00] · active=True enabled=True · affectsAgent0=True layer=0
|
||||||
|
베이크 0.2 s · 결과 OK
|
||||||
|
[재베이크] 삼각형 4969 · 정점 11151 · 총 면적 61215.0 m^2 · y -6.21 ~ 2.96
|
||||||
|
[재베이크] y>1.5 1926.4 · y>2.0 562.5 · y>2.5 104.0 · **y>3.0 0.0** · y>3.5 0.0 · y>5.0 0.0 m^2
|
||||||
|
[재베이크] 시작→스포너 23 : PathComplete 23 · Partial/Invalid 0 · 밖 0
|
||||||
|
[재베이크] 아레나 (139.0, 0.8, -20.0) → (139.00, 0.81, -20.00) · 시작→아레나 PathComplete
|
||||||
|
[재베이크] 아레나 원주 12점 → 중심 PathComplete 8/12
|
||||||
|
[재베이크] 앵커 Portal_Start (115.00, 0.00, 9.98) → (115.00, 0.11, 9.98)
|
||||||
|
[재베이크] 앵커 Reincarnation_Start (115.00, 0.00, 9.98) → (115.00, 0.11, 9.98)
|
||||||
|
[재베이크] 스폰 표집 1000점 · **y>3.0 0** · NavMesh 밖 0 · 원점 폴백 0
|
||||||
|
y 분포: [-2]=88 [-1]=60 [0]=678 [1]=94 [2]=80
|
||||||
|
자산 저장 완료 · 667268 → 593864 bytes (-73404)
|
||||||
|
GUID = ef300913c80dbda47b476b9261fdd786 (기대 ef300913c80dbda47b476b9261fdd786)
|
||||||
|
씬 정리 완료(저장 0) · scene=
|
||||||
|
|
||||||
|
########## 4. 가드 프로브 (WL813t2_Guard.Probe) ##########
|
||||||
|
===== WL-813t2 가드 프로브 (2026-09-10 00:53:32) =====
|
||||||
|
|
||||||
|
── ① SO 실값 (에셋에서 읽음) ──
|
||||||
|
enabled=true verboseLog=false check=0.2s rescan=3s
|
||||||
|
[지형] terrainGuardEnabled=true maxWalkableY=3.5 offNavMeshTolerance=1 dwell=1s cool=3s near=4 far=30
|
||||||
|
[지형] guardPlayer=true guardMobs=true guardBoss=true warpSnapsCameraLead=true
|
||||||
|
[범위] heightGuardMapIds=[-1] allMaps=false whenUnknown=true (WL_Nature = BattleMapConfig n_MapID -1)
|
||||||
|
[스폰] spawnGuardEnabled=true check=0.5s sampleRadius=2 retries=3 fallbackToNode=true
|
||||||
|
|
||||||
|
── ② 언덕 격자 — 재베이크 뒤 walkable 0 인지 ──
|
||||||
|
클러스터 C1(X96~160 · Z−100~−16 · y 3~20) 1000점 · 반경 1 m 스냅 성공 0 · maxWalkableY(3.5) 초과 0
|
||||||
|
NavMesh 정점 최고 y = 2.96 (maxWalkableY 3.5 아래여야 한다)
|
||||||
|
|
||||||
|
── ③ FindGround — D-48 실좌표에서 되돌릴 지점 ──
|
||||||
|
(139.00, 3.66, -20.00) → (139.00, 0.81, -20.00) · Δy 2.85 · 수평 0.00 m · PASS
|
||||||
|
(139.00, 5.48, -20.00) → (139.00, 0.81, -20.00) · Δy 4.67 · 수평 0.00 m · PASS
|
||||||
|
(139.00, 5.27, -20.00) → (139.00, 0.81, -20.00) · Δy 4.46 · 수평 0.00 m · PASS
|
||||||
|
(128.00, 8.00, -58.00) → (128.00, 1.28, -58.00) · Δy 6.72 · 수평 0.00 m · PASS
|
||||||
|
(147.00, 6.00, 30.00) → (147.00, 2.54, 30.00) · Δy 3.46 · 수평 0.00 m · PASS
|
||||||
|
|
||||||
|
── ④ 스폰 보정 경로 — 언덕 위 가짜 스폰점 200개 (SpawnPositionGuard.ResolveSpawnPoint 실호출) ──
|
||||||
|
200점 · 원위치 유효 0 · 재표집 200 · 스포너 노드 폴백 0 · 보정 실패 0 · **보정 결과가 언덕 위 0**
|
||||||
|
스포너 노드 23기 · walkable 23 · 언덕 위 0 (0 이어야 폴백이 안전하다)
|
||||||
|
|
||||||
|
── ⑤ C8 진리표 (임시 SO · 실에셋 무수정) ──
|
||||||
|
실에셋 → TerrainGuard.Active=true · SpawnPositionGuard.Active=true
|
||||||
|
enabled=0 → false / false (기대 false/false)
|
||||||
|
terrainGuard=0 → false / true (기대 false/true)
|
||||||
|
spawnGuard=0 → true / false (기대 true/false)
|
||||||
|
RuntimeDisabled → false / false (기대 false/false)
|
||||||
|
에셋 없음 → false / false (기대 false/false)
|
||||||
|
|
||||||
|
── ⑥ GC — 틱 1000회 + 판정 경로 1000회 ──
|
||||||
|
틱 1000 + FindGround 1000 + ResolveSpawnPoint 1000 · GC 총량 1181982720 → 1181982720 (Δ 0 B) · 재수집 1 · Spawn 스캔 1
|
||||||
|
정리: 캐시 초기화 · RuntimeDisabled=false · 실에셋 재로드 = True
|
||||||
|
|
||||||
|
===== 끝 =====
|
||||||
|
|
@ -0,0 +1,442 @@
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// TerrainGuard.cs — 언덕(이동 불가 지형) 가드 + 스폰 위치 검증
|
||||||
|
//
|
||||||
|
// PD 지시 #813 추가 (2026-09-10) · 발주서 WL-813t2 §1-3 · §1-4
|
||||||
|
// 「언덕 지형은 플레이어나 몬스터가 갈 수 없도록 · 언덕을 뚫고 가는 상황 및 카메라 연출 꼬임 ·
|
||||||
|
// 이동 불가 지역에는 몬스터가 생성되어서도 안 됨」
|
||||||
|
//
|
||||||
|
// ■ 관측된 결함 (QA W2-12 D-48 실측)
|
||||||
|
// 보스전 중 PC 가 아레나 밖 지형으로 올라갔다 — `y 0.66 → 3.66(t=295.46) → 5.48(t=309.33)`.
|
||||||
|
// 그 뒤 보스 HP 가 `797.2` 15 s · `730.0` 14 s 동안 완전 무변 → 「시간 종료 · 미처치」.
|
||||||
|
// 813w4 가이드는 `ArenaExits=1` 로 이탈을 **판정은 했지만** `ArenaWarps=0` 으로 **되돌리지 못했다**
|
||||||
|
// (되돌릴 지점이 NavMesh 밖이면 워프를 포기하도록 813w3 이 설계돼 있다 — 지형 뚫기 방지).
|
||||||
|
//
|
||||||
|
// ■ 이 파일이 맡는 것 = 「올라간 뒤」의 복구. 「올라가는 것」 자체는 같은 커밋의 NavMesh 재베이크가 막는다.
|
||||||
|
// ① NavMesh 재베이크(WL_Nature.prefab 의 NavMeshModifierVolume · Not Walkable · Y[3, 45])
|
||||||
|
// → 언덕 위 walkable 면적 6044.6 → **0.0 m²**(실측). 걸어서는 못 올라간다.
|
||||||
|
// ② 그래도 넉백·점프(보스 스킬2 `Actor.Co_JumpToPosition` 은 `m_NavMeshAgent.enabled=false` 로 끄고
|
||||||
|
// 트랜스폼을 직접 옮긴다)로 올라갈 수 있다 → **이 파일**이 유예 뒤 가장 가까운 NavMesh 위로 되돌린다.
|
||||||
|
//
|
||||||
|
// ■ 원본 훅 0 — 폴링만 쓴다(MobNavRecovery(813x) · Survival.PollRunBoundary 선례).
|
||||||
|
// 🔴 `CombatEvents.Spawned` 를 쓰지 않는 이유(실측): `CombatEvents.RaiseSpawned`(CombatEvents.cs:302)
|
||||||
|
// 첫 줄이 `if (!Enabled …) return;` 이라 **코어 스위치가 꺼지면 이벤트가 0** 이다.
|
||||||
|
// 스폰 보정은 코어 A/B 중에도 살아 있어야 해서(같은 이유로 813x 도 코어와 무관하게 만들었다) 폴링으로 간다.
|
||||||
|
//
|
||||||
|
// ■ 되돌리는 방법 = 원본 `Actor.Set_Warp`(Actor.cs:2111) — 813i `DeathFlow.On_Regen → Set_Warp`,
|
||||||
|
// 813w3 `RunGuide.WarpToArena`, 813x `MobNavRecovery.Repair` 와 같은 API. 이펙트·잔상 0.
|
||||||
|
// PC 는 `Get_NavSnapRadius()=0`(Actor.cs:2133) 이라 Set_Warp 이 스스로 스냅하지 않는다
|
||||||
|
// → **반드시 먼저 `NavMesh.SamplePosition` 으로 스냅한 지점**을 넘긴다(지형 뚫기 방지).
|
||||||
|
//
|
||||||
|
// ■ 값(C45) 전부 WLTerrainGuardSettings. 게임 수치 상수 0.
|
||||||
|
// 🔴 어셈블리 주의: Assets/WL/Combat/ 에 .asmdef 를 만들지 말 것(Actor/MobActor 가 Assembly-CSharp 에 있다).
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.AI;
|
||||||
|
|
||||||
|
namespace WL.Combat.Nav
|
||||||
|
{
|
||||||
|
/// <summary>PC·몹·보스가 NavMesh 밖 또는 언덕 위에 머무르면 가장 가까운 NavMesh 위로 되돌린다.</summary>
|
||||||
|
public static class TerrainGuard
|
||||||
|
{
|
||||||
|
static WLTerrainGuardSettings Cfg { get { return WLTerrainGuardSettings.Instance; } }
|
||||||
|
|
||||||
|
/// <summary>지형 가드 축이 살아 있는가.</summary>
|
||||||
|
public static bool Active
|
||||||
|
{
|
||||||
|
get { var c = Cfg; return c != null && c.enabled && c.terrainGuardEnabled && !WLTerrainGuardSettings.RuntimeDisabled; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ───────────────────────────────────────── 진단 카운터 (프로브가 읽는다)
|
||||||
|
public static int Warped, WarpFailed, Scans, Checks, AnomaliesSeen, SkippedCooldown;
|
||||||
|
public static string LastLog = "";
|
||||||
|
public static Vector3 LastWarpFrom, LastWarpTo;
|
||||||
|
public static string LastReason = "";
|
||||||
|
|
||||||
|
// ───────────────────────────────────────── 내부 상태(GC 0 — 틱에서 새로 만들지 않는다)
|
||||||
|
static MobActor[] s_mobs = new MobActor[0];
|
||||||
|
static readonly Dictionary<int, float> s_badSince = new Dictionary<int, float>();
|
||||||
|
static readonly Dictionary<int, float> s_lastWarp = new Dictionary<int, float>();
|
||||||
|
static float s_nextRescan, s_nextCheck;
|
||||||
|
static bool s_faulted;
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────
|
||||||
|
internal static void Tick()
|
||||||
|
{
|
||||||
|
if (s_faulted || !Active) return;
|
||||||
|
var cfg = Cfg;
|
||||||
|
float now = Time.unscaledTime;
|
||||||
|
|
||||||
|
if (now >= s_nextRescan)
|
||||||
|
{
|
||||||
|
s_nextRescan = now + Mathf.Max(0.1f, cfg.rescanSeconds);
|
||||||
|
Rescan();
|
||||||
|
}
|
||||||
|
if (now < s_nextCheck) return;
|
||||||
|
s_nextCheck = now + Mathf.Max(0.02f, cfg.checkSeconds);
|
||||||
|
|
||||||
|
// 🔴 Debug.LogException 을 쓰지 않는다 — ErrorLogHookManager 가 그것을 플레이어 모달로 띄운다(813x).
|
||||||
|
try { CheckAll(cfg, now); }
|
||||||
|
catch (System.Exception ex) { s_faulted = true; Log("중단(예외) — " + ex.Message); }
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Rescan()
|
||||||
|
{
|
||||||
|
// FindObjectsInactive.Exclude — 비활성 몹은 풀에 들어간 사체다(Actor.cs:633 에서 에이전트를 끈다).
|
||||||
|
s_mobs = Object.FindObjectsByType<MobActor>(FindObjectsInactive.Exclude, FindObjectsSortMode.None);
|
||||||
|
Scans++;
|
||||||
|
if (s_badSince.Count > 128) s_badSince.Clear(); // 풀 재사용으로 쌓인 죽은 키 정리
|
||||||
|
if (s_lastWarp.Count > 128) s_lastWarp.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void CheckAll(WLTerrainGuardSettings cfg, float now)
|
||||||
|
{
|
||||||
|
bool heightRule = HeightRuleApplies(cfg);
|
||||||
|
|
||||||
|
if (cfg.guardPlayer)
|
||||||
|
{
|
||||||
|
var pc = MyValue.MyPC;
|
||||||
|
if (!DSUtil.CheckNull(pc)) CheckOne(cfg, pc, now, heightRule, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
var boss = WL.Combat.Boss.BossArena.Boss;
|
||||||
|
if (cfg.guardBoss && !DSUtil.CheckNull(boss)) CheckOne(cfg, boss, now, heightRule, false);
|
||||||
|
|
||||||
|
if (!cfg.guardMobs) return;
|
||||||
|
var mobs = s_mobs;
|
||||||
|
for (int i = 0; i < mobs.Length; i++)
|
||||||
|
{
|
||||||
|
var m = mobs[i];
|
||||||
|
if (DSUtil.CheckNull(m) || ReferenceEquals(m, boss)) continue;
|
||||||
|
CheckOne(cfg, m, now, heightRule, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>높이 규칙을 이 맵에 적용하는가. 다른 맵은 고지대가 정상 플레이 영역일 수 있다.</summary>
|
||||||
|
public static bool HeightRuleApplies(WLTerrainGuardSettings cfg)
|
||||||
|
{
|
||||||
|
if (cfg.heightGuardAllMaps) return true;
|
||||||
|
var md = MyValue.MyChoiceMapData;
|
||||||
|
if (md == null) return cfg.heightGuardWhenMapUnknown;
|
||||||
|
var ids = cfg.heightGuardMapIds;
|
||||||
|
if (ids == null) return false;
|
||||||
|
for (int i = 0; i < ids.Length; i++) if (ids[i] == md.n_MapID) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void CheckOne(WLTerrainGuardSettings cfg, Actor actor, float now, bool heightRule, bool isPlayer)
|
||||||
|
{
|
||||||
|
int key = actor.GetInstanceID();
|
||||||
|
if (actor.IsDead()) { s_badSince.Remove(key); return; }
|
||||||
|
Checks++;
|
||||||
|
|
||||||
|
Vector3 pos = actor.Get_position();
|
||||||
|
NavMeshHit near;
|
||||||
|
bool onMesh = NavMesh.SamplePosition(pos, out near, Mathf.Max(0.05f, cfg.offNavMeshTolerance), NavMesh.AllAreas);
|
||||||
|
bool tooHigh = heightRule && pos.y > cfg.maxWalkableY;
|
||||||
|
|
||||||
|
if (onMesh && !tooHigh) { s_badSince.Remove(key); return; }
|
||||||
|
|
||||||
|
float since;
|
||||||
|
if (!s_badSince.TryGetValue(key, out since))
|
||||||
|
{
|
||||||
|
s_badSince[key] = now; // 점프·넉백일 수 있다 — 유예를 준다
|
||||||
|
AnomaliesSeen++;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (now - since < Mathf.Max(0f, cfg.dwellSeconds)) return;
|
||||||
|
|
||||||
|
float last;
|
||||||
|
if (s_lastWarp.TryGetValue(key, out last) && now - last < Mathf.Max(0f, cfg.warpCooldownSeconds))
|
||||||
|
{ SkippedCooldown++; return; }
|
||||||
|
|
||||||
|
WarpBack(cfg, actor, key, pos, now, isPlayer, tooHigh ? (onMesh ? "언덕(y " + pos.y.ToString("F2") + ")" : "언덕+NavMesh 밖") : "NavMesh 밖");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void WarpBack(WLTerrainGuardSettings cfg, Actor actor, int key, Vector3 from, float now, bool isPlayer, string reason)
|
||||||
|
{
|
||||||
|
s_badSince.Remove(key); // 성공이든 실패든 창을 다시 연다(실패 시 다음 유예 뒤 재시도)
|
||||||
|
|
||||||
|
Vector3 target;
|
||||||
|
if (!FindGround(cfg, from, out target))
|
||||||
|
{
|
||||||
|
WarpFailed++;
|
||||||
|
Log("복귀 실패 — 반경 " + cfg.warpFarRadius + " m 안에 NavMesh 없음 " + from.ToString("F2") + " (" + reason + ")");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var agent = actor.Get_Agent();
|
||||||
|
if (agent != null && agent.enabled && agent.isOnNavMesh) agent.ResetPath(); // 남은 경로 제거
|
||||||
|
|
||||||
|
actor.Set_Warp(target); // 원본 API · 이펙트 0 · PC 는 이미 스냅된 지점을 받는다
|
||||||
|
if (isPlayer && cfg.warpSnapsCameraLead) SnapCameraLead();
|
||||||
|
|
||||||
|
s_lastWarp[key] = now;
|
||||||
|
Warped++;
|
||||||
|
LastWarpFrom = from; LastWarpTo = target; LastReason = reason;
|
||||||
|
Log("복귀 #" + Warped + " " + actor.name + " " + from.ToString("F2") + " → " + target.ToString("F2") + " (" + reason + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>가장 가까운 walkable 지점. 1차 반경 → 2차 반경. 언덕 위로 되돌리지 않는다.</summary>
|
||||||
|
public static bool FindGround(WLTerrainGuardSettings cfg, Vector3 from, out Vector3 target)
|
||||||
|
{
|
||||||
|
target = from;
|
||||||
|
NavMeshHit hit;
|
||||||
|
if (NavMesh.SamplePosition(from, out hit, Mathf.Max(0.1f, cfg.warpNearRadius), NavMesh.AllAreas)
|
||||||
|
|| NavMesh.SamplePosition(from, out hit, Mathf.Max(0.1f, cfg.warpFarRadius), NavMesh.AllAreas))
|
||||||
|
{
|
||||||
|
target = hit.position;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>되돌린 직후 카메라 이동 리드를 0 으로 스냅한다(811 CameraMoveLead.Snap · RealCamera 무수정).</summary>
|
||||||
|
static void SnapCameraLead()
|
||||||
|
{
|
||||||
|
var cam = MyValue.m_RealCamera;
|
||||||
|
if (DSUtil.CheckNull(cam)) return;
|
||||||
|
var lead = cam.Get_MoveLead();
|
||||||
|
if (lead != null) lead.Snap();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Log(string msg)
|
||||||
|
{
|
||||||
|
LastLog = msg;
|
||||||
|
var c = Cfg;
|
||||||
|
if (c != null && c.verboseLog) Debug.Log("[TerrainGuard] " + msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ───────────────────────────────────────── 진단 · 초기화
|
||||||
|
/// <summary>프로브용: 카운터·유예 창 초기화.</summary>
|
||||||
|
public static void ResetDiagnostics()
|
||||||
|
{
|
||||||
|
Warped = WarpFailed = Scans = Checks = AnomaliesSeen = SkippedCooldown = 0;
|
||||||
|
LastLog = ""; LastReason = ""; LastWarpFrom = LastWarpTo = Vector3.zero;
|
||||||
|
s_badSince.Clear(); s_lastWarp.Clear(); s_mobs = new MobActor[0];
|
||||||
|
s_nextRescan = s_nextCheck = 0f; s_faulted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>프로브용: 러너 없이 1회 검사(에디트 모드 · 강제 틱).</summary>
|
||||||
|
public static void ForceTick() { s_nextRescan = s_nextCheck = 0f; Tick(); }
|
||||||
|
|
||||||
|
/// <summary>프로브용: 러너가 부르는 것과 똑같은 틱(타이머 초기화 0 — GC 측정용).</summary>
|
||||||
|
public static void ProbeTick() { Tick(); }
|
||||||
|
|
||||||
|
/// <summary>프로브용: 임의 액터 1명을 지금 판정한다(유예·쿨 무시하고 즉시 복귀까지).</summary>
|
||||||
|
public static bool ForceCheck(Actor actor)
|
||||||
|
{
|
||||||
|
var cfg = Cfg;
|
||||||
|
if (cfg == null || actor == null) return false;
|
||||||
|
int key = actor.GetInstanceID();
|
||||||
|
Vector3 pos = actor.Get_position();
|
||||||
|
NavMeshHit near;
|
||||||
|
bool onMesh = NavMesh.SamplePosition(pos, out near, Mathf.Max(0.05f, cfg.offNavMeshTolerance), NavMesh.AllAreas);
|
||||||
|
bool tooHigh = HeightRuleApplies(cfg) && pos.y > cfg.maxWalkableY;
|
||||||
|
if (onMesh && !tooHigh) return false;
|
||||||
|
WarpBack(cfg, actor, key, pos, Time.unscaledTime, ReferenceEquals(actor, MyValue.MyPC),
|
||||||
|
tooHigh ? "언덕(y " + pos.y.ToString("F2") + ")" : "NavMesh 밖");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
/// <summary>새로 나타난 몬스터가 이동 불가 지역에 서 있으면 walkable 지점으로 보정한다(발주서 §1-4).</summary>
|
||||||
|
public static class SpawnPositionGuard
|
||||||
|
{
|
||||||
|
static WLTerrainGuardSettings Cfg { get { return WLTerrainGuardSettings.Instance; } }
|
||||||
|
|
||||||
|
/// <summary>스폰 보정 축이 살아 있는가.</summary>
|
||||||
|
public static bool Active
|
||||||
|
{
|
||||||
|
get { var c = Cfg; return c != null && c.enabled && c.spawnGuardEnabled && !WLTerrainGuardSettings.RuntimeDisabled; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ───────────────────────────────────────── 진단 카운터
|
||||||
|
public static int Seen, Checked, Fixed, FixFailed, FellBackToNode, Scans;
|
||||||
|
public static string LastLog = "";
|
||||||
|
public static Vector3 LastFixFrom, LastFixTo;
|
||||||
|
|
||||||
|
// ───────────────────────────────────────── 내부 상태
|
||||||
|
static MobActor[] s_mobs = new MobActor[0];
|
||||||
|
static readonly HashSet<int> s_seen = new HashSet<int>();
|
||||||
|
static readonly Dictionary<int, Vector3> s_spawnerNode = new Dictionary<int, Vector3>();
|
||||||
|
static MobControlMgr[] s_mgrs = new MobControlMgr[0];
|
||||||
|
static float s_nextScan;
|
||||||
|
static bool s_faulted;
|
||||||
|
|
||||||
|
internal static void Tick()
|
||||||
|
{
|
||||||
|
if (s_faulted || !Active) return;
|
||||||
|
var cfg = Cfg;
|
||||||
|
float now = Time.unscaledTime;
|
||||||
|
if (now < s_nextScan) return;
|
||||||
|
s_nextScan = now + Mathf.Max(0.05f, cfg.spawnCheckSeconds);
|
||||||
|
|
||||||
|
try { ScanNew(cfg); }
|
||||||
|
catch (System.Exception ex) { s_faulted = true; Log("중단(예외) — " + ex.Message); }
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ScanNew(WLTerrainGuardSettings cfg)
|
||||||
|
{
|
||||||
|
s_mobs = Object.FindObjectsByType<MobActor>(FindObjectsInactive.Exclude, FindObjectsSortMode.None);
|
||||||
|
Scans++;
|
||||||
|
if (s_seen.Count > 512) s_seen.Clear(); // 풀 재사용으로 쌓인 죽은 키 정리
|
||||||
|
|
||||||
|
for (int i = 0; i < s_mobs.Length; i++)
|
||||||
|
{
|
||||||
|
var m = s_mobs[i];
|
||||||
|
if (DSUtil.CheckNull(m) || m.IsDead()) continue;
|
||||||
|
int key = m.GetInstanceID();
|
||||||
|
if (!s_seen.Add(key)) continue; // 이미 본 개체
|
||||||
|
Seen++;
|
||||||
|
Validate(cfg, m);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>스폰 보정 결과. 0 = 원 위치가 이미 유효 · 1..n = n 번째 재표집 · −1 = 스포너 노드 · −2 = 실패.</summary>
|
||||||
|
public const int ResultValid = 0, ResultNode = -1, ResultFailed = -2;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 스폰 지점 판정 — 원 위치 → 재표집(반경 확대) → 스포너 노드 순. **좌표만 다루는 순수 함수**라
|
||||||
|
/// 프로브가 실 MobActor 없이 같은 코드를 그대로 검증할 수 있다(GC 0 · NavMeshHit 는 struct).
|
||||||
|
/// </summary>
|
||||||
|
public static int ResolveSpawnPoint(WLTerrainGuardSettings cfg, Vector3 pos, int spawnerId, out Vector3 target)
|
||||||
|
{
|
||||||
|
target = pos;
|
||||||
|
bool heightRule = TerrainGuard.HeightRuleApplies(cfg);
|
||||||
|
float baseR = Mathf.Max(0.1f, cfg.spawnSampleRadius);
|
||||||
|
NavMeshHit hit;
|
||||||
|
|
||||||
|
// ① 원 위치가 유효하면 아무것도 하지 않는다.
|
||||||
|
if (NavMesh.SamplePosition(pos, out hit, baseR, NavMesh.AllAreas)
|
||||||
|
&& !(heightRule && hit.position.y > cfg.maxWalkableY))
|
||||||
|
{ target = hit.position; return ResultValid; }
|
||||||
|
|
||||||
|
// ② 재시도 — 시도마다 반경을 1배씩 넓힌다(원본 MapData.spawnSampleTries 와 같은 뜻의 재시도).
|
||||||
|
int tries = Mathf.Max(0, cfg.spawnRetries);
|
||||||
|
for (int t = 1; t <= tries; t++)
|
||||||
|
{
|
||||||
|
if (!NavMesh.SamplePosition(pos, out hit, baseR * (t + 1), NavMesh.AllAreas)) continue;
|
||||||
|
if (heightRule && hit.position.y > cfg.maxWalkableY) continue;
|
||||||
|
target = hit.position; return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ③ 스포너 노드 중심.
|
||||||
|
if (cfg.spawnFallbackToSpawnerNode)
|
||||||
|
{
|
||||||
|
Vector3 node;
|
||||||
|
if (TryGetSpawnerNode(spawnerId, out node)
|
||||||
|
&& NavMesh.SamplePosition(node, out hit, baseR, NavMesh.AllAreas)
|
||||||
|
&& !(heightRule && hit.position.y > cfg.maxWalkableY))
|
||||||
|
{ target = hit.position; return ResultNode; }
|
||||||
|
}
|
||||||
|
return ResultFailed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>스폰 지점이 walkable 인지 확인하고, 아니면 재시도 → 스포너 노드 순으로 보정한다.</summary>
|
||||||
|
internal static bool Validate(WLTerrainGuardSettings cfg, MobActor mob)
|
||||||
|
{
|
||||||
|
Checked++;
|
||||||
|
Vector3 pos = mob.Get_position();
|
||||||
|
int spawnerId = mob.Get_Group();
|
||||||
|
Vector3 target;
|
||||||
|
int how = ResolveSpawnPoint(cfg, pos, spawnerId, out target);
|
||||||
|
|
||||||
|
if (how == ResultValid) return false;
|
||||||
|
if (how == ResultFailed)
|
||||||
|
{
|
||||||
|
FixFailed++;
|
||||||
|
Log("보정 실패 " + mob.name + " " + pos.ToString("F2"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (how == ResultNode) FellBackToNode++;
|
||||||
|
Apply(mob, pos, target, how == ResultNode ? "스포너 노드 " + spawnerId : "재표집 #" + how);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Apply(MobActor mob, Vector3 from, Vector3 to, string how)
|
||||||
|
{
|
||||||
|
var agent = mob.Get_Agent();
|
||||||
|
if (agent != null && agent.enabled && agent.isOnNavMesh) agent.ResetPath();
|
||||||
|
mob.Set_Warp(to);
|
||||||
|
Fixed++;
|
||||||
|
LastFixFrom = from; LastFixTo = to;
|
||||||
|
Log("보정 #" + Fixed + " " + mob.name + " " + from.ToString("F2") + " → " + to.ToString("F2") + " · " + how);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>스포너 ID → 노드 월드 좌표. 노드는 움직이지 않으므로 1회만 수집한다.</summary>
|
||||||
|
public static bool TryGetSpawnerNode(int spawnerId, out Vector3 pos)
|
||||||
|
{
|
||||||
|
if (s_spawnerNode.Count == 0)
|
||||||
|
{
|
||||||
|
s_mgrs = Object.FindObjectsByType<MobControlMgr>(FindObjectsInactive.Include, FindObjectsSortMode.None);
|
||||||
|
for (int i = 0; i < s_mgrs.Length; i++)
|
||||||
|
{
|
||||||
|
var g = s_mgrs[i];
|
||||||
|
if (DSUtil.CheckNull(g)) continue;
|
||||||
|
s_spawnerNode[g.SpawnerID] = g.transform.position;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s_spawnerNode.TryGetValue(spawnerId, out pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Log(string msg)
|
||||||
|
{
|
||||||
|
LastLog = msg;
|
||||||
|
var c = Cfg;
|
||||||
|
if (c != null && c.verboseLog) Debug.Log("[SpawnPositionGuard] " + msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ───────────────────────────────────────── 진단 · 초기화
|
||||||
|
/// <summary>프로브용: 카운터·기억 초기화.</summary>
|
||||||
|
public static void ResetDiagnostics()
|
||||||
|
{
|
||||||
|
Seen = Checked = Fixed = FixFailed = FellBackToNode = Scans = 0;
|
||||||
|
LastLog = ""; LastFixFrom = LastFixTo = Vector3.zero;
|
||||||
|
s_seen.Clear(); s_spawnerNode.Clear(); s_mobs = new MobActor[0]; s_mgrs = new MobControlMgr[0];
|
||||||
|
s_nextScan = 0f; s_faulted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>프로브용: 러너 없이 1회 검사.</summary>
|
||||||
|
public static void ForceTick() { s_nextScan = 0f; Tick(); }
|
||||||
|
|
||||||
|
/// <summary>프로브용: 러너가 부르는 것과 똑같은 틱(타이머 초기화 0 — GC 측정용).</summary>
|
||||||
|
public static void ProbeTick() { Tick(); }
|
||||||
|
|
||||||
|
/// <summary>프로브용: 스포너 노드 캐시를 프리팹 인스턴스 없이 직접 심는다(에디트 모드).</summary>
|
||||||
|
public static void SeedSpawnerNodeForProbe(int spawnerId, Vector3 pos) { s_spawnerNode[spawnerId] = pos; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
/// <summary>TerrainGuard · SpawnPositionGuard 의 시간 축. 숨김 GameObject 1개 · 코루틴 0.</summary>
|
||||||
|
internal sealed class TerrainGuardRunner : MonoBehaviour
|
||||||
|
{
|
||||||
|
void Update()
|
||||||
|
{
|
||||||
|
TerrainGuard.Tick();
|
||||||
|
SpawnPositionGuard.Tick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>러너 부팅(C8 — SO 가 꺼져 있으면 오브젝트 0).</summary>
|
||||||
|
public static class TerrainGuardBoot
|
||||||
|
{
|
||||||
|
static TerrainGuardRunner s_runner;
|
||||||
|
|
||||||
|
internal static void EnsureRunner()
|
||||||
|
{
|
||||||
|
if (!Application.isPlaying || !DSUtil.CheckNull(s_runner)) return;
|
||||||
|
var go = new GameObject("[WL813t2] TerrainGuardRunner");
|
||||||
|
go.hideFlags = HideFlags.HideAndDontSave; // 씬 전환에도 살아남고 씬·프리팹에 저장되지 않는다
|
||||||
|
s_runner = go.AddComponent<TerrainGuardRunner>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
|
||||||
|
static void Boot()
|
||||||
|
{
|
||||||
|
if (WLTerrainGuardSettings.Active) EnsureRunner();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 7159f67e61b5aa64fb6e3f96eca4cdff
|
||||||
|
|
@ -0,0 +1,142 @@
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// WLTerrainGuardSettings.cs — 언덕(이동 불가 지형) 가드 값의 단일 출처(SOT · C45)
|
||||||
|
//
|
||||||
|
// PD 지시 #813 (2026-09-10) · 발주서 WL-813t2 §1-3 · §1-4
|
||||||
|
// 「언덕 지형은 플레이어나 몬스터가 갈 수 없도록 · 언덕을 뚫고 가는 상황 및 카메라 연출 꼬임 ·
|
||||||
|
// 이동 불가 지역에는 몬스터가 생성되어서도 안 됨」
|
||||||
|
// 근거 = QA W2-12 D-48(PC 가 아레나 밖 고지대 y 3.66 → 5.48 로 올라가 보스를 한 대도 못 때리고 시간 종료)
|
||||||
|
//
|
||||||
|
// 에셋 경로(고정): Assets/WL/Combat/Settings/Resources/WL/WLTerrainGuardSettings.asset
|
||||||
|
// → Resources.Load<WLTerrainGuardSettings>("WL/WLTerrainGuardSettings") 로 1회 로드 후 캐시
|
||||||
|
// (WLCombatCoreSettings·WLBossArenaSettings 등 기존 설정 SO 관례 그대로)
|
||||||
|
//
|
||||||
|
// ■ C8 롤백
|
||||||
|
// ① 에셋이 없다 / ② enabled = 0 / ③ RuntimeDisabled = true → 러너 자체가 뜨지 않는다(오브젝트 0).
|
||||||
|
// 축별로도 끌 수 있다 — terrainGuardEnabled = 0(가드만 off) · spawnGuardEnabled = 0(스폰 보정만 off).
|
||||||
|
// 🔴 코어 스위치(WLCombatCoreSettings.Enabled)에 의존하지 않는다 — MobNavRecovery(813x)와 같은 이유로,
|
||||||
|
// 코어를 끈 A/B 중에도 「언덕에 올라간 채 굳는」 상태가 되살아나면 안 된다.
|
||||||
|
//
|
||||||
|
// ■ 아래 기본값은 에셋 생성 시 초기값이다. 실제로 읽히는 값은 항상 .asset 이며 코드에 수치 상수는 없다(C45).
|
||||||
|
//
|
||||||
|
// 🔴 어셈블리 주의: Assets/WL/Combat/ 에 .asmdef 를 만들지 말 것(Actor/MobActor 가 Assembly-CSharp 에 있다).
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace WL.Combat.Nav
|
||||||
|
{
|
||||||
|
[CreateAssetMenu(fileName = "WLTerrainGuardSettings", menuName = "WL/Terrain Guard Settings", order = 332)]
|
||||||
|
public sealed class WLTerrainGuardSettings : ScriptableObject
|
||||||
|
{
|
||||||
|
public const string ResourcesPath = "WL/WLTerrainGuardSettings";
|
||||||
|
|
||||||
|
static WLTerrainGuardSettings s_cached;
|
||||||
|
static bool s_lookupDone;
|
||||||
|
|
||||||
|
/// <summary>설정 에셋(1회 로드 후 캐시). 없으면 null = 기능 전체 off.</summary>
|
||||||
|
public static WLTerrainGuardSettings Instance
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (!s_lookupDone)
|
||||||
|
{
|
||||||
|
s_cached = Resources.Load<WLTerrainGuardSettings>(ResourcesPath);
|
||||||
|
s_lookupDone = true;
|
||||||
|
}
|
||||||
|
return s_cached;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>진단·A/B 전용 런타임 스위치. 도메인 리로드로 자동 false.</summary>
|
||||||
|
public static bool RuntimeDisabled;
|
||||||
|
|
||||||
|
/// <summary>두 축(지형 가드 · 스폰 보정)이 살아 있는가.</summary>
|
||||||
|
public static bool Active
|
||||||
|
{
|
||||||
|
get { var i = Instance; return i != null && i.enabled && !RuntimeDisabled; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>테스트용: 캐시를 비워 다음 접근 때 다시 Resources.Load 한다.</summary>
|
||||||
|
public static void ClearCache() { s_cached = null; s_lookupDone = false; }
|
||||||
|
|
||||||
|
/// <summary>프로브용: 임시 SO 를 잠시 Instance 자리에 끼운다(C8 진리표 · 실에셋은 읽기만 한다).</summary>
|
||||||
|
public static void SetInstanceForProbe(WLTerrainGuardSettings so) { s_cached = so; s_lookupDone = true; }
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────── 공통
|
||||||
|
[Header("공통")]
|
||||||
|
[Tooltip("0 이면 러너가 뜨지 않는다 = 원본 동작 100% (C8 롤백).")]
|
||||||
|
public bool enabled = true;
|
||||||
|
|
||||||
|
[Tooltip("복구·보정 1건마다 로그 1줄.")]
|
||||||
|
public bool verboseLog;
|
||||||
|
|
||||||
|
[Tooltip("점검 주기(초). 액터 1명당 판정 1회.")]
|
||||||
|
public float checkSeconds = 0.2f;
|
||||||
|
|
||||||
|
[Tooltip("몬스터 목록 재수집 주기(초). FindObjectsByType 는 여기서만 부른다(GC).")]
|
||||||
|
public float rescanSeconds = 3f;
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────── 지형 가드(§1-3)
|
||||||
|
[Header("지형 가드 — 언덕 위/NavMesh 밖 복귀")]
|
||||||
|
[Tooltip("0 이면 지형 가드만 끈다.")]
|
||||||
|
public bool terrainGuardEnabled = true;
|
||||||
|
|
||||||
|
[Tooltip("이 높이(m)를 넘는 곳에 서 있으면 언덕으로 본다. 813t2 재베이크 후 WL_Nature 의 walkable 최고 y = 2.96 m → 여유 0.5 m.")]
|
||||||
|
public float maxWalkableY = 3.5f;
|
||||||
|
|
||||||
|
[Tooltip("이 반경(m) 안에 NavMesh 가 없으면 「NavMesh 밖」으로 본다. 원본 넉백·점프의 짧은 이탈을 흘려보낼 만큼은 커야 한다.")]
|
||||||
|
public float offNavMeshTolerance = 1f;
|
||||||
|
|
||||||
|
[Tooltip("이상 상태가 이 시간(초)을 넘게 지속돼야 되돌린다. 점프·넉백 중에는 원본이 의도적으로 에이전트를 끈다.")]
|
||||||
|
public float dwellSeconds = 1f;
|
||||||
|
|
||||||
|
[Tooltip("같은 액터를 다시 되돌리기까지의 최소 간격(초).")]
|
||||||
|
public float warpCooldownSeconds = 3f;
|
||||||
|
|
||||||
|
[Tooltip("되돌릴 지점을 찾는 1차 반경(m).")]
|
||||||
|
public float warpNearRadius = 4f;
|
||||||
|
|
||||||
|
[Tooltip("1차에서 못 찾으면 쓰는 2차 반경(m).")]
|
||||||
|
public float warpFarRadius = 30f;
|
||||||
|
|
||||||
|
[Tooltip("플레이어를 가드 대상에 넣는다(D-48 의 당사자).")]
|
||||||
|
public bool guardPlayer = true;
|
||||||
|
|
||||||
|
[Tooltip("일반 몬스터를 가드 대상에 넣는다.")]
|
||||||
|
public bool guardMobs = true;
|
||||||
|
|
||||||
|
[Tooltip("보스를 가드 대상에 넣는다(Anubis 스킬2 점프).")]
|
||||||
|
public bool guardBoss = true;
|
||||||
|
|
||||||
|
[Tooltip("플레이어를 되돌린 직후 카메라 이동 리드를 0 으로 스냅한다(PD 「카메라 연출 꼬임」 · RealCamera 무수정).")]
|
||||||
|
public bool warpSnapsCameraLead = true;
|
||||||
|
|
||||||
|
[Header("높이 규칙의 적용 범위")]
|
||||||
|
[Tooltip("높이 규칙(maxWalkableY)을 적용할 맵 ID. WL_Nature = -1(BattleMapConfig 실측). " +
|
||||||
|
"다른 맵은 고지대가 정상 플레이 영역일 수 있어 높이 규칙을 적용하지 않는다(「NavMesh 밖」 규칙만 남는다).")]
|
||||||
|
public int[] heightGuardMapIds = new int[] { -1 };
|
||||||
|
|
||||||
|
[Tooltip("1 이면 맵 ID 와 무관하게 높이 규칙을 적용한다(진단용).")]
|
||||||
|
public bool heightGuardAllMaps;
|
||||||
|
|
||||||
|
[Tooltip("현재 맵 ID 를 못 읽을 때 높이 규칙을 적용할지. WL_Nature 는 로비 겸용(Load_Map(-1))이라 기본 1.")]
|
||||||
|
public bool heightGuardWhenMapUnknown = true;
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────── 스폰 위치 검증(§1-4)
|
||||||
|
[Header("스폰 위치 검증 — 이동 불가 지역 스폰 0")]
|
||||||
|
[Tooltip("0 이면 스폰 보정만 끈다.")]
|
||||||
|
public bool spawnGuardEnabled = true;
|
||||||
|
|
||||||
|
[Tooltip("새로 나타난 몬스터를 훑는 주기(초).")]
|
||||||
|
public float spawnCheckSeconds = 0.5f;
|
||||||
|
|
||||||
|
[Tooltip("스폰 지점이 유효한지 볼 때 쓰는 NavMesh 표집 반경(m). 원본 MapData.spawnSnapRadius(WL_Nature = 2)와 같은 뜻.")]
|
||||||
|
public float spawnSampleRadius = 2f;
|
||||||
|
|
||||||
|
[Tooltip("표집 재시도 횟수. 시도마다 반경을 1배씩 넓힌다(2 → 4 → 6 m).")]
|
||||||
|
public int spawnRetries = 3;
|
||||||
|
|
||||||
|
[Tooltip("재시도가 전부 실패하면 스포너 노드 중심으로 옮긴다(발주서 §1-4).")]
|
||||||
|
public bool spawnFallbackToSpawnerNode = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 562e876b3638e364496cbc31202aae5c
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
%YAML 1.1
|
||||||
|
%TAG !u! tag:unity3d.com,2011:
|
||||||
|
--- !u!114 &11400000
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 0}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: 562e876b3638e364496cbc31202aae5c, type: 3}
|
||||||
|
m_Name: WLTerrainGuardSettings
|
||||||
|
m_EditorClassIdentifier: Assembly-CSharp::WL.Combat.Nav.WLTerrainGuardSettings
|
||||||
|
enabled: 1
|
||||||
|
verboseLog: 0
|
||||||
|
checkSeconds: 0.2
|
||||||
|
rescanSeconds: 3
|
||||||
|
terrainGuardEnabled: 1
|
||||||
|
maxWalkableY: 3.5
|
||||||
|
offNavMeshTolerance: 1
|
||||||
|
dwellSeconds: 1
|
||||||
|
warpCooldownSeconds: 3
|
||||||
|
warpNearRadius: 4
|
||||||
|
warpFarRadius: 30
|
||||||
|
guardPlayer: 1
|
||||||
|
guardMobs: 1
|
||||||
|
guardBoss: 1
|
||||||
|
warpSnapsCameraLead: 1
|
||||||
|
heightGuardMapIds: ffffffff
|
||||||
|
heightGuardAllMaps: 0
|
||||||
|
heightGuardWhenMapUnknown: 1
|
||||||
|
spawnGuardEnabled: 1
|
||||||
|
spawnCheckSeconds: 0.5
|
||||||
|
spawnSampleRadius: 2
|
||||||
|
spawnRetries: 3
|
||||||
|
spawnFallbackToSpawnerNode: 1
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 9284524d54685de4288e8a26718c046e
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
Loading…
Reference in New Issue