Project_WL/AgentScripts/WL797_Probe.cs

320 lines
16 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
// 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);
}
}
}