// ───────────────────────────────────────────────────────────────────────────── // ImpactRunner.cs — 811gh 전용 틱 러너(숨은 오브젝트 1개 · DontDestroyOnLoad · 코루틴 없음 · GC 0) // // PD 지시 #813 · 발주서 WL-811gh (2026-09-09) // // ■ 왜 WLReactionRunner 에 얹지 않았나 // 기존 러너(__WLHitFeel · __WLTimeScaleArbiter · __WLReaction · LevelUpBurstRunner · LootBurstRunner)와 같은 패턴을 // 하나 더 두는 것이지 새 Manager/Singleton 패러다임이 아니다. WLReactionRunner 는 811def 가 만들고 811i(진행 중)도 // 같은 파일에 틱을 붙일 가능성이 커서, 같은 줄을 고치면 병합 충돌이 난다 — **파일을 나눠 충돌을 0** 으로 만든다. // // ■ 무엇을 하나 // PcHitReaction(림라이트 만료 · 넉백 진행) · DashAfterImage(예약 스폰 · 페이드 · 공격전진 폴링) 틱. // 파괴·비활성·앱 종료 시 MPB 원복 + 잔상 예산 반환. 씬 전환 시 두 시스템의 상태를 비운다(파괴된 Actor 참조 방지). // // ■ 에디트 모드 // 프로브가 Play 없이 검증할 수 있게 ForceTick() 을 둔다(러너 오브젝트 없이 같은 틱 1회). // // 🔴 어셈블리 주의: Assets/WL/Combat/ 에 .asmdef 를 만들지 말 것. // ───────────────────────────────────────────────────────────────────────────── using UnityEngine; using UnityEngine.SceneManagement; namespace WL.Combat.Reaction { public sealed class ImpactRunner : MonoBehaviour { static ImpactRunner s_runner; static bool s_sceneHooked; public static bool Exists { get { return s_runner != null; } } public static void Ensure() { if (s_runner != null) return; if (!Application.isPlaying) return; // 에디트 모드에서는 오브젝트를 만들지 않는다(프로브는 ForceTick 을 쓴다) var go = new GameObject("__WLImpact"); go.hideFlags = HideFlags.DontSave; DontDestroyOnLoad(go); s_runner = go.AddComponent(); if (!s_sceneHooked) { SceneManager.activeSceneChanged += OnSceneChanged; s_sceneHooked = true; } } static void OnSceneChanged(Scene from, Scene to) { PcHitReaction.ClearAll(); DashAfterImage.ClearAll(); } // ── 에디트 모드 시뮬레이션 시계 // 에디트 모드에서는 한 번의 스크립트 호출 안에서 Time.unscaledTime 이 **멈춰 있다** → // ForceTick 이 시계를 직접 밀어 준다. Play 에서는 s_simActive 가 false 라 항상 Time.unscaledTime 이다. static float s_simNow; static bool s_simActive; /// 연출 타이밍의 기준 시각. Play = Time.unscaledTime · 프로브(ForceTick) = 시뮬레이션 시계. public static float Now { get { return s_simActive ? s_simNow : Time.unscaledTime; } } void Update() { s_simActive = false; float now = Time.unscaledTime; PcHitReaction.Tick(now, Time.unscaledDeltaTime); DashAfterImage.Tick(now); } /// 프로브(에디트 모드)용 — 러너 없이 같은 틱을 1회 돌리고 시뮬레이션 시계를 delta 만큼 민다. public static void ForceTick(float unscaledDelta) { if (!s_simActive) { s_simActive = true; s_simNow = Time.unscaledTime; } s_simNow += unscaledDelta; PcHitReaction.Tick(s_simNow, unscaledDelta); DashAfterImage.Tick(s_simNow); } /// 프로브용 — 시뮬레이션 시계를 끄고 실제 시간으로 되돌린다. public static void ResetForceClock() { s_simActive = false; s_simNow = 0f; } void OnDisable() { PcHitReaction.RestoreAll(); DashAfterImage.RestoreAll(); } void OnApplicationQuit() { PcHitReaction.RestoreAll(); DashAfterImage.RestoreAll(); } } }