// WL792_Verify.cs — #792 무기 정합 배치 검증 (Play 전용) // // unity command run_script --file AgentScripts/WL792_Verify.cs --entry WL792_Verify.Dirs --args '[10501]' // unity command run_script --file AgentScripts/WL792_Verify.cs --entry WL792_Verify.Dirs --args '[10101]' // unity command run_script --file AgentScripts/WL792_Verify.cs --entry WL792_Verify.Combo --args '[10501]' // unity command run_script --file AgentScripts/WL792_Verify.cs --entry WL792_Verify.Moving --args '[10501]' // unity command run_script --file AgentScripts/WL792_Verify.cs --entry WL792_Verify.Budget // // 재는 것 (발주 §4) // 법선 오차(도) · 호 방향(볼록) 오차(도) · 반지름 비(호/칼끝) · 중심 오프셋(m) · 스폰 지연(ms) // 찌르기: 축 방향 오차(도) · 길이 비 // 값은 추정이 아니라 WeaponTrailDriver.LastPlacement 에 담긴 **목표값과 배치 결과값**을 그대로 쓴다(C39). // // 캡처: Screenshots_WL/combat6/ (클립 피크 프레임 · 방향별 · 이동 중 · 콤보 연속) using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text; using UnityEngine; using WL.Combat; public static class WL792_Verify { const string OutDir = "Screenshots_WL/combat6"; const BindingFlags BF = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; public static void Dirs(int classId) { Launch(h => h.Co_Dirs(classId), "dirs" + classId); } public static void Combo(int classId) { Launch(h => h.Co_Combo(classId), "combo" + classId); } public static void ComboSpeed(int classId, float speed) { Launch(h => h.Co_ComboSpeed(classId, speed), "comboS" + classId); } // #804 클립 원속 재실측 public static void Moving(int classId) { Launch(h => h.Co_Moving(classId), "moving" + classId); } public static void Budget() { Launch(h => h.Co_Budget(), "budget"); } public static void Regress() { Launch(h => h.Co_Regress(), "regress"); } public static void Trace(int classId) { Launch(h => h.Co_Trace(classId), "trace" + classId); } public static void Trace3(int classId) { Launch(h => h.Co_Trace3(classId), "trace3_" + classId); } // #804 Attack3 한 클립 프레임 추적 /// 런타임 캘리브레이션·기하 캐시를 비운다(설정/알고리즘을 바꾼 뒤 재실측). public static void Reset() { WeaponTrailDriver.ClearMappedCache(); SlashTrailSettings.ClearCache(); Debug.Log("[WL #792] 런타임 캐시 초기화 — 다음 스윙부터 다시 실측한다"); } /// 런타임 캘리브레이션 캐시를 그대로 덤프한다(베이크 전 확인용). public static void DumpCal() { var sb = new StringBuilder(); sb.AppendLine("# 런타임 캘리브레이션 " + WeaponTrailDriver.RuntimeCalibrations.Count + "행 " + DateTime.Now.ToString("HH:mm:ss")); foreach (var kv in WeaponTrailDriver.RuntimeCalibrations) { var c = kv.Value; sb.AppendLine(string.Format("{0,-30} {1,-5} sweep={2,6:F1}° tipR={3:F3} hiltR={4:F3} stabLen={5:F3} | {6}", kv.Key, c.kind, c.sweepDeg, c.tipRadius, c.hiltRadius, c.stabLength, c.source)); } System.IO.Directory.CreateDirectory(OutDir); System.IO.File.WriteAllText(System.IO.Path.Combine(OutDir, "v_cal.txt"), sb.ToString(), new System.Text.UTF8Encoding(false)); Debug.Log(sb.ToString()); } static void Launch(Func make, string tag) { var old = GameObject.Find("__wl792verify"); if (old != null) UnityEngine.Object.DestroyImmediate(old); var go = new GameObject("__wl792verify"); var h = go.AddComponent(); h.tag_ = tag; h.StartCoroutine(make(h)); } // ───────────────────────────────────────────────────────────────────────── public class Host : MonoBehaviour { public string tag_ = "x"; // ── 유틸 ──────────────────────────────────────────────────────────── static double HP(Actor a) { var mi = typeof(Actor).GetMethod("Get_HP", BF); if (mi == null || a == null) return double.NaN; try { return Convert.ToDouble(mi.Invoke(a, null)); } catch { return double.NaN; } } static double ASpd(Actor me) { try { var fi = typeof(Actor).GetField("m_Stat", BF); var stat = fi != null ? fi.GetValue(me) : null; var gm = stat != null ? stat.GetType().GetMethod("Get_Stat", new[] { typeof(eStat) }) : null; if (gm != null) return Convert.ToDouble(gm.Invoke(stat, new object[] { eStat.FinalAttackSpeed })); } catch { } return 1.0; } static void SuppressPets() { foreach (var p in UnityEngine.Object.FindObjectsByType(FindObjectsInactive.Include, FindObjectsSortMode.None)) if (p.gameObject.activeSelf) p.gameObject.SetActive(false); } static MyActor Me() { return UnityEngine.Object.FindObjectsByType(FindObjectsInactive.Exclude, FindObjectsSortMode.None).FirstOrDefault(); } static string ClipName(Animator a) { if (a == null || a.layerCount == 0) return "(none)"; var ci = a.GetCurrentAnimatorClipInfo(0); return ci.Length > 0 && ci[0].clip != null ? ci[0].clip.name : "(none)"; } IEnumerator SetClass(StringBuilder sb, int classId) { var me = Me(); var pc = me as PCActor; if (classId > 0 && pc != null) { var data = table_classconfig.Ins != null ? table_classconfig.Ins.Get_Data_orNull(classId) : null; if (data == null) { sb.AppendLine("!! class data null " + classId); yield break; } pc.Change_Class(data, true); yield return new WaitForSeconds(1.5f); } // 상시 규칙 — 펫 금지 · 무적 유지 SuppressPets(); if (me != null) me.Immortal(1); yield return null; } IEnumerator Cap(string name) { yield return new WaitForEndOfFrame(); Texture2D tex = null; try { tex = ScreenCapture.CaptureScreenshotAsTexture(); } catch { } if (tex != null) { System.IO.Directory.CreateDirectory(OutDir); System.IO.File.WriteAllBytes(System.IO.Path.Combine(OutDir, name + ".png"), tex.EncodeToPNG()); UnityEngine.Object.Destroy(tex); } } // ── 배치 1회를 재고 한 줄로 ───────────────────────────────────────── static string Row(string label, WeaponTrailDriver.MappedPlacement p) { if (p.kind == SwingKind.Stab) { float axErr = Vector3.Angle(p.stabAxisTarget, p.stabAxisPlaced); float lenRatio = p.stabLenTarget > 1e-4f ? p.stabLenPlaced / p.stabLenTarget : 0f; float startOff = Vector3.Distance(p.stabStartTarget, p.stabStartPlaced); return string.Format( "{0,-14} | 찌르기 {1,-20} 축오차 {2,5:F1}° 길이비 {3,5:F2} 시작오프 {4,5:F3} m 스케일 {5:F3} 지연 {6,5:F1} ms {7}", label, p.effect, axErr, lenRatio, startOff, p.scale, p.delayMs, p.fromCache ? "[캐시]" : "[실측]"); } else { float nErr = Vector3.Angle(p.targetNormal, p.arcNormal); float bErr = Vector3.Angle(p.targetBisector, p.arcBisector); float rRatio = p.tipRadius > 1e-4f ? p.arcRadius / p.tipRadius : 0f; float cOff = Vector3.Distance(p.targetPivot, p.arcCenter); return string.Format( "{0,-14} | 휘두르기 {1,-18} 법선 {2,5:F1}° 볼록 {3,5:F1}° 반지름비 {4,5:F2} 중심 {5,5:F3} m " + "스윕(스윙/메시) {6,5:F0}/{7,5:F0}° 스케일 {8:F3} 지연 {9,5:F1} ms {10}", label, p.effect, nErr, bErr, rRatio, cOff, p.sweepSwing, p.sweepMesh, p.scale, p.delayMs, p.fromCache ? "[캐시]" : "[실측]"); } } /// 공격 1회를 돌리고, 배치가 일어나면 그 시점의 지표와 캡처를 남긴다. IEnumerator OneAttack(StringBuilder sb, string label, bool capture) { var me = Me(); if (me == null) yield break; var anim = me.GetComponentInChildren(); int before = WeaponTrailDriver.MappedSpawnCount; me.Play_Attack(0, (float)ASpd(me)); float t0 = Time.time; bool shot = false; while (Time.time - t0 < 1.1f) { yield return null; if (WeaponTrailDriver.MappedSpawnCount > before) { var p = WeaponTrailDriver.LastPlacement; sb.AppendLine(" " + Row(label + " " + ClipName(anim), p)); sb.AppendLine(" " + WeaponTrailDriver.LastMappedInfo); before = WeaponTrailDriver.MappedSpawnCount; if (capture && !shot) { // 배치 직후 이펙트가 가장 잘 보이는 프레임 근처에서 한 장 yield return new WaitForSeconds(0.06f); yield return Cap(tag_ + "_" + label.Replace(" ", "").Replace("/", "-")); shot = true; } } } } // ── ① 방향 × 클립 ─────────────────────────────────────────────────── public IEnumerator Co_Dirs(int classId) { var sb = new StringBuilder(); sb.AppendLine("# WL #792 방향별 정합 검증 class=" + classId + " " + DateTime.Now.ToString("HH:mm:ss")); yield return SetClass(sb, classId); var me = Me(); if (me == null) { Done(sb); yield break; } string[] dn = { "N", "E", "S", "W" }; float[] dy = { 0f, 90f, 180f, 270f }; for (int d = 0; d < 4; d++) { sb.AppendLine("── 방향 " + dn[d] + " (yaw " + dy[d] + "°)"); for (int combo = 1; combo <= 3; combo++) { me.transform.rotation = Quaternion.Euler(0f, dy[d], 0f); yield return null; yield return OneAttack(sb, dn[d] + "/A" + combo, true); yield return new WaitForSeconds(0.35f); } yield return new WaitForSeconds(0.5f); } sb.AppendLine(); sb.AppendLine("판정 목표 — 법선/볼록 ≤ 10° · 반지름비 0.9~1.1 · 중심 ≤ 0.10 m · 지연 ≤ 1 프레임(≈16.7 ms)"); sb.AppendLine("찌르기 목표 — 축 오차 ≤ 10° · 길이비 0.9~1.1"); Done(sb); } // ── ①-b 클립 전체 칼끝 궤적 추적 (스윙 창이 맞는지 보는 진단) ────── // WeaponTrailDriver 의 궤적 샘플은 ShowEffect 이벤트부터 swingWindowNormalized 동안만 모인다. // 그 창이 실제 스윙 구간을 못 잡으면(예: 9 샘플 · 칼끝 이동 9 cm) 캘리브레이션이 무의미해진다. // 여기서는 창과 무관하게 **클립 전체**를 매 프레임 훑어 각속도 프로파일을 낸다. public IEnumerator Co_Trace(int classId) { var sb = new StringBuilder(); sb.AppendLine("# WL #792 클립 전체 칼끝 궤적 class=" + classId + " " + DateTime.Now.ToString("HH:mm:ss")); yield return SetClass(sb, classId); var me = Me(); if (me == null) { Done(sb); yield break; } var anim = me.GetComponentInChildren(); for (int round = 0; round < 9; round++) { var drv = me.GetComponent(); if (drv == null || drv.ActiveSlotCount == 0) { // 드라이버는 첫 ShowEffect 때 붙는다 — 한 번 때려서 붙인다 me.Play_Attack(0, (float)ASpd(me)); yield return new WaitForSeconds(1.2f); continue; } int spawn0 = WeaponTrailDriver.MappedSpawnCount; string prevClip = ClipName(anim); me.Play_Attack(0, (float)ASpd(me)); // 🔴 클립이 **처음부터** 도는 순간을 잡아야 한다. Play_Attack 직후 한두 프레임은 // 아직 이전 상태이거나 전이 블렌드 중이라, 그때의 칼끝 이동은 블렌드가 만든 가짜 점프다 // (1차 추적에서 이 때문에 Attack3 에 1.0 m 짜리 허위 스텝이 잡혔다). float tw = Time.time; string clip0 = null; while (Time.time - tw < 1.0f) { yield return null; var s2 = anim.GetCurrentAnimatorStateInfo(0); string c2 = ClipName(anim); if (!anim.IsInTransition(0) && c2 != "(none)" && (s2.normalizedTime % 1f) < 0.12f) { clip0 = c2; break; } } if (clip0 == null) { yield return new WaitForSeconds(0.5f); continue; } var rows = new List(); Vector3 prevTip = drv.GetTipWorld(0), prevDir = (drv.GetTipWorld(0) - drv.GetHiltWorld(0)).normalized; float t0 = Time.time; float pathLen = 0f, maxStep = 0f, totalAng = 0f; int eventFrame = -1, fi = 0; float ntPeak = 0f, stepPeak = 0f; while (Time.time - t0 < 2.0f) { yield return null; fi++; var st = anim.GetCurrentAnimatorStateInfo(0); float nt = st.normalizedTime % 1f; if (ClipName(anim) != clip0) break; if (nt >= 0.80f && st.normalizedTime >= 0.80f) break; // 콤보 창(0.70~0.84) 안에서 끊어야 다음 타로 이어진다 Vector3 tip = drv.GetTipWorld(0); Vector3 hilt = drv.GetHiltWorld(0); if (tip == Vector3.zero) continue; Vector3 dir = (tip - hilt).normalized; float step = Vector3.Distance(tip, prevTip); float ang = Vector3.Angle(prevDir, dir); pathLen += step; totalAng += ang; if (step > maxStep) { maxStep = step; } if (step > stepPeak) { stepPeak = step; ntPeak = nt; } if (WeaponTrailDriver.MappedSpawnCount > spawn0 && eventFrame < 0) eventFrame = fi; rows.Add(string.Format(" f{0,-3} nt={1,5:F3} 칼끝이동={2,6:F3} m 칼날각속={3,6:F2}°/f swing={4}", fi, nt, step, ang, drv.IsSwinging ? 1 : 0)); prevTip = tip; prevDir = dir; } sb.AppendLine(string.Format("── {0} 프레임 {1} · 칼끝 경로 {2:F3} m · 최대 스텝 {3:F3} m (nt={4:F3}) · 칼날 총 회전 {5:F1}°", clip0, fi, pathLen, maxStep, ntPeak, totalAng)); foreach (var r in rows) sb.AppendLine(r); yield return new WaitForSeconds(0.02f); // 콤보를 이어 붙여 Attack2/3 까지 훑는다 } Done(sb); } // ── ② 콤보 연속 ───────────────────────────────────────────────────── public IEnumerator Co_Combo(int classId) { var sb = new StringBuilder(); sb.AppendLine("# WL #792 콤보 연속 class=" + classId + " " + DateTime.Now.ToString("HH:mm:ss")); yield return SetClass(sb, classId); var me = Me(); if (me == null) { Done(sb); yield break; } var anim = me.GetComponentInChildren(); int before = WeaponTrailDriver.MappedSpawnCount; int shots = 0; float t0 = Time.time; me.Play_Attack(0, (float)ASpd(me)); while (Time.time - t0 < 6f && shots < 6) { yield return null; if (WeaponTrailDriver.MappedSpawnCount > before) { before = WeaponTrailDriver.MappedSpawnCount; var p = WeaponTrailDriver.LastPlacement; sb.AppendLine(" " + Row("combo" + shots + " " + ClipName(anim), p)); yield return new WaitForSeconds(0.05f); yield return Cap(tag_ + "_seq" + shots); shots++; } if (Time.time - t0 > 1.2f && shots < 6) { me.Play_Attack(0, (float)ASpd(me)); t0 = Time.time; } } sb.AppendLine("캡처 " + shots + "장"); Done(sb); } // ── ②-S 콤보 연속 · 지정 공격 속도 (#804 · 2026-09-07) ────────────────── // 런타임 공격 속도(FinalAttackSpeed 약 3.5)에서는 Attack3 스윙 창 안 샘플이 6~9 개뿐이라 // 캘리브레이션 품질이 낮다(실측 path 0.25 m · Stab 오판). 클립 원속(1.0)으로 돌려 샘플을 채운다. // 베이크된 캘리브레이션은 이벤트 시점에 캐릭터 로컬로 펼치므로 공격 속도와 무관하게 재사용된다. public IEnumerator Co_ComboSpeed(int classId, float speed) { var sb = new StringBuilder(); sb.AppendLine("# WL #804 콤보 연속(속도 " + speed.ToString("F2") + ") class=" + classId + " " + DateTime.Now.ToString("HH:mm:ss")); yield return SetClass(sb, classId); var me = Me(); if (me == null) { Done(sb); yield break; } var anim = me.GetComponentInChildren(); int before = WeaponTrailDriver.MappedSpawnCount; int shots = 0; float gap = 1.3f / Mathf.Max(speed, 0.1f); float t0 = Time.time; me.Play_Attack(0, speed); float tEnd = Time.time + 6f * gap + 3f; while (Time.time < tEnd && shots < 6) { yield return null; if (WeaponTrailDriver.MappedSpawnCount > before) { before = WeaponTrailDriver.MappedSpawnCount; var p = WeaponTrailDriver.LastPlacement; sb.AppendLine(" " + Row("comboS" + shots + " " + ClipName(anim), p)); yield return new WaitForSeconds(0.05f); yield return Cap(tag_ + "_seq" + shots); shots++; } if (Time.time - t0 > gap && shots < 6) { me.Play_Attack(0, speed); t0 = Time.time; } } sb.AppendLine("캡처 " + shots + "장"); Done(sb); } // ── #804 Attack3 한 클립을 프레임마다 추적 — nt · 칼끝 이동 · 스윙 창 on/off · 배치 스폰 ───── public IEnumerator Co_Trace3(int classId) { var sb = new StringBuilder(); sb.AppendLine("# WL #804 Attack3 프레임 추적 class=" + classId + " " + DateTime.Now.ToString("HH:mm:ss")); yield return SetClass(sb, classId); var me = Me(); if (me == null) { Done(sb); yield break; } var anim = me.GetComponentInChildren(); float aspd = (float)ASpd(me); sb.AppendLine("FinalAttackSpeed=" + aspd.ToString("F3") + " animator.speed=" + anim.speed.ToString("F3")); float t0 = Time.time; float tEnd = Time.time + 12f; me.Play_Attack(0, aspd); bool recorded = false; var rows = new List(); while (Time.time < tEnd && !recorded) { yield return null; if (Time.time - t0 > 1.2f) { me.Play_Attack(0, aspd); t0 = Time.time; } string c = ClipName(anim); if (!c.Contains("Attack3")) continue; // Attack3 진입 — 클립이 끝날 때까지 매 프레임 기록 var drv = me.GetComponent(); Vector3 prevTip = drv != null ? drv.GetTipWorld(0) : Vector3.zero; int spawn0 = WeaponTrailDriver.MappedSpawnCount; int fi = 0; float path = 0f; while (ClipName(anim).Contains("Attack3") && fi < 200) { var st = anim.GetCurrentAnimatorStateInfo(0); float nt = st.normalizedTime % 1f; Vector3 tip = drv != null ? drv.GetTipWorld(0) : Vector3.zero; float step = Vector3.Distance(tip, prevTip); if (fi > 0) path += step; rows.Add(string.Format(" f{0,-3} nt={1,5:F3} inTrans={2} speed={3:F2} tipStep={4:F3} path={5:F2} swing={6} spawned={7} localTip={8}", fi, nt, anim.IsInTransition(0) ? 1 : 0, anim.speed, step, path, drv != null && drv.IsSwinging ? 1 : 0, WeaponTrailDriver.MappedSpawnCount - spawn0, me.transform.InverseTransformPoint(tip).ToString("F2"))); prevTip = tip; fi++; yield return null; } recorded = true; } sb.AppendLine(recorded ? "Attack3 frames=" + rows.Count : "!! Attack3 not reached in 12 s"); foreach (var r in rows) sb.AppendLine(r); Done(sb); } // ── ③ 이동 중 공격 ────────────────────────────────────────────────── public IEnumerator Co_Moving(int classId) { var sb = new StringBuilder(); sb.AppendLine("# WL #792 이동 중 공격 class=" + classId + " " + DateTime.Now.ToString("HH:mm:ss")); yield return SetClass(sb, classId); var me = Me(); if (me == null) { Done(sb); yield break; } Vector3 p0 = me.transform.position; me.transform.rotation = Quaternion.Euler(0f, 45f, 0f); yield return OneAttack(sb, "move", true); sb.AppendLine(" 루트모션 이동 " + Vector3.Distance(p0, me.transform.position).ToString("F3") + " m"); Done(sb); } // ── ⑤ 회귀 — 3콤보 3히트 · 자동 교전 · 대쉬 ──────────────────────── public IEnumerator Co_Regress() { var sb = new StringBuilder(); sb.AppendLine("# WL #792 회귀 " + DateTime.Now.ToString("HH:mm:ss")); var me = Me(); if (me == null) { Done(sb); yield break; } SuppressPets(); me.Immortal(1); var anim = me.GetComponentInChildren(); // 자동 교전 그대로 두고 관찰만 한다(어그로 조작 없음) var clips = new List(); var hitTargets = new Dictionary(); int placements0 = WeaponTrailDriver.MappedSpawnCount; Vector3 p0 = me.transform.position; float maxMove = 0f; string last = ""; float t0 = Time.time; while (Time.time - t0 < 14f) { yield return null; string c = ClipName(anim); if (c != last && c.Contains("Attack")) { clips.Add(c); last = c; } else if (c != last) last = c; maxMove = Mathf.Max(maxMove, Vector3.Distance(p0, me.transform.position)); foreach (var m in UnityEngine.Object.FindObjectsByType(FindObjectsInactive.Exclude, FindObjectsSortMode.None)) { string k = m.GetInstanceID().ToString(); double hp = HP(m); if (double.IsNaN(hp)) continue; if (!hitTargets.ContainsKey(k)) hitTargets[k] = hp; } } int damaged = 0; foreach (var kv in hitTargets) foreach (var m in UnityEngine.Object.FindObjectsByType(FindObjectsInactive.Include, FindObjectsSortMode.None)) if (m.GetInstanceID().ToString() == kv.Key && HP(m) < kv.Value) { damaged++; break; } sb.AppendLine("자동 교전 14 s — 공격 클립 전이 " + clips.Count + "회"); var uniq = new HashSet(clips); sb.AppendLine(" 등장 클립: " + string.Join(", ", uniq)); sb.AppendLine(" 3콤보 성립(Attack1/2/3 모두 등장) = " + (uniq.Count >= 3)); sb.AppendLine(" #792 이펙트 배치 " + (WeaponTrailDriver.MappedSpawnCount - placements0) + "회"); sb.AppendLine(" HP 가 줄어든 몬스터 " + damaged + "체 (히트 판정 정상)"); sb.AppendLine(" 이동(대쉬 포함) 최대 변위 " + maxMove.ToString("F2") + " m"); sb.AppendLine(" " + Stats()); Done(sb); } // ── ④ 예산 ───────────────────────────────────────────────────────── public IEnumerator Co_Budget() { var sb = new StringBuilder(); sb.AppendLine("# WL #792 예산 " + DateTime.Now.ToString("HH:mm:ss")); var me = Me(); if (me == null) { Done(sb); yield break; } SuppressPets(); me.Immortal(1); yield return new WaitForSeconds(0.5f); sb.AppendLine("기준(공격 없음) " + Stats()); int before = WeaponTrailDriver.MappedSpawnCount; me.Play_Attack(0, (float)ASpd(me)); float t0 = Time.time; int maxSet = 0, maxBatch = 0, maxDraw = 0, maxTri = 0; while (Time.time - t0 < 1.2f) { yield return new WaitForEndOfFrame(); maxSet = Mathf.Max(maxSet, UnityEditor.UnityStats.setPassCalls); maxBatch = Mathf.Max(maxBatch, UnityEditor.UnityStats.batches); maxDraw = Mathf.Max(maxDraw, UnityEditor.UnityStats.drawCalls); maxTri = Mathf.Max(maxTri, UnityEditor.UnityStats.triangles); } sb.AppendLine(string.Format("스윙 중 최대 setPass={0} batches={1} draw={2} tris={3}", maxSet, maxBatch, maxDraw, maxTri)); sb.AppendLine("예산 — SetPass ≤150 · 배치 ≤300 · 드로우 ≤400 · 삼각형 ≤30만"); sb.AppendLine("배치 횟수 " + (WeaponTrailDriver.MappedSpawnCount - before)); // 살아 있는 이펙트 인스턴스의 파티클 수 int parts = 0; foreach (var name in new[] { "Effect_WLSwingArc", "Effect_WLStab" }) { foreach (var ps in UnityEngine.Object.FindObjectsByType(FindObjectsInactive.Exclude, FindObjectsSortMode.None)) { var root = ps.transform.root; if (root != null && root.name.StartsWith(name)) parts += ps.particleCount; } } sb.AppendLine("살아 있는 매핑 이펙트 파티클 합 " + parts + " (목표 ≤ 30)"); Done(sb); } static string Stats() { return "setPass=" + UnityEditor.UnityStats.setPassCalls + " batches=" + UnityEditor.UnityStats.batches + " draw=" + UnityEditor.UnityStats.drawCalls + " tris=" + UnityEditor.UnityStats.triangles; } void Done(StringBuilder sb) { System.IO.Directory.CreateDirectory(OutDir); System.IO.File.WriteAllText(System.IO.Path.Combine(OutDir, "v_" + tag_ + ".txt"), sb.ToString(), new System.Text.UTF8Encoding(false)); Debug.Log(sb.ToString()); var go = GameObject.Find("__wl792verify"); if (go != null) UnityEngine.Object.Destroy(go); } } }