Project_WL/Assets/WL/Combat/Reaction/ImpactRunner.cs

83 lines
4.3 KiB
C#
Raw Permalink Normal View History

// ─────────────────────────────────────────────────────────────────────────────
// 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<ImpactRunner>();
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;
/// <summary>연출 타이밍의 기준 시각. Play = Time.unscaledTime · 프로브(ForceTick) = 시뮬레이션 시계.</summary>
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);
}
/// <summary>프로브(에디트 모드)용 — 러너 없이 같은 틱을 1회 돌리고 시뮬레이션 시계를 delta 만큼 민다.</summary>
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);
}
/// <summary>프로브용 — 시뮬레이션 시계를 끄고 실제 시간으로 되돌린다.</summary>
public static void ResetForceClock() { s_simActive = false; s_simNow = 0f; }
void OnDisable() { PcHitReaction.RestoreAll(); DashAfterImage.RestoreAll(); }
void OnApplicationQuit() { PcHitReaction.RestoreAll(); DashAfterImage.RestoreAll(); }
}
}