Project_WL/AgentScripts/WL792_Verify.cs

486 lines
24 KiB
C#
Raw Normal View History

WL 리뉴얼 세션 2026-09-06~07 (#760~#800) — 전투·배경·UI·타격감 (조직 PD 로그 #746~#800 · 대화로그 §14~§24) - 전투: Knight@Attack1~3_S 3콤보(4 컨트롤러·클래스별 서브클립) · 타겟팅 4규칙(정면 우선·재타겟·즉시 공격·어그로 금지 · WLTargetingSettings) · 대쉬 후 공격(Stander@Chase_Start · DashDriver · 5 m · 사정거리+0.8 · 근거리 적 우선) · 공격 이동 FrameTable(발 접지 실측 표 · AttackRootMotion) · 충돌 반경/투사체 0.3 배(WLCollisionTuning) · 무적(임시)·펫 금지(WLGameplaySettings) - 검기: NamuFX Slash_B 배리언트 Effect_WLSwingArc 원 피팅 정합 배치 + 캘리브레이션(SlashArcMeasure · SlashTrailSettings) · 찌르기 Effect_WLStab 대기 · 램프 리본(BladeTrail · WL_BladeRibbon.shader · T_WL_BladeRibbonRamp) 보존(drawRibbon 0) - 타격감: Assets/WL/Feel(WLHitFeel · 히트스톱 0.03 · 셰이크 0.10 m · 몹 펀치 1.12 · Actor.Get_Damage 훅 1줄 · 원본 RealCamera 셰이크 결함 대체) - 배경/맵: LMHPOLY Demo_01~10 → WL_Nature01~10(프리팹·씬·NavMesh·스포너·BattleMapConfig) · 물(ToonWaterU) · 포스트 블룸 0.9/0.3 · 잔디(BruteForce·드레싱) 제거 · 마젠타 머티리얼 URP 변환 - UI: 세로 HUD(WL_HUD · 하단 5메뉴 폭 전체 · 채팅/전투 패드 숨김 · WLIngameUiOverride) · Title/TitleInfo 1080×1920 Expand + 배경 높이 fit(WLBackgroundFit) · 로딩 SortOrder_5(WLRawImageAspectSync · 초점표 8장) · Loading1~8 ASTC 6×6 - 도구: AgentScripts/*(LightProbe · WL_MapSwitch · WL760~WL800 프로브/집행/검증 · 상단 사용법 주석) · 에디터 락 프로토콜 파일(staging) - 제외(별도 커밋 예정): Assets/LMHPOLY(703 MB) · Assets/Feel(422 MB) · Assets/Shinabro(300 MB) — 에셋 스토어/구 WL 팩 원본 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 23:24:25 +00:00
// 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 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); }
/// <summary>런타임 캘리브레이션·기하 캐시를 비운다(설정/알고리즘을 바꾼 뒤 재실측).</summary>
public static void Reset()
{
WeaponTrailDriver.ClearMappedCache();
SlashTrailSettings.ClearCache();
Debug.Log("[WL #792] 런타임 캐시 초기화 — 다음 스윙부터 다시 실측한다");
}
/// <summary>런타임 캘리브레이션 캐시를 그대로 덤프한다(베이크 전 확인용).</summary>
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<Host, IEnumerator> make, string tag)
{
var old = GameObject.Find("__wl792verify");
if (old != null) UnityEngine.Object.DestroyImmediate(old);
var go = new GameObject("__wl792verify");
var h = go.AddComponent<Host>();
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<PetActor>(FindObjectsInactive.Include, FindObjectsSortMode.None))
if (p.gameObject.activeSelf) p.gameObject.SetActive(false);
}
static MyActor Me()
{
return UnityEngine.Object.FindObjectsByType<MyActor>(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 ? "[캐시]" : "[실측]");
}
}
/// <summary>공격 1회를 돌리고, 배치가 일어나면 그 시점의 지표와 캡처를 남긴다.</summary>
IEnumerator OneAttack(StringBuilder sb, string label, bool capture)
{
var me = Me();
if (me == null) yield break;
var anim = me.GetComponentInChildren<Animator>();
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<Animator>();
for (int round = 0; round < 9; round++)
{
var drv = me.GetComponent<WeaponTrailDriver>();
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<string>();
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<Animator>();
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);
}
// ── ③ 이동 중 공격 ──────────────────────────────────────────────────
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<Animator>();
// 자동 교전 그대로 두고 관찰만 한다(어그로 조작 없음)
var clips = new List<string>();
var hitTargets = new Dictionary<string, double>();
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<MobActor>(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<MobActor>(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<string>(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<ParticleSystem>(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);
}
}
}