320 lines
16 KiB
C#
320 lines
16 KiB
C#
|
|
// WL797_Probe.cs — PD #797 타격 임팩트 연출 실측 · 캡처 (Play 전용)
|
||
|
|
//
|
||
|
|
// unity command run_script --file AgentScripts/WL797_Probe.cs --entry WL797_Probe.Watch --args '[8.0,"combo"]'
|
||
|
|
// unity command run_script --file AgentScripts/WL797_Probe.cs --entry WL797_Probe.Result --args '[]'
|
||
|
|
// unity command run_script --file AgentScripts/WL797_Probe.cs --entry WL797_Probe.Budget --args '[4.0]'
|
||
|
|
// unity command run_script --file AgentScripts/WL797_Probe.cs --entry WL797_Probe.State --args '[]'
|
||
|
|
//
|
||
|
|
// 측정 항목
|
||
|
|
// · 타격 시각(훅 호출) vs 피드백 발동 시각(카메라 오프셋이 실제로 0 이 아닌 첫 프레임) — 지연 프레임 수
|
||
|
|
// · 스윙당 발동 횟수 (HitCount / FiredCount / ThrottledCount)
|
||
|
|
// · 히트스톱 실측 길이 (timeScale 이 1 미만인 연속 구간을 realtime 으로 잰다)
|
||
|
|
// · 카메라 오프셋 최대 크기(m)
|
||
|
|
// · 피격 몹 스케일 배율 최대치
|
||
|
|
// · 콘솔 에러는 get_console_logs 로 별도 확인
|
||
|
|
//
|
||
|
|
// 캡처: Screenshots_WL/feel/{tag}_{before|hit|p050|p100}.png
|
||
|
|
// "before" 는 매 프레임 1 장을 메모리에 덮어쓰며 유지하다가, 타격이 감지되면 직전 프레임 것을 저장한다.
|
||
|
|
// ⚠ 에셋(SO·머티리얼·프리팹)을 런타임에 고치지 않는다. A/B 는 WLHitFeel.RuntimeDisabled 만 토글한다.
|
||
|
|
using System;
|
||
|
|
using System.Collections;
|
||
|
|
using System.Collections.Generic;
|
||
|
|
using System.Text;
|
||
|
|
using UnityEngine;
|
||
|
|
using WL.Feel;
|
||
|
|
|
||
|
|
public static class WL797_Probe
|
||
|
|
{
|
||
|
|
const string OutDir = "Screenshots_WL/feel";
|
||
|
|
public static string Result_ = "(not run)";
|
||
|
|
|
||
|
|
public static object Result() { return Result_; }
|
||
|
|
|
||
|
|
/// <summary>현재 상태 스냅샷 — Play 진입 직후 배선 확인용.</summary>
|
||
|
|
public static object State()
|
||
|
|
{
|
||
|
|
var sb = new StringBuilder();
|
||
|
|
var st = WLHitFeel.Settings;
|
||
|
|
sb.AppendLine("isPlaying=" + Application.isPlaying + " timeScale=" + Time.timeScale);
|
||
|
|
sb.AppendLine("Settings=" + (st == null ? "NULL (에셋 없음 → 기능 비활성)" : "OK enabled=" + st.enabled
|
||
|
|
+ " mode=" + st.hitStopMode + " stopDur=" + st.hitStopDuration
|
||
|
|
+ " shakeAmp=" + st.shakeAmplitude + " punch=" + st.scalePunch
|
||
|
|
+ " flash=" + st.flashEnabled + " suppressLegacy=" + st.suppressLegacyShake));
|
||
|
|
var cam = Camera.main;
|
||
|
|
sb.AppendLine("Camera.main=" + (cam == null ? "NULL" : cam.name)
|
||
|
|
+ " parent=" + (cam != null && cam.transform.parent != null ? cam.transform.parent.name : "(none)"));
|
||
|
|
var sh = WLHitFeel.GetShaker(false);
|
||
|
|
sb.AppendLine("Shaker=" + (sh == null ? "not attached (첫 타격 때 자동 부착)" : "attached maxOffset=" + sh.MaxOffsetMagnitude));
|
||
|
|
sb.AppendLine("RuntimeDisabled=" + WLHitFeel.RuntimeDisabled);
|
||
|
|
sb.AppendLine("counts hit=" + WLHitFeel.HitCount + " fired=" + WLHitFeel.FiredCount + " throttled=" + WLHitFeel.ThrottledCount);
|
||
|
|
return sb.ToString();
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>seconds 동안 전투를 관찰하며 실측 + 첫 3 회 타격 캡처.</summary>
|
||
|
|
public static object Watch(float seconds, string tag)
|
||
|
|
{
|
||
|
|
return WatchN(seconds, tag, 3);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// maxShots = 0 이면 캡처를 하지 않는다 → 루프가 매 프레임 정확히 1 회 돌아
|
||
|
|
/// 히트스톱 길이를 프레임 단위로 정확히 잴 수 있다(캡처 대기가 측정을 오염시키지 않는다).
|
||
|
|
/// </summary>
|
||
|
|
public static object WatchN(float seconds, string tag, int maxShots)
|
||
|
|
{
|
||
|
|
if (!Application.isPlaying) return "not playing";
|
||
|
|
if (GameObject.Find("__wl797probe") != null) return "already running";
|
||
|
|
var h = new GameObject("__wl797probe").AddComponent<H>();
|
||
|
|
h.MaxShots = maxShots;
|
||
|
|
h.StartCoroutine(h.CoWatch(seconds, string.IsNullOrEmpty(tag) ? "combo" : tag));
|
||
|
|
return "started watch " + seconds + "s tag=" + tag + " maxShots=" + maxShots;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>ON/OFF 각 seconds 초씩 돌려 모바일 예산 증분을 잰다(에셋 무수정).</summary>
|
||
|
|
public static object Budget(float seconds)
|
||
|
|
{
|
||
|
|
if (!Application.isPlaying) return "not playing";
|
||
|
|
if (GameObject.Find("__wl797budget") != null) return "already running";
|
||
|
|
var h = new GameObject("__wl797budget").AddComponent<H>();
|
||
|
|
h.StartCoroutine(h.CoBudget(seconds));
|
||
|
|
return "started budget " + seconds + "s x2";
|
||
|
|
}
|
||
|
|
|
||
|
|
class Sample
|
||
|
|
{
|
||
|
|
public int frame; public float t; public float ts; public float camOff; public float scaleRatio;
|
||
|
|
public int hit, fired, thr;
|
||
|
|
}
|
||
|
|
|
||
|
|
class H : MonoBehaviour
|
||
|
|
{
|
||
|
|
public int MaxShots = 3;
|
||
|
|
|
||
|
|
// ── 관찰 + 캡처 ────────────────────────────────────────────────────
|
||
|
|
public IEnumerator CoWatch(float sec, string tag)
|
||
|
|
{
|
||
|
|
System.IO.Directory.CreateDirectory(OutDir);
|
||
|
|
WLHitFeel.ResetDiagnostics();
|
||
|
|
|
||
|
|
var log = new List<string>();
|
||
|
|
var samples = new List<Sample>();
|
||
|
|
var st = WLHitFeel.Settings;
|
||
|
|
log.Add("=== WL797 watch tag=" + tag + " sec=" + sec + " ===");
|
||
|
|
log.Add(st == null ? "Settings=NULL" :
|
||
|
|
"Settings enabled=" + st.enabled + " throttle=" + st.throttleSeconds
|
||
|
|
+ " stopDur=" + st.hitStopDuration + " stopKill=" + st.hitStopKillDuration
|
||
|
|
+ " stopScale=" + st.hitStopTimeScale + " mode=" + st.hitStopMode
|
||
|
|
+ " shakeAmp=" + st.shakeAmplitude + " shakeDur=" + st.shakeDuration
|
||
|
|
+ " punch=" + st.scalePunch + " punchDur=" + st.scalePunchDuration
|
||
|
|
+ " flash=" + st.flashEnabled + " suppressLegacy=" + st.suppressLegacyShake);
|
||
|
|
|
||
|
|
Texture2D prevShot = null;
|
||
|
|
int prevFired = WLHitFeel.FiredCount;
|
||
|
|
int shotsTaken = 0;
|
||
|
|
|
||
|
|
// 히트스톱 구간 측정
|
||
|
|
bool inStop = false; float stopT0 = 0f;
|
||
|
|
var stopLens = new List<float>();
|
||
|
|
|
||
|
|
// 피드백 지연 측정
|
||
|
|
int pendingFireFrame = -1; float pendingFireTime = 0f;
|
||
|
|
|
||
|
|
float maxCamOff = 0f, maxScaleRatio = 1f;
|
||
|
|
float t0 = Time.realtimeSinceStartup;
|
||
|
|
|
||
|
|
while (Time.realtimeSinceStartup - t0 < sec)
|
||
|
|
{
|
||
|
|
yield return new WaitForEndOfFrame();
|
||
|
|
|
||
|
|
var sh = WLHitFeel.GetShaker(false);
|
||
|
|
float camOff = sh != null ? sh.CurrentOffset.magnitude : 0f;
|
||
|
|
if (camOff > maxCamOff) maxCamOff = camOff;
|
||
|
|
|
||
|
|
float ratio = 1f;
|
||
|
|
if (WLHitFeel.LastVictimTf != null && WLHitFeel.LastVictimBaseScaleX > 0.0001f)
|
||
|
|
{
|
||
|
|
ratio = WLHitFeel.LastVictimTf.localScale.x / WLHitFeel.LastVictimBaseScaleX;
|
||
|
|
if (ratio > maxScaleRatio) maxScaleRatio = ratio;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 히트스톱 구간
|
||
|
|
if (!inStop && Time.timeScale < 0.9f) { inStop = true; stopT0 = Time.realtimeSinceStartup; }
|
||
|
|
else if (inStop && Time.timeScale >= 0.9f)
|
||
|
|
{
|
||
|
|
inStop = false;
|
||
|
|
stopLens.Add(Time.realtimeSinceStartup - stopT0);
|
||
|
|
}
|
||
|
|
|
||
|
|
samples.Add(new Sample
|
||
|
|
{
|
||
|
|
frame = Time.frameCount,
|
||
|
|
t = Time.realtimeSinceStartup - t0,
|
||
|
|
ts = Time.timeScale,
|
||
|
|
camOff = camOff,
|
||
|
|
scaleRatio = ratio,
|
||
|
|
hit = WLHitFeel.HitCount,
|
||
|
|
fired = WLHitFeel.FiredCount,
|
||
|
|
thr = WLHitFeel.ThrottledCount,
|
||
|
|
});
|
||
|
|
|
||
|
|
// ── 타격 감지
|
||
|
|
int fired = WLHitFeel.FiredCount;
|
||
|
|
if (fired != prevFired)
|
||
|
|
{
|
||
|
|
prevFired = fired;
|
||
|
|
pendingFireFrame = WLHitFeel.LastFiredFrame;
|
||
|
|
pendingFireTime = Time.realtimeSinceStartup;
|
||
|
|
log.Add(string.Format("[FIRE #{0}] frame={1} t={2:F3} kill={3} camOff={4:F4} ts={5:F3}",
|
||
|
|
fired, WLHitFeel.LastFiredFrame, Time.realtimeSinceStartup - t0,
|
||
|
|
WLHitFeel.LastWasKill, camOff, Time.timeScale));
|
||
|
|
|
||
|
|
if (shotsTaken < MaxShots)
|
||
|
|
{
|
||
|
|
shotsTaken++;
|
||
|
|
string p = tag + "_" + shotsTaken;
|
||
|
|
// 직전 프레임(유지해 둔 것)
|
||
|
|
if (prevShot != null) SaveTex(prevShot, p + "_0before", log);
|
||
|
|
// 타격 프레임
|
||
|
|
yield return Cap(p + "_1hit", log);
|
||
|
|
// +0.05 s / +0.10 s (realtime — 히트스톱 중에도 흐른다)
|
||
|
|
yield return WaitReal(0.05f);
|
||
|
|
yield return Cap(p + "_2p050", log);
|
||
|
|
yield return WaitReal(0.05f);
|
||
|
|
yield return Cap(p + "_3p100", log);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else if (pendingFireFrame >= 0 && camOff > 0.0001f)
|
||
|
|
{
|
||
|
|
log.Add(string.Format(" → 피드백 가시화 frame={0} (타격 frame={1}, 지연 {2} 프레임 · {3:F1} ms)",
|
||
|
|
Time.frameCount, pendingFireFrame, Time.frameCount - pendingFireFrame,
|
||
|
|
(Time.realtimeSinceStartup - pendingFireTime) * 1000f));
|
||
|
|
pendingFireFrame = -1;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 다음 루프의 "직전" 후보로 이번 프레임을 유지
|
||
|
|
if (shotsTaken < MaxShots)
|
||
|
|
{
|
||
|
|
if (prevShot != null) UnityEngine.Object.Destroy(prevShot);
|
||
|
|
prevShot = TryCapture();
|
||
|
|
}
|
||
|
|
else if (prevShot != null) { UnityEngine.Object.Destroy(prevShot); prevShot = null; }
|
||
|
|
}
|
||
|
|
|
||
|
|
if (prevShot != null) UnityEngine.Object.Destroy(prevShot);
|
||
|
|
|
||
|
|
// ── 요약
|
||
|
|
var sb = new StringBuilder();
|
||
|
|
foreach (var l in log) sb.AppendLine(l);
|
||
|
|
sb.AppendLine("── 요약 ──");
|
||
|
|
sb.AppendLine("HitCount=" + WLHitFeel.HitCount + " FiredCount=" + WLHitFeel.FiredCount
|
||
|
|
+ " Throttled=" + WLHitFeel.ThrottledCount
|
||
|
|
+ " (스윙당 1회면 Fired ≒ 스윙 수, Throttled = 같은 스윙의 나머지 몹)");
|
||
|
|
sb.AppendLine("카메라 오프셋 최대 = " + maxCamOff.ToString("F4") + " m");
|
||
|
|
var shk = WLHitFeel.GetShaker(false);
|
||
|
|
sb.AppendLine("Shaker.MaxOffsetMagnitude = " + (shk != null ? shk.MaxOffsetMagnitude.ToString("F4") : "(none)") + " m");
|
||
|
|
sb.AppendLine("몹 스케일 배율 최대 = " + maxScaleRatio.ToString("F3") + " x");
|
||
|
|
if (stopLens.Count > 0)
|
||
|
|
{
|
||
|
|
float sum = 0f, mx = 0f;
|
||
|
|
foreach (var v in stopLens) { sum += v; if (v > mx) mx = v; }
|
||
|
|
sb.AppendLine("히트스톱 실측 n=" + stopLens.Count + " 평균=" + (sum / stopLens.Count).ToString("F4")
|
||
|
|
+ "s 최대=" + mx.ToString("F4") + "s");
|
||
|
|
}
|
||
|
|
else sb.AppendLine("히트스톱 실측 = 관측 없음");
|
||
|
|
sb.AppendLine("WLHitFeel.LastHitStopMeasured = " + WLHitFeel.LastHitStopMeasured.ToString("F4") + "s (-1 = 외부 개입으로 복원 생략)");
|
||
|
|
|
||
|
|
// 프레임 표 (타격 근처만)
|
||
|
|
sb.AppendLine("── 프레임 표 (timeScale<1 또는 camOff>0 또는 scale≠1) ──");
|
||
|
|
int printed = 0;
|
||
|
|
foreach (var s in samples)
|
||
|
|
{
|
||
|
|
if (printed >= 160) { sb.AppendLine(" ...(생략)"); break; }
|
||
|
|
if (s.ts < 0.999f || s.camOff > 0.0001f || Mathf.Abs(s.scaleRatio - 1f) > 0.002f)
|
||
|
|
{
|
||
|
|
sb.AppendLine(string.Format(" f={0} t={1:F3} ts={2:F3} camOff={3:F4} scale={4:F3} h/f/t={5}/{6}/{7}",
|
||
|
|
s.frame, s.t, s.ts, s.camOff, s.scaleRatio, s.hit, s.fired, s.thr));
|
||
|
|
printed++;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
Result_ = sb.ToString();
|
||
|
|
System.IO.File.WriteAllText(System.IO.Path.Combine(OutDir, "watch_" + tag + ".txt"), Result_);
|
||
|
|
Debug.Log("[WL797 watch]\n" + Result_);
|
||
|
|
Destroy(gameObject);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── 예산 A/B ───────────────────────────────────────────────────────
|
||
|
|
public IEnumerator CoBudget(float sec)
|
||
|
|
{
|
||
|
|
var sb = new StringBuilder();
|
||
|
|
bool orig = WLHitFeel.RuntimeDisabled;
|
||
|
|
for (int pass = 0; pass < 2; pass++)
|
||
|
|
{
|
||
|
|
bool on = pass == 0;
|
||
|
|
WLHitFeel.RuntimeDisabled = !on; // 에셋은 건드리지 않는다
|
||
|
|
yield return WaitReal(0.4f); // 안정화
|
||
|
|
|
||
|
|
int mSet = 0, mBat = 0, mDraw = 0, mTri = 0, n = 0;
|
||
|
|
long aSet = 0, aBat = 0, aDraw = 0, aTri = 0;
|
||
|
|
float t0 = Time.realtimeSinceStartup;
|
||
|
|
while (Time.realtimeSinceStartup - t0 < sec)
|
||
|
|
{
|
||
|
|
yield return new WaitForEndOfFrame();
|
||
|
|
int sp = UnityEditor.UnityStats.setPassCalls, b = UnityEditor.UnityStats.batches;
|
||
|
|
int d = UnityEditor.UnityStats.drawCalls, tr = UnityEditor.UnityStats.triangles;
|
||
|
|
if (sp > mSet) mSet = sp; if (b > mBat) mBat = b;
|
||
|
|
if (d > mDraw) mDraw = d; if (tr > mTri) mTri = tr;
|
||
|
|
aSet += sp; aBat += b; aDraw += d; aTri += tr; n++;
|
||
|
|
yield return null;
|
||
|
|
}
|
||
|
|
sb.AppendLine((on ? "HitFeel ON " : "HitFeel OFF ") +
|
||
|
|
" max setPass=" + mSet + " batches=" + mBat + " draw=" + mDraw + " tris=" + mTri +
|
||
|
|
" | avg setPass=" + (n > 0 ? aSet / n : 0) + " batches=" + (n > 0 ? aBat / n : 0) +
|
||
|
|
" draw=" + (n > 0 ? aDraw / n : 0) + " tris=" + (n > 0 ? aTri / n : 0) + " frames=" + n);
|
||
|
|
}
|
||
|
|
WLHitFeel.RuntimeDisabled = orig;
|
||
|
|
sb.AppendLine("RuntimeDisabled 원복 = " + WLHitFeel.RuntimeDisabled);
|
||
|
|
sb.AppendLine("예산 상한: SetPass ≤150 · 배치 ≤300 · 드로우 ≤400 · 삼각형 ≤300000");
|
||
|
|
Result_ = sb.ToString();
|
||
|
|
System.IO.Directory.CreateDirectory(OutDir);
|
||
|
|
System.IO.File.WriteAllText(System.IO.Path.Combine(OutDir, "budget.txt"), Result_);
|
||
|
|
Debug.Log("[WL797 예산]\n" + Result_);
|
||
|
|
Destroy(gameObject);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── 유틸 ───────────────────────────────────────────────────────────
|
||
|
|
static IEnumerator WaitReal(float s)
|
||
|
|
{
|
||
|
|
float t0 = Time.realtimeSinceStartup;
|
||
|
|
while (Time.realtimeSinceStartup - t0 < s) yield return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
static Texture2D TryCapture()
|
||
|
|
{
|
||
|
|
try { return ScreenCapture.CaptureScreenshotAsTexture(); }
|
||
|
|
catch { return null; }
|
||
|
|
}
|
||
|
|
|
||
|
|
static void SaveTex(Texture2D tex, string name, List<string> log)
|
||
|
|
{
|
||
|
|
if (tex == null) return;
|
||
|
|
try
|
||
|
|
{
|
||
|
|
System.IO.Directory.CreateDirectory(OutDir);
|
||
|
|
var path = System.IO.Path.Combine(OutDir, name + ".png");
|
||
|
|
System.IO.File.WriteAllBytes(path, tex.EncodeToPNG());
|
||
|
|
log.Add(" CAP " + path);
|
||
|
|
}
|
||
|
|
catch (Exception e) { log.Add(" CAP FAIL " + name + " " + e.GetType().Name); }
|
||
|
|
}
|
||
|
|
|
||
|
|
static IEnumerator Cap(string name, List<string> log)
|
||
|
|
{
|
||
|
|
yield return new WaitForEndOfFrame();
|
||
|
|
var tex = TryCapture();
|
||
|
|
SaveTex(tex, name, log);
|
||
|
|
if (tex != null) UnityEngine.Object.Destroy(tex);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|