using UnityEngine; using UnityEngine.Playables; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal; public class LocalManager : MonoBehaviour,ISceneInitializable { [SerializeField] private string _startSceneName; [SerializeField] private string _nextSceneName; [SerializeField] private AudioClip _caffebeneBGM; // 흑백 전환에 걸리는 시간(초) [SerializeField, Min(0f)] private float _grayscaleFadeTime = 1.5f; // BGM 재생 시작 후 씬 전환까지 대기 시간(초) [SerializeField, Min(0f)] private float _caffebeneSceneChangeDelay = 13f; // 채도 -100으로 만들어둔 공용 프로파일 (Assets/Settings/GrayscaleVolumeProfile) [SerializeField] private VolumeProfile _grayscaleProfile; // 씬에 이미 있는 글로벌 볼륨 — 새로 만들지 않고 이 볼륨의 프로파일을 흑백으로 교체한다 [SerializeField] private Volume _globalVolume; [SerializeField] private GameObject CaffebeneImgObj; // 이 씬 전용 스카이박스 (비우면 SceneLoadManager의 기본 스카이박스 사용) [SerializeField] private Material _sceneSkybox; // FadeToGrayscale 직전의 원래 볼륨 상태 — FadeToColor로 되돌릴 때 복원용 private VolumeProfile _preGrayscaleProfile; private float _preGrayscaleWeight; private bool _grayscaleActive; public void NextScene(int delay) { _ = Util.RunDelayed((float)delay,()=>SceneLoadManager.Instance.RequestSceneChange(_nextSceneName)); } public void CaffebeneNextSceneChange(int delay) { _ = CaffebeneSequence(delay); } // 모든 애니메이션 정지 → 시야 흑백 페이드 → BGM 재생 → 대기 후 씬 전환 private async Awaitable CaffebeneSequence(int delay) { try { await Awaitable.WaitForSecondsAsync((float)delay); PauseAllAnimations(); CaffebeneImgObj.SetActive(true); await FadeToGrayscale(_grayscaleFadeTime); SoundManager.Instance.PlayOverrideBGM(_caffebeneBGM); await Awaitable.WaitForSecondsAsync(_caffebeneSceneChangeDelay, destroyCancellationToken); CaffebeneImgObj.SetActive(false); SceneLoadManager.Instance.RequestSceneChange(_nextSceneName); } catch (System.OperationCanceledException) { // 씬 전환 등으로 자신이 파괴되어 취소됨 } } // 씬 안의 Animator / 타임라인 / 파티클을 전부 일시정지 // (Time.timeScale은 건드리지 않는다 — BGM 페이드와 씬 전환 딜레이가 scaled time 기준이라 함께 멈춰버림) public static void PauseAllAnimations() { foreach (var animator in FindObjectsByType()) animator.speed = 0f; foreach (var director in FindObjectsByType()) director.Pause(); foreach (var particle in FindObjectsByType()) particle.Pause(); } // 씬의 글로벌 볼륨 프로파일을 흑백으로 교체하고 weight를 0 → 1로 페이드 (완전 흑백) // 기존 프로파일(그레인/모션블러 포함)이 통째로 빠지므로 정지 화면에서 지글거림도 없다 // 같은 씬 어디서든 참조로 호출 가능. 되돌리려면 FadeToColor 사용. public async Awaitable FadeToGrayscale(float duration) { if (_grayscaleProfile == null || _globalVolume == null) { Debug.LogWarning("[LocalManager] _grayscaleProfile 또는 _globalVolume이 비어 있어 흑백 페이드를 건너뜁니다."); return; } // 원래 볼륨 상태를 기억해 둔다 (FadeToColor 복원용). 이미 흑백이면 덮어쓰지 않는다. if (!_grayscaleActive) { _preGrayscaleProfile = _globalVolume.sharedProfile; _preGrayscaleWeight = _globalVolume.weight; _grayscaleActive = true; } // sharedProfile로 물려야 에셋이 복제·수정되지 않는다 (페이드는 weight로만) // 씬 오브젝트의 프로퍼티 변경이라 플레이 종료 시 원래대로 돌아온다 _globalVolume.sharedProfile = _grayscaleProfile; _globalVolume.weight = 0f; // 메인 카메라뿐 아니라 RecordCamera 등 모든 카메라에 포스트 프로세싱을 켠다 foreach (var cam in FindObjectsByType()) cam.GetUniversalAdditionalCameraData().renderPostProcessing = true; await FadeVolumeWeight(0f, 1f, duration); } // 흑백 → 원래 컬러로 되돌린다. weight를 0으로 뺀 뒤, FadeToGrayscale가 기억해둔 원래 프로파일/weight로 복원. // 흑백 상태가 아니면 아무것도 하지 않는다. 같은 씬 어디서든 참조로 호출 가능. public async Awaitable FadeToColor(float duration) { if (_globalVolume == null || !_grayscaleActive) return; await FadeVolumeWeight(_globalVolume.weight, 0f, duration); _globalVolume.sharedProfile = _preGrayscaleProfile; _globalVolume.weight = _preGrayscaleWeight; _grayscaleActive = false; } // 인스펙터 UnityEvent에서 호출용 void 래퍼. // (async 메서드는 반환형이 void가 아니라 UnityEvent 드롭다운에 안 뜬다 — 여기서 fire-and-forget으로 감싼다) public void FadeToGrayscaleEvent(float duration) => _ = FadeToGrayscale(duration); public void FadeToColorEvent(float duration) => _ = FadeToColor(duration); // 글로벌 볼륨 weight를 from → to로 duration 동안 페이드 (duration ≤ 0이면 즉시 to) private async Awaitable FadeVolumeWeight(float from, float to, float duration) { if (duration <= 0f) { _globalVolume.weight = to; return; } float timer = 0f; while (timer < duration) { timer += Time.deltaTime; _globalVolume.weight = Mathf.Lerp(from, to, Mathf.Clamp01(timer / duration)); await Awaitable.NextFrameAsync(destroyCancellationToken); } _globalVolume.weight = to; } // 씬 로드 시 이 씬 전용 스카이박스를 적용 // (SceneLoadManager가 기본 스카이박스를 깐 뒤에 호출되므로 그 위에 덮어쓴다) public void OnSceneLoaded() { if (_sceneSkybox != null && SceneLoadManager.Instance != null) SceneLoadManager.Instance.SetSceneSkybox(_sceneSkybox); } public void StartSceneChange(int delay) { _ = StartSceneChangeSequence(delay); } private async Awaitable StartSceneChangeSequence(int delay) { try { await Awaitable.WaitForSecondsAsync((float)delay); SceneLoadManager.Instance.RequestSceneChange(_startSceneName); } catch (System.OperationCanceledException) { // 씬 전환 등으로 자신이 파괴되어 취소됨 } } }