Files
Dino_Love_Simulation/Assets/02_Scripts/Managers/LocalManager.cs
2026-07-23 17:56:58 +09:00

220 lines
9.0 KiB
C#

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;
// 일반 씬 전환(NextSceneWithBgm)용 전용 BGM과, 재생 후 씬 전환까지의 대기(초, 0이면 즉시 전환)
[SerializeField] private AudioClip _sceneChangeBGM;
[SerializeField, Min(0f)] private float _sceneChangeBgmPlayTime = 0f;
[Tooltip("런타임에 번호로 골라 _sceneChangeBGM을 교체할 BGM 후보들 (SetSceneChangeBgm(번호)로 선택)")]
[SerializeField] private AudioClip[] _sceneChangeBgmOptions;
// 흑백 전환에 걸리는 시간(초)
[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));
}
// 일반 씬 전환 + 전용 BGM. delay 뒤 전용 BGM을 재생하고(있으면), _sceneChangeBgmPlayTime 만큼 두었다가 씬 전환.
// 카페 시퀀스와 달리 흑백/이미지 연출은 없다. UnityEvent에서 호출 가능.
public void NextSceneWithBgm(int delay)
{
_ = NextSceneWithBgmSequence(delay);
}
// UnityEvent에서 번호로 호출 — _sceneChangeBgmOptions[index]를 다음 NextSceneWithBgm에 쓸 전용 BGM으로 교체한다.
public void SetSceneChangeBgm(int index)
{
if (_sceneChangeBgmOptions == null || index < 0 || index >= _sceneChangeBgmOptions.Length)
{
Debug.LogWarning($"[LocalManager] 잘못된 Scene Change BGM 번호: {index} (후보 {(_sceneChangeBgmOptions != null ? _sceneChangeBgmOptions.Length : 0)}개)");
return;
}
_sceneChangeBGM = _sceneChangeBgmOptions[index];
}
private async Awaitable NextSceneWithBgmSequence(int delay)
{
try
{
await Awaitable.WaitForSecondsAsync((float)delay, destroyCancellationToken);
if (_sceneChangeBGM != null && SoundManager.Instance != null)
SoundManager.Instance.PlayOverrideBGM(_sceneChangeBGM);
if (_sceneChangeBgmPlayTime > 0f)
await Awaitable.WaitForSecondsAsync(_sceneChangeBgmPlayTime, destroyCancellationToken);
SceneLoadManager.Instance.RequestSceneChange(_nextSceneName);
}
catch (System.OperationCanceledException)
{
// 씬 전환 등으로 자신이 파괴되어 취소됨
}
}
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>())
animator.speed = 0f;
foreach (var director in FindObjectsByType<PlayableDirector>())
director.Pause();
foreach (var particle in FindObjectsByType<ParticleSystem>())
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<Camera>())
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)
{
// 씬 전환 등으로 자신이 파괴되어 취소됨
}
}
}