Project_WL/AgentScripts/WL815c_Probe.cs

637 lines
34 KiB
C#
Raw Permalink Normal View History

// ─────────────────────────────────────────────────────────────────────────────
// 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<Unity.AI.Navigation.NavMeshSurface>();
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<GameObject>("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<Actor>();
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<GameObject>("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<Actor>();
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<GameObject>("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);
}
/// <summary>
/// 에디트 모드 스캐폴딩 — Awake 가 넣었을 `Actor.m_NavMeshAgent`(비직렬화 protected)를 심는다.
/// 게임 코드에는 리플렉션이 0 이고, 이 프로브에서만 쓴다.
/// </summary>
static string SeedAgent(Actor a, GameObject go)
{
try
{
var agent = go.GetComponent<UnityEngine.AI.NavMeshAgent>();
if (agent == null) agent = go.GetComponentInChildren<UnityEngine.AI.NavMeshAgent>(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;
}
}