Project_WL/AgentScripts/WL815c_Probe.cs

637 lines
34 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ─────────────────────────────────────────────────────────────────────────────
// 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;
}
}