// ───────────────────────────────────────────────────────────────────────────── // WL815c_Probe.cs — 스테이지 장벽 검증(에디트 모드 · Play 0 · 로그인 0) // unity command run_script --file AgentScripts/WL815c_Probe.cs --entry WL815c_Probe.RunAll // 산출물: AgentScripts/WL815c_PROBE.txt // ───────────────────────────────────────────────────────────────────────────── using System; using System.Diagnostics; using System.IO; using System.Text; using UnityEditor; using UnityEditor.SceneManagement; using UnityEngine; using UnityEngine.AI; using UnityEngine.Profiling; using UnityEngine.SceneManagement; using WL.Combat.Stage; public static class WL815c_Probe { static StringBuilder s_sb; static int s_pass, s_fail; // 기준서 §D-1 존1 앵커 static readonly Vector3 kAnchor = new Vector3(115f, 0.4f, -22f); const float kRadius = 14f; static void L(string s) { s_sb.AppendLine(s); } static void Chk(string name, bool ok, string detail) { if (ok) s_pass++; else s_fail++; L((ok ? " PASS " : " FAIL ") + name + " — " + detail); } public static string RunAll() { s_sb = new StringBuilder(); s_pass = 0; s_fail = 0; L("WL-815c 스테이지 장벽 프로브 · " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); L(" 앵커 = 기준서 §D-1 존1 " + kAnchor.ToString("F1") + " 반경 " + kRadius + " m"); var keepSo = WLStageBarrierSettings.Instance; Scene temp = default(Scene); try { temp = EditorSceneManager.NewScene( NewSceneSetup.DefaultGameObjects, NewSceneMode.Single); Sec1_Settings(); var ground = MakeGround(); bool navOk = BakeNav(ground); Sec2_SetClear(); Sec3_WarpMath(); Sec4_WarpReal(navOk); Sec5_Styles(); Sec6_Carve(); Sec7_GC(); Sec8_C8(); Sec9_Cost(); } catch (Exception ex) { s_fail++; L(" FAIL 예외 — " + ex.GetType().Name + " " + ex.Message + "\n" + ex.StackTrace); } finally { try { StageBarrier.HardResetForProbe(); } catch { } WLStageBarrierSettings.SetInstanceForProbe(keepSo); WLStageBarrierSettings.RuntimeDisabled = false; } L(""); L("RESULT " + (s_fail == 0 ? "PASS" : "FAIL") + " · " + s_pass + " PASS · " + s_fail + " FAIL"); File.WriteAllText("AgentScripts/WL815c_PROBE.txt", s_sb.ToString()); return (s_fail == 0 ? "PASS" : "FAIL") + " " + s_pass + "/" + (s_pass + s_fail); } // ═════════════════════════════════════════════════════════════════════════ static void Sec1_Settings() { L(""); L("① 설정 에셋(C45 — 값은 전부 SO)"); var c = WLStageBarrierSettings.Instance; Chk("1-1 에셋 로드", c != null, c != null ? "Resources/WL/WLStageBarrierSettings" : "null"); if (c == null) return; Chk("1-2 막기 값", c.leaveMargin > 0 && c.graceSeconds >= 0, "leaveMargin " + c.leaveMargin + " · grace " + c.graceSeconds + " s · 쿨 " + c.warpCooldownSeconds + " s · inset " + c.warpInsetMeters + " m · 샘플 " + c.warpNavSampleRadius + "/" + c.warpNavSampleRadiusFar + " m"); Chk("1-3 몹 값", c.blockMobs, "몹여유 " + c.mobLeaveMargin + " · 유예 " + c.mobGraceSeconds + " s · 쿨 " + c.mobWarpCooldownSeconds + " s"); Chk("1-4 카브 기본 off", !c.carveNavMesh, "carveNavMesh " + (c.carveNavMesh ? 1 : 0) + " · arc " + c.carveSegmentArcMeters + " m · 상한 " + c.maxCarveSegments + " · 두께 " + c.carveThickness + " · 높이 " + c.carveHeight); Chk("1-5 스타일", true, "style=" + c.style + " · 입장 " + c.appearSeconds + " s · 클리어 " + c.fadeSeconds + " s"); Chk("1-6 ⓐ 링 프리팹", c.ringPrefab != null, (c.ringPrefab != null ? c.ringPrefab.name : "null") + " · 실측 시각반경 " + c.ringPrefabVisualRadius + " m · 겹 " + (1 + c.ringExtraRings)); Chk("1-7 ⓑ 프롭 프리팹", c.propPrefabs != null && c.propPrefabs.Length > 0, Names(c.propPrefabs) + " · 간격 " + c.propSpacing + " m · 상한 " + c.maxProps); Chk("1-8 ⓒ 불꽃 프리팹", c.firePrefabs != null && c.firePrefabs.Length > 0, Names(c.firePrefabs) + " · 간격 " + c.fireSpacing + " m · 상한 " + c.maxFires + " · 파티클 총량 " + c.maxTotalParticles); } static string Names(GameObject[] a) { if (a == null || a.Length == 0) return "(없음)"; var sb = new StringBuilder(); for (int i = 0; i < a.Length; i++) { if (i > 0) sb.Append(", "); sb.Append(a[i] != null ? a[i].name : "null"); } return sb.ToString(); } // ═════════════════════════════════════════════════════════════════════════ static GameObject MakeGround() { var g = GameObject.CreatePrimitive(PrimitiveType.Plane); g.name = "[WL815c] Ground"; g.transform.position = new Vector3(kAnchor.x, kAnchor.y, kAnchor.z); g.transform.localScale = Vector3.one * 12f; // 120 × 120 m return g; } static bool BakeNav(GameObject ground) { L(""); L("② 임시 NavMesh 베이크(에디트 모드에서 실 워프 지점을 재기 위해)"); try { var surf = ground.AddComponent(); surf.collectObjects = Unity.AI.Navigation.CollectObjects.All; surf.BuildNavMesh(); var tri = NavMesh.CalculateTriangulation(); NavMeshHit h; bool hit = NavMesh.SamplePosition(kAnchor, out h, 5f, NavMesh.AllAreas); Chk("2-1 NavMesh 베이크", tri.vertices != null && tri.vertices.Length > 0 && hit, "정점 " + (tri.vertices != null ? tri.vertices.Length : 0) + " · 앵커 스냅 " + (hit ? h.position.ToString("F2") : "실패")); return hit; } catch (Exception ex) { Chk("2-1 NavMesh 베이크", false, "미확인 — " + ex.Message); return false; } } // ═════════════════════════════════════════════════════════════════════════ static void Sec2_SetClear() { L(""); L("③ Set / Clear 왕복 (오브젝트 0 · 풀 반납)"); var B = typeof(StageBarrier); StageBarrier.HardResetForProbe(); Chk("3-1 초기", !StageBarrier.Active, "Active=false · IsInside(어디든)=true → " + StageBarrier.IsInside(new Vector3(999f, 0f, 999f))); StageBarrier.Set(kAnchor, kRadius); int inst1 = StageBarrierVisual.Instantiates; int live1 = StageBarrierVisual.LiveCount; Chk("3-2 Set", StageBarrier.Active && live1 > 0, "Active=true · 중심 " + StageBarrier.Center.ToString("F1") + " 반경 " + StageBarrier.Radius + " · 그림 인스턴스 " + live1 + " · Instantiate 누계 " + inst1); Chk("3-3 IsInside", StageBarrier.IsInside(kAnchor) && StageBarrier.IsInside(kAnchor + new Vector3(13.9f, 0f, 0f)) && !StageBarrier.IsInside(kAnchor + new Vector3(14.1f, 0f, 0f)), "중심=안 · 13.9 m=안 · 14.1 m=밖 · 높이 무관(y+50 → " + StageBarrier.IsInside(kAnchor + new Vector3(0f, 50f, 0f)) + ")"); // 클리어 → 페이드 도중에는 아직 살아 있어야 한다(연출) StageBarrier.Clear(); StageBarrier.ForceTick(); int liveMid = StageBarrierVisual.LiveCount; Chk("3-4a Clear 직후 = 페이드 중", !StageBarrier.Active && liveMid > 0, "Active=false · 페이드 중 활성 " + liveMid + " · 진행도 " + StageBarrierVisual.Phase.ToString("F2")); // 페이드 시계를 끝으로 → 실제 종료 경로(ReturnAll)를 탄다 StageBarrierVisual.FinishPhaseForProbe(); StageBarrier.ForceTick(); int live2 = StageBarrierVisual.LiveCount; int pooled = StageBarrierVisual.PooledInstances; Chk("3-4 페이드 끝 → 활성 0 · 풀 반납", !StageBarrier.Active && live2 == 0 && pooled == inst1, "활성 " + live2 + " · 풀 " + pooled + "(= Instantiate 누계 " + inst1 + ") · Destroy 0"); // 두 번째 Set — 풀에서 꺼내 쓰므로 Instantiate 가 늘지 않아야 한다 StageBarrier.Set(kAnchor, kRadius); int inst2 = StageBarrierVisual.Instantiates; Chk("3-5 재Set → 풀 재사용", inst2 == inst1 && StageBarrierVisual.LiveCount == live1, "Instantiate " + inst1 + " → " + inst2 + "(증가 " + (inst2 - inst1) + ") · 활성 " + StageBarrierVisual.LiveCount); // 입장 연출 StageBarrierVisual.ForcePhase(0f); float s0 = FirstScale(); StageBarrierVisual.ForcePhase(0.5f); float s5 = FirstScale(); StageBarrierVisual.ForcePhase(1f); float s1 = FirstScale(); Chk("3-6 입장/클리어 연출", s0 < s5 && s5 < s1, "스케일 t=0 " + s0.ToString("F2") + " → t=.5 " + s5.ToString("F2") + " → t=1 " + s1.ToString("F2") + " (appear " + WLStageBarrierSettings.Instance.appearSeconds + " s / fade " + WLStageBarrierSettings.Instance.fadeSeconds + " s)"); Chk("3-7 반경 0 → Clear", true, ProbeZero()); _ = B; } static string ProbeZero() { StageBarrier.Set(kAnchor, 0f); string r = "Set(r=0) → Active=" + StageBarrier.Active; StageBarrier.Set(kAnchor, kRadius); return r; } static float FirstScale() { var root = StageBarrierVisual.Root; if (root == null || root.childCount == 0) return -1f; for (int i = 0; i < root.childCount; i++) if (root.GetChild(i).gameObject.activeSelf) return root.GetChild(i).localScale.x; return 0f; } // ═════════════════════════════════════════════════════════════════════════ static void Sec3_WarpMath() { L(""); L("④ 되돌릴 지점 계산(순수 함수 · NearestInsidePoint)"); var c = WLStageBarrierSettings.Instance; float inset = c.warpInsetMeters; float want = kRadius - inset; Vector3[] outs = { kAnchor + new Vector3(30f, 0f, 0f), kAnchor + new Vector3(0f, 0f, -25f), kAnchor + new Vector3(-40f, 3f, 40f), kAnchor + new Vector3(14.5f, 0f, 0f), }; bool all = true; for (int i = 0; i < outs.Length; i++) { var p = StageBarrier.NearestInsidePoint(c, kAnchor, kRadius, outs[i]); float d = new Vector2(p.x - kAnchor.x, p.z - kAnchor.z).magnitude; bool ok = Mathf.Abs(d - want) < 0.01f; // 방향이 유지되는가(가장 가까운 안쪽 지점 = 같은 방위각) var dirIn = new Vector2(p.x - kAnchor.x, p.z - kAnchor.z).normalized; var dirOut = new Vector2(outs[i].x - kAnchor.x, outs[i].z - kAnchor.z).normalized; bool sameDir = Vector2.Dot(dirIn, dirOut) > 0.999f; all = all && ok && sameDir; L(" " + outs[i].ToString("F1") + " (중심 " + new Vector2(outs[i].x - kAnchor.x, outs[i].z - kAnchor.z).magnitude.ToString("F1") + " m) → " + p.ToString("F2") + " (중심 " + d.ToString("F2") + " m · 방위 유지 " + (sameDir ? "Y" : "N") + ")"); } Chk("4-1 안쪽 " + want + " m · 방위 유지", all, "4/4 · inset " + inset + " m"); var mid = StageBarrier.NearestInsidePoint(c, kAnchor, kRadius, kAnchor); Chk("4-2 정중앙 예외", Mathf.Approximately(mid.x, kAnchor.x) && Mathf.Approximately(mid.z, kAnchor.z), "중심 그대로 " + mid.ToString("F2") + "(0 나누기 없음)"); } // ═════════════════════════════════════════════════════════════════════════ static void Sec4_WarpReal(bool navOk) { L(""); L("⑤ 실 워프(원본 Actor.Set_Warp · NavMesh 스냅)"); var c = WLStageBarrierSettings.Instance; StageBarrier.Set(kAnchor, kRadius); StageBarrier.ResetDiagnostics(); if (!navOk) { Chk("5-0 NavMesh", false, "미확인 — 베이크 실패라 실 워프 지점은 재지 못했다"); } else { Vector3 t; bool ok = StageBarrier.ResolveWarpTarget(c, kAnchor, kRadius, kAnchor + new Vector3(35f, 0f, 0f), out t); float d = new Vector2(t.x - kAnchor.x, t.z - kAnchor.z).magnitude; NavMeshHit h; bool onMesh = NavMesh.SamplePosition(t, out h, 0.3f, NavMesh.AllAreas); Chk("5-1 스냅 지점 = 안쪽 · NavMesh 위", ok && d < kRadius && onMesh, "밖 35 m → " + t.ToString("F2") + " · 중심 " + d.ToString("F2") + " m(< " + kRadius + ") · NavMesh 위 " + onMesh); // NavMesh 가 아예 없는 방향(평면 밖 200 m) Vector3 t2; bool ok2 = StageBarrier.ResolveWarpTarget(c, new Vector3(9000f, 0f, 9000f), kRadius, new Vector3(9100f, 0f, 9000f), out t2); Chk("5-2 NavMesh 없으면 워프 안 함", !ok2, "먼 좌표(9000,0,9000) → " + (ok2 ? "워프함(FAIL)" : "false = 워프 포기(지형 뚫기 방지)")); } // 실 PC 프리팹으로 왕복 var pcPrefab = AssetDatabase.LoadAssetAtPath("Assets/Res_Addr/PC/Ai01.prefab"); if (pcPrefab == null) { Chk("5-3 PC 프리팹", false, "미확인 — Ai01.prefab 없음"); } else { var pc = UnityEngine.Object.Instantiate(pcPrefab); pc.transform.position = kAnchor + new Vector3(30f, 0f, 0f); var actor = pc.GetComponent(); if (actor == null) Chk("5-3 PCActor", false, "미확인 — Actor 컴포넌트 없음"); else { // 🔴 에디트 모드는 Awake 가 돌지 않아 Actor.m_NavMeshAgent(비직렬화 protected)가 null 이다. // Set_Warp 이 그것을 쓰므로 **프로브가** Awake 가 넣었을 값을 직접 심는다(게임 코드 무관 · 스캐폴딩). SeedAgent(actor, pc); try { Vector3 before = actor.Get_position(); bool warped = StageBarrier.ForceCheck(actor, true); Vector3 after = actor.Get_position(); float dAfter = new Vector2(after.x - kAnchor.x, after.z - kAnchor.z).magnitude; Chk("5-3 PC 실 워프", warped && dAfter < kRadius, "밖 " + new Vector2(before.x - kAnchor.x, before.z - kAnchor.z).magnitude.ToString("F1") + " m → 중심 " + dAfter.ToString("F2") + " m · PlayerWarps " + StageBarrier.PlayerWarps + " · " + StageBarrier.LastWarpFrom.ToString("F1") + " → " + StageBarrier.LastWarpTo.ToString("F1")); // 안에 있으면 워프하지 않는다 actor.transform.position = kAnchor; bool w2 = StageBarrier.ForceCheck(actor, true); Chk("5-4 안에 있으면 무동작", !w2, "중심에서 ForceCheck → 워프 " + (w2 ? "함(FAIL)" : "안 함")); } catch (Exception ex) { Chk("5-3 PC 실 워프", false, "미확인 — " + ex.GetType().Name + " " + ex.Message); } } UnityEngine.Object.DestroyImmediate(pc); } // 몹 var mobPrefab = AssetDatabase.LoadAssetAtPath("Assets/Res_Addr/Mobs/Mob/Batty_A.prefab"); if (mobPrefab == null) { Chk("5-5 몹 프리팹", false, "미확인 — Batty_A.prefab 없음"); } else { var mob = UnityEngine.Object.Instantiate(mobPrefab); mob.transform.position = kAnchor + new Vector3(0f, 0f, 28f); var ma = mob.GetComponent(); if (ma == null) Chk("5-5 MobActor", false, "미확인 — Actor 없음"); else { SeedAgent(ma, mob); try { bool warped = StageBarrier.ForceCheck(ma, false); float dAfter = new Vector2(ma.Get_position().x - kAnchor.x, ma.Get_position().z - kAnchor.z).magnitude; Chk("5-5 몹 복귀", warped && dAfter < kRadius, "밖 28 m → 중심 " + dAfter.ToString("F2") + " m · MobWarps " + StageBarrier.MobWarps); } catch (Exception ex) { Chk("5-5 몹 복귀", false, "미확인 — " + ex.GetType().Name + " " + ex.Message); } } UnityEngine.Object.DestroyImmediate(mob); } // 유예 · 쿨다운 진리표(가짜 시각 없이 — 유예를 0 으로 둔 임시 SO) var tmp = Clone(c); tmp.graceSeconds = 999f; WLStageBarrierSettings.SetInstanceForProbe(tmp); StageBarrier.ResetDiagnostics(); StageBarrier.Set(kAnchor, kRadius); var g = new GameObject("probe_dummy"); g.transform.position = kAnchor + new Vector3(40f, 0f, 0f); StageBarrier.ForceTick(); StageBarrier.ForceTick(); Chk("5-6 유예 안에서는 안 되돌린다", StageBarrier.PlayerWarps == 0 && StageBarrier.MobWarps == 0, "grace 999 s · 틱 ×2 → 워프 0 (OutsideSeen " + StageBarrier.OutsideSeen + ")"); UnityEngine.Object.DestroyImmediate(g); WLStageBarrierSettings.SetInstanceForProbe(c); } // ═════════════════════════════════════════════════════════════════════════ static void Sec5_Styles() { L(""); L("⑥ 스타일 전환 3회 (PD 가 값 하나로 바꾼다)"); var c = WLStageBarrierSettings.Instance; StageBarrier.HardResetForProbe(); WLStageBarrierSettings.SetInstanceForProbe(c); var styles = new StageBarrierStyle[] { StageBarrierStyle.MagicRing, StageBarrierStyle.Props, StageBarrierStyle.Fire, }; for (int i = 0; i < styles.Length; i++) { StageBarrierVisual.StyleOverride = styles[i]; StageBarrier.Set(kAnchor, kRadius); StageBarrierVisual.ForcePhase(1f); int rend, mats, ps; long tri; StageBarrierVisual.MeasureCost(out rend, out mats, out tri, out ps); string extra = ""; if (styles[i] == StageBarrierStyle.MagicRing) extra = " · 링 스케일 " + StageBarrierVisual.RingScale.ToString("F2") + "(반경 " + kRadius + " ÷ 실측 " + c.ringPrefabVisualRadius + ")"; else if (styles[i] == StageBarrierStyle.Props) extra = " · 프롭 " + StageBarrierVisual.PropCount + "개 · 실간격 " + StageBarrierVisual.PropSpacingUsed.ToString("F2") + " m"; else extra = " · 불꽃 " + StageBarrierVisual.FireCount + "개 · 방출률 ×" + StageBarrierVisual.FireRateMul.ToString("F2") + " · 파티클 추정 " + StageBarrierVisual.FireParticleEstimate + "(개당 " + StageBarrierVisual.FirePerInstanceEstimate + ")"; Chk("6-" + (i + 1) + " " + styles[i], StageBarrierVisual.CurrentStyle == styles[i] && StageBarrierVisual.LiveCount > 0, "활성 " + StageBarrierVisual.LiveCount + " · 렌더러 " + rend + " · 머티리얼 " + mats + " · tri " + tri + " · PS " + ps + " · 빌드 " + StageBarrierVisual.LastBuildMs.ToString("F1") + " ms" + extra); } Chk("6-4 전환 누계", StageBarrierVisual.StyleSwitches >= 2, "StyleSwitches " + StageBarrierVisual.StyleSwitches + " · Instantiate 누계 " + StageBarrierVisual.Instantiates); // None 으로 되돌리기 StageBarrierVisual.StyleOverride = StageBarrierStyle.None; StageBarrier.Set(kAnchor, kRadius); Chk("6-5 None → 그림 0", StageBarrierVisual.LiveCount == 0 && StageBarrier.Active, "활성 그림 " + StageBarrierVisual.LiveCount + " · 판정은 그대로 Active=" + StageBarrier.Active); StageBarrierVisual.StyleOverride = (StageBarrierStyle)(-1); } // ═════════════════════════════════════════════════════════════════════════ static void Sec6_Carve() { L(""); L("⑦ NavMesh 카브 on/off (옵션 · 기준서 §D-2 ②)"); var c = WLStageBarrierSettings.Instance; StageBarrier.HardResetForProbe(); WLStageBarrierSettings.SetInstanceForProbe(c); StageBarrier.Set(kAnchor, kRadius); Chk("7-1 off(기본) → 카브 0", StageBarrier.CarveObjectCount == 0, "carveNavMesh " + (c.carveNavMesh ? 1 : 0) + " → 오브젝트 " + StageBarrier.CarveObjectCount); var tmp = Clone(c); tmp.carveNavMesh = true; WLStageBarrierSettings.SetInstanceForProbe(tmp); StageBarrier.HardResetForProbe(); WLStageBarrierSettings.SetInstanceForProbe(tmp); int want = StageBarrier.CarveSegmentCount(tmp, kRadius); float circ = 2f * Mathf.PI * kRadius; var before = NavMesh.CalculateTriangulation(); var sw = Stopwatch.StartNew(); StageBarrier.Set(kAnchor, kRadius); sw.Stop(); Chk("7-2 on → 세그먼트 생성", StageBarrier.CarveObjectCount == want && want > 0, "둘레 " + circ.ToString("F1") + " m ÷ arc " + tmp.carveSegmentArcMeters + " m = " + want + "(상한 " + tmp.maxCarveSegments + ") · 오브젝트 " + StageBarrier.CarveObjectCount + " · 생성 " + StageBarrier.LastCarveBuildMs.ToString("F2") + " ms · Set 전체 " + sw.Elapsed.TotalMilliseconds.ToString("F2") + " ms"); // 세그먼트 기하 확인 — 카브 오브젝트는 HideAndDontSave 라 FindObjectsByType 에 안 잡힌다(내부 목록으로 읽는다) int carving = 0, boxShape = 0; float gap = -1f, sizeX = 0f, sizeZ = 0f, ringDev = 0f; Vector3 p0 = Vector3.zero, p1 = Vector3.zero; for (int i = 0; i < StageBarrier.CarveObjectCount; i++) { var ob = StageBarrier.CarveObstacleAt(i); if (ob == null) continue; if (ob.carving) carving++; if (ob.shape == NavMeshObstacleShape.Box) boxShape++; if (i == 0) { p0 = ob.transform.position; sizeX = ob.size.x; sizeZ = ob.size.z; } if (i == 1) p1 = ob.transform.position; ringDev = Mathf.Max(ringDev, Mathf.Abs( new Vector2(ob.transform.position.x - kAnchor.x, ob.transform.position.z - kAnchor.z).magnitude - kRadius)); } gap = Vector3.Distance(p0, p1); float step = circ / Mathf.Max(1, want); Chk("7-3 세그먼트 기하", carving == want && boxShape == want && sizeX > gap && ringDev < 0.01f, "carving=true " + carving + " · Box " + boxShape + " · 상자 " + sizeX.ToString("F2") + " × 두께 " + sizeZ.ToString("F2") + " m > 이웃 간격 " + gap.ToString("F2") + " m(이론 " + step.ToString("F2") + " · 겹침 " + (tmp.carveOverlap * 100f).ToString("F0") + " %) · 원 이탈 최대 " + ringDev.ToString("F4") + " m"); // 재베이크(카브 반영) 비용 — 에디트 모드에서 실제로 반영되는지 var after = NavMesh.CalculateTriangulation(); L(" NavMesh 삼각형 " + (before.indices != null ? before.indices.Length / 3 : 0) + " → " + (after.indices != null ? after.indices.Length / 3 : 0) + " (에디트 모드는 carve 가 즉시 반영되지 않을 수 있다 — 실 반영·재베이크 ms 는 Play 확인)"); StageBarrier.Clear(); Chk("7-4 Clear → 카브 회수", StageBarrier.CarveObjectCount == 0, "오브젝트 " + StageBarrier.CarveObjectCount); WLStageBarrierSettings.SetInstanceForProbe(c); StageBarrier.HardResetForProbe(); WLStageBarrierSettings.SetInstanceForProbe(c); } // ═════════════════════════════════════════════════════════════════════════ static void Sec7_GC() { L(""); L("⑧ GC (프레임당 할당 0)"); var c = WLStageBarrierSettings.Instance; StageBarrier.Set(kAnchor, kRadius); StageBarrierVisual.ForcePhase(1f); for (int i = 0; i < 10; i++) StageBarrier.ProbeTick(); // 워밍 long a = Profiler.GetMonoUsedSizeLong(); for (int i = 0; i < 200; i++) StageBarrier.ProbeTick(); long b = Profiler.GetMonoUsedSizeLong(); Chk("8-1 반경 안 · 틱 ×200", b - a == 0, "할당 " + (b - a) + " B (틱마다 " + ((b - a) / 200f).ToString("F1") + " B) · 계측 = Profiler.GetMonoUsedSizeLong"); // 대조군 — 확실히 할당되는 배열 200개(계측기가 살아 있는지 확인) long c0 = Profiler.GetMonoUsedSizeLong(); object sink = null; for (int i = 0; i < 200; i++) sink = new byte[65536]; long c1 = Profiler.GetMonoUsedSizeLong(); L(" 대조군(byte[65536] ×200 = 12.8 MB) " + (c1 - c0) + " B · sink " + (sink != null)); // 폴링 검사가 실제로 도는지 — 씬에 몹을 하나 놓고 재수집·판정이 도는지 본다 var mobPrefab = AssetDatabase.LoadAssetAtPath("Assets/Res_Addr/Mobs/Mob/Batty_A.prefab"); GameObject probeMob = null; if (mobPrefab != null) { probeMob = UnityEngine.Object.Instantiate(mobPrefab); probeMob.transform.position = kAnchor; // 안쪽 = 워프 대상 아님 } int chk0 = StageBarrier.Checks; StageBarrier.ForceTick(); Chk("8-2 폴링 동작", StageBarrier.Scans > 0 && (probeMob == null || StageBarrier.Checks > chk0), "Scans " + StageBarrier.Scans + " · Checks " + chk0 + " → " + StageBarrier.Checks + " (몹 " + (probeMob != null ? 1 : 0) + "기 · 안쪽이라 워프 " + (StageBarrier.MobWarps) + ")"); // 몹이 있는 상태에서 다시 GC 측정(실전에 가까운 조건) for (int i = 0; i < 10; i++) StageBarrier.ProbeTick(); long d0 = Profiler.GetMonoUsedSizeLong(); for (int i = 0; i < 200; i++) StageBarrier.ProbeTick(); long d1 = Profiler.GetMonoUsedSizeLong(); Chk("8-3 몹 1기 · 틱 ×200", d1 - d0 == 0, "할당 " + (d1 - d0) + " B"); if (probeMob != null) UnityEngine.Object.DestroyImmediate(probeMob); _ = c; } // ═════════════════════════════════════════════════════════════════════════ static void Sec8_C8() { L(""); L("⑨ C8 롤백 진리표"); var real = WLStageBarrierSettings.Instance; // enabled = 0 var off = Clone(real); off.enabled = false; StageBarrier.HardResetForProbe(); WLStageBarrierSettings.SetInstanceForProbe(off); StageBarrier.Set(kAnchor, kRadius); Chk("9-1 enabled=0 → Set 무동작", !StageBarrier.Active && StageBarrierVisual.Instantiates == 0, "Active=" + StageBarrier.Active + " · Instantiate " + StageBarrierVisual.Instantiates + " (오브젝트 0)"); // RuntimeDisabled StageBarrier.HardResetForProbe(); WLStageBarrierSettings.SetInstanceForProbe(real); WLStageBarrierSettings.RuntimeDisabled = true; StageBarrier.Set(kAnchor, kRadius); Chk("9-2 RuntimeDisabled=1 → Set 무동작", !StageBarrier.Active, "Active=" + StageBarrier.Active + " · Instantiate " + StageBarrierVisual.Instantiates); WLStageBarrierSettings.RuntimeDisabled = false; // 에셋 없음 StageBarrier.HardResetForProbe(); WLStageBarrierSettings.SetInstanceForProbe(null); StageBarrier.Set(kAnchor, kRadius); Chk("9-3 에셋 없음 → Set 무동작", !StageBarrier.Active && !StageBarrier.FeatureEnabled, "Active=" + StageBarrier.Active + " · FeatureEnabled=" + StageBarrier.FeatureEnabled); // blockEnabled = 0 → 그림만 var noBlock = Clone(real); noBlock.blockEnabled = false; StageBarrier.HardResetForProbe(); WLStageBarrierSettings.SetInstanceForProbe(noBlock); StageBarrier.Set(kAnchor, kRadius); var d = new GameObject("probe_far"); d.transform.position = kAnchor + new Vector3(60f, 0f, 0f); for (int i = 0; i < 5; i++) StageBarrier.ForceTick(); Chk("9-4 blockEnabled=0 → 판정 0 · 그림은 남는다", StageBarrier.Checks == 0 && StageBarrierVisual.LiveCount > 0, "Checks " + StageBarrier.Checks + " · 그림 " + StageBarrierVisual.LiveCount); UnityEngine.Object.DestroyImmediate(d); // showVisual = 0 → 판정만 var noVis = Clone(real); noVis.showVisual = false; StageBarrier.HardResetForProbe(); WLStageBarrierSettings.SetInstanceForProbe(noVis); StageBarrier.Set(kAnchor, kRadius); Chk("9-5 showVisual=0 → 그림 0 · 판정은 산다", StageBarrierVisual.LiveCount == 0 && StageBarrier.Active, "그림 " + StageBarrierVisual.LiveCount + " · Active=" + StageBarrier.Active); StageBarrier.HardResetForProbe(); WLStageBarrierSettings.SetInstanceForProbe(real); } // ═════════════════════════════════════════════════════════════════════════ static void Sec9_Cost() { L(""); L("⑩ 성능 표 (반경 14 m · 스타일별)"); var c = WLStageBarrierSettings.Instance; StageBarrier.HardResetForProbe(); WLStageBarrierSettings.SetInstanceForProbe(c); L(" | 스타일 | 인스턴스 | 렌더러 | 머티리얼 | 삼각형 | 파티클시스템 | 정상상태 파티클 | 빌드 ms |"); L(" |---|---|---|---|---|---|---|---|"); var styles = new StageBarrierStyle[] { StageBarrierStyle.MagicRing, StageBarrierStyle.Props, StageBarrierStyle.Fire, }; for (int i = 0; i < styles.Length; i++) { StageBarrierVisual.StyleOverride = styles[i]; StageBarrier.Set(kAnchor, kRadius); StageBarrierVisual.ForcePhase(1f); int rend, mats, ps; long tri; StageBarrierVisual.MeasureCost(out rend, out mats, out tri, out ps); int est = styles[i] == StageBarrierStyle.Fire ? StageBarrierVisual.FireParticleEstimate : (styles[i] == StageBarrierStyle.MagicRing ? StageBarrierVisual.LiveCount * 4 : 0); L(" | " + styles[i] + " | " + StageBarrierVisual.LiveCount + " | " + rend + " | " + mats + " | " + tri + " | " + ps + " | " + est + " | " + StageBarrierVisual.LastBuildMs.ToString("F1") + " |"); } StageBarrierVisual.StyleOverride = (StageBarrierStyle)(-1); Chk("10-1 성능 표", true, "위 표 참조(에디트 모드 측정 · 실 드로우콜은 캡처의 UnityStats)"); // 반경별 개수 L(" 반경별 프롭 개수(간격 " + c.propSpacing + " m · 상한 " + c.maxProps + ")"); float[] rs = { 8f, 14f, 20f, 30f, 40f }; StageBarrierVisual.StyleOverride = StageBarrierStyle.Props; for (int i = 0; i < rs.Length; i++) { StageBarrier.Set(kAnchor, rs[i]); L(" r " + rs[i] + " m(둘레 " + (2f * Mathf.PI * rs[i]).ToString("F1") + " m) → 프롭 " + StageBarrierVisual.PropCount + " · 실간격 " + StageBarrierVisual.PropSpacingUsed.ToString("F2") + " m · 카브 세그(on 일 때) " + StageBarrier.CarveSegmentCount(c, rs[i])); } StageBarrierVisual.StyleOverride = (StageBarrierStyle)(-1); StageBarrier.HardResetForProbe(); WLStageBarrierSettings.SetInstanceForProbe(c); } /// /// 에디트 모드 스캐폴딩 — Awake 가 넣었을 `Actor.m_NavMeshAgent`(비직렬화 protected)를 심는다. /// 게임 코드에는 리플렉션이 0 이고, 이 프로브에서만 쓴다. /// static string SeedAgent(Actor a, GameObject go) { try { var agent = go.GetComponent(); if (agent == null) agent = go.GetComponentInChildren(true); if (agent == null) return "NavMeshAgent 없음"; var fi = typeof(Actor).GetField("m_NavMeshAgent", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); if (fi == null) return "필드 없음"; fi.SetValue(a, agent); return "seeded"; } catch (Exception ex) { return ex.Message; } } // ═════════════════════════════════════════════════════════════════════════ static WLStageBarrierSettings Clone(WLStageBarrierSettings src) { var t = UnityEngine.Object.Instantiate(src); t.hideFlags = HideFlags.HideAndDontSave; return t; } }