Project_WL/Assets/WL/Tools/WLHitchSettings.cs

152 lines
9.4 KiB
C#

// ─────────────────────────────────────────────────────────────────────────────
// WLHitchSettings.cs — 히치(버벅임) 계측 · 1차 완화 값의 단일 출처 (SOT · C45)
//
// PD 지시 #813(「게임이 자꾸 버벅대는 듯 멈추는 렉」) · 발주서 WL-811q §1-2 · §1-3 (2026-09-10)
//
// ■ 왜 이 SO 하나에 「레코더 + 워밍」이 같이 있나
// 둘 다 811q 히치 축이고, QA·PD 가 A/B 를 할 때 **한 에셋만 열면 되게** 하려는 것이다.
// 스폰 분산(`rearmPerFrame`)만은 재무장 코드가 있는 `WLBossArenaSettings` 에 뒀다(같은 기능 옆).
//
// ■ 근거(실측)
// · Q6 `qa/W2-6_재채점_v4.md` 7-3 = 히치 32프레임/3275 · 최악 **637.1 ms** · 최저 fps 1.6 (군중 구간)
// · Q8 `qa/W2-8_재채점_v6.md` 7-3 = 히치 6프레임/3520 · 최악 **1003.2 ms**(에디터 스톨 혼입 「가능성」)
// · Q12 `qa/W2-12_회귀.md` 7-3 = 히치 8프레임/3573 · 최악 **263.9 ms**
// · 원본 `Assets/ResWork/Table/Export/EffectList.json` = 90행 · **프리로드 인스턴스 합 514**
// (이펙트 312 · 투사체 202) — `InGameInfo.Start()` 가 맵 로드 때 한꺼번에 만든다.
// 🔴 그런데 WL 이 쓰는 원본 풀 이펙트 이름 **22종**(검기 21 + `BoomHit`)은 이 표에 **없다**
// → 전투 도중 첫 사용에서 Addressables 로드 + Instantiate 가 그대로 터진다. 그것이 warmPoolPrefabs 다.
//
// ■ C8 롤백
// 에셋이 없거나 `enabled = 0` → 러너 0 · 레코더 0 · 워밍 0 = 100 % 기존 동작.
// `recorderEnabled = 0` → 계측만 끈다 / `warmOnMapEnter = 0` → 워밍만 끈다.
//
// 🔴 어셈블리 주의: Assets/WL/ 에 .asmdef 를 만들지 말 것(Actor/InGameInfo 가 Assembly-CSharp 에 있다).
// ─────────────────────────────────────────────────────────────────────────────
using UnityEngine;
namespace WL.Tools
{
/// <summary>히치 계측·완화 값. 에셋 = Assets/WL/Combat/Settings/Resources/WL/WLHitchSettings.asset</summary>
[CreateAssetMenu(fileName = "WLHitchSettings", menuName = "WL/Hitch Settings", order = 327)]
public sealed class WLHitchSettings : ScriptableObject
{
public const string ResourcesPath = "WL/WLHitchSettings";
static WLHitchSettings s_cached;
static bool s_lookupDone;
/// <summary>설정 에셋(1회 로드 후 캐시). 없으면 null = 기능 전체 off.</summary>
public static WLHitchSettings Instance
{
get
{
if (!s_lookupDone) { s_cached = Resources.Load<WLHitchSettings>(ResourcesPath); s_lookupDone = true; }
return s_cached;
}
}
/// <summary>프로브·A/B 전용. 0 = 에셋의 enabled · 1 = 강제 on · -1 = 강제 off.</summary>
public static int RuntimeOverride;
/// <summary>축 전체가 살아 있는가. false 면 러너조차 만들지 않는다(C8).</summary>
public static bool Enabled
{
get
{
var i = Instance;
if (i == null || RuntimeOverride < 0) return false;
return RuntimeOverride > 0 || i.enabled;
}
}
/// <summary>
/// 프로브가 임시 에셋을 끼워 넣을 때 쓴다(실에셋은 읽기만).
/// null 을 넣으면 캐시가 비워져 다음 접근에서 실에셋을 다시 로드한다.
/// 🔴 런타임 게임 경로에서는 절대 부르지 않는다.
/// </summary>
public static void OverrideInstanceForProbe(WLHitchSettings st) { s_cached = st; s_lookupDone = st != null; }
// ═══════════════════════════════════════════ 공통
[Header("공통 (C8)")]
[Tooltip("이 축 전체 on/off. 끄면 러너 오브젝트도 만들지 않는다 = 흔적 0.")]
public bool enabled = true;
[Tooltip("콘솔 로그를 낸다. 🔴 문자열 결합은 이 값이 켜졌을 때만 일어난다(프레임당 할당 0 유지).")]
public bool verboseLog = false;
// ═══════════════════════════════════════════ ① 히치 레코더 (발주서 §1-3)
[Header("① 히치 레코더 (Screenshots_WL/WL811q/hitch_*.txt · QA 가 읽는다)")]
[Tooltip("프레임 시간을 재서 링버퍼에 기록한다. 끄면 Update 가 즉시 반환한다.")]
public bool recorderEnabled = true;
[Tooltip("🔴 이 시간(ms)을 넘는 프레임을 「히치」로 본다. 발주서 §1-3 기본 50 ms. "
+ "60 fps = 16.7 ms 이므로 50 ms 는 약 3프레임분이다. QA 채점 기준과 맞추려면 여기만 바꾼다.")]
public float hitchMilliseconds = 50f;
[Tooltip("링버퍼 칸 수(발주서 §1-3 = 64). 게임 수치가 아니라 버퍼 크기다. 넘치면 오래된 것부터 덮어쓴다.")]
public int ringCapacity = 64;
[Tooltip("첫 N 프레임은 무시한다 — 맵 로드 직후 프레임은 항상 길다(로딩 화면 구간).")]
public int warmupFramesIgnored = 30;
[Tooltip("GC.GetTotalMemory(false) 로 힙 델타(KB)를 같이 적는다. 끄면 GC 열이 0 으로 나온다.")]
public bool sampleManagedHeap = true;
[Tooltip("히치가 이 개수만큼 쌓일 때마다 파일을 쓴다(0 = 자동 기록 없음 · 종료·사망·런종료에만).")]
public int autoWriteEveryHitches = 16;
[Tooltip("PC 사망(DeathFlow.Died) 때 파일을 쓴다 — Q8 최악 831 ms 가 사망 구간이었다.")]
public bool writeOnDeath = true;
[Tooltip("런 종료(RunEvents.RunEnded) 때 파일을 쓴다 — 한 판이 통째로 한 파일이 된다.")]
public bool writeOnRunEnd = true;
[Tooltip("파일을 쓸 때 콘솔에 1줄 요약을 남긴다(verboseLog 와 무관 · QA 가 이 줄만 봐도 된다).")]
public bool consoleLineOnWrite = true;
[Tooltip("기록 폴더(프로젝트 루트 기준 · 빌드는 persistentDataPath 기준). Assets 밖 = 임포트 0.")]
public string hitchDir = "Screenshots_WL/WL811q";
// ═══════════════════════════════════════════ ② 이펙트 사전 워밍 (발주서 §1-2 ⓑ)
[Header("② 이펙트 사전 워밍 (맵 진입 = 로딩 구간으로 비용을 앞당긴다)")]
[Tooltip("🔴 맵 진입(메인 PC 교체 또는 맵 ID 변경 · AutoCombatOnEnter 와 같은 경계)에서 워밍을 시작한다. "
+ "끄면 811i 는 예전대로 **첫 AttackStarted** 에 프리로드한다(= 첫 스킬 직전에 로드가 몰린다).")]
public bool warmOnMapEnter = true;
[Tooltip("맵 경계 감시 주기(초 · unscaled). 폴링 자체는 할당 0.")]
public float warmPollSeconds = 0.25f;
[Tooltip("경계를 본 뒤 이만큼(초) 기다렸다가 워밍을 시작한다 — 맵 로드 직후의 원본 프리로드 514개와 겹치지 않게.")]
public float warmStartDelaySeconds = 1.5f;
[Tooltip("🔴 한 프레임에 요청할 811i 행 수(3단 = 프리팹 최대 3개/행). 0 = 한 프레임에 전부(= 워밍이 스스로 히치가 된다).")]
public int warmRowsPerFrame = 1;
[Tooltip("🔴 한 프레임에 워밍할 원본 풀 이펙트 이름 수. 0 = 한 프레임에 전부.")]
public int warmPoolPerFrame = 1;
[Tooltip("원본 풀(InGameInfo.Show_Effect) 로 미리 1개씩 만들어 둘 이펙트 이름. "
+ "🔴 기본값 22종 = WL 이 쓰는데 EffectList.json 프리로드 표에 **없는** 것 전부(실측 · 검기 21 + BoomHit). "
+ "🔴 여기에 넣는 이름은 프리팹에 TurnOff_GO 가 있어야 한다(없으면 원본 Show_Effect 가 NRE) — "
+ "기본 22종은 디스크에서 전수 확인했다.")]
public string[] warmPoolPrefabs = new string[]
{
"Effect_Slash_10101_1", "Effect_Slash_10101_2", "Effect_Slash_10101_3",
"Effect_Slash_10102_1", "Effect_Slash_10102_2", "Effect_Slash_10102_3", "Effect_Slash_10102_4",
"Effect_Slash_10201_1", "Effect_Slash_10201_2", "Effect_Slash_10201_3",
"Effect_Slash_10202_1", "Effect_Slash_10202_2", "Effect_Slash_10202_3", "Effect_Slash_10202_4",
"Effect_Slash_10501_1", "Effect_Slash_10501_2", "Effect_Slash_10501_3",
"Effect_Slash_10502_1", "Effect_Slash_10502_2",
"Effect_WLStab", "Effect_WLSwingArc", "BoomHit"
};
[Tooltip("워밍 인스턴스를 놓는 좌표(원본 InGameInfo.Start 의 사전 로드와 같은 곳 = Vector3.one * 이 값). "
+ "TurnOff_GO 가 수명 뒤 스스로 꺼서 풀로 돌아간다.")]
public float warmFarDistance = 100000f;
[Tooltip("🔴 811i 스킬 이펙트를 클래스 무관 **전 행** 워밍한다. 끄면 메인 PC 클래스 행만 워밍한다(로드량 1/3).")]
public bool warmAllClasses = false;
}
}