637 lines
34 KiB
C#
637 lines
34 KiB
C#
// ─────────────────────────────────────────────────────────────────────────────
|
||
// 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;
|
||
}
|
||
}
|
||
|