// ───────────────────────────────────────────────────────────────────────────── // 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 { /// 히치 계측·완화 값. 에셋 = Assets/WL/Combat/Settings/Resources/WL/WLHitchSettings.asset [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; /// 설정 에셋(1회 로드 후 캐시). 없으면 null = 기능 전체 off. public static WLHitchSettings Instance { get { if (!s_lookupDone) { s_cached = Resources.Load(ResourcesPath); s_lookupDone = true; } return s_cached; } } /// 프로브·A/B 전용. 0 = 에셋의 enabled · 1 = 강제 on · -1 = 강제 off. public static int RuntimeOverride; /// 축 전체가 살아 있는가. false 면 러너조차 만들지 않는다(C8). public static bool Enabled { get { var i = Instance; if (i == null || RuntimeOverride < 0) return false; return RuntimeOverride > 0 || i.enabled; } } /// /// 프로브가 임시 에셋을 끼워 넣을 때 쓴다(실에셋은 읽기만). /// null 을 넣으면 캐시가 비워져 다음 접근에서 실에셋을 다시 로드한다. /// 🔴 런타임 게임 경로에서는 절대 부르지 않는다. /// 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; } }