using System; using System.Threading.Tasks; using UnityEngine; using UnityEngine.SceneManagement; public class SceneLoadManager : MonoBehaviour { public static SceneLoadManager Instance; [SerializeField] private GameObject _loadingRoot; [SerializeField] private Camera _loadingCam; [SerializeField] private Transform _loadingCamTargetTransform; [SerializeField] private LoadingScreen _loadingScreen; [SerializeField] private Material _supermarketStartSkybox; [SerializeField] private Material _supermarketLoadingSkybox; [SerializeField, Min(0f)] private float _skyboxFadeTime = 1f; // 정상(밝음) 상태에서의 스카이박스 _Exposure 값. 페이드 인의 도착 지점. [SerializeField, Min(0f)] private float _skyboxNormalExposure = 1f; // 공유 에셋이 더러워지지 않도록 런타임 인스턴스로 복제해 사용 private Material _runtimeStartSkybox; private Material _runtimeLoadingSkybox; private void Awake() { if (Instance == null) { Instance = this; //만들어진 자신을 인스턴스로 설정 } else { Destroy(gameObject); //이미 인스턴스가 있으면 자신을 파괴 } _loadingScreen = _loadingCam.GetComponentInChildren(); if (_supermarketStartSkybox != null) _runtimeStartSkybox = new Material(_supermarketStartSkybox); if (_supermarketLoadingSkybox != null) _runtimeLoadingSkybox = new Material(_supermarketLoadingSkybox); if (_runtimeStartSkybox != null) { RenderSettings.skybox = _runtimeStartSkybox; DynamicGI.UpdateEnvironment(); } } private void OnDestroy() { // 런타임 복제 인스턴스는 직접 정리 if (_runtimeStartSkybox != null) Destroy(_runtimeStartSkybox); if (_runtimeLoadingSkybox != null) Destroy(_runtimeLoadingSkybox); } private void Start() { SceneManager.sceneLoaded += OnSceneLoaded; OnSceneLoaded(SceneManager.GetActiveScene(), LoadSceneMode.Single); } private void Update() { if(_loadingCamTargetTransform != null) { _loadingCam.transform.position = _loadingCamTargetTransform.position; _loadingCam.transform.rotation = _loadingCamTargetTransform.rotation; } } //씬이 로드되었을때 호출 private void OnSceneLoaded(Scene scene, LoadSceneMode mode) { Debug.Log("씬 로드됨"); if(scene.name == "GameScene") { MonoBehaviour[] allObjs = UnityEngine.Object.FindObjectsByType(FindObjectsSortMode.None); foreach (var obj in allObjs) { if (obj is ITransScenePossible itsp) { itsp.OnSceneLoaded(); } } } } public async Awaitable FadeLoadingCanvas(bool isOut,float fadeTime) { float startAlpha = isOut ? 1f : 0f; float endAlpha = isOut ? 0f : 1f; float timer = 0; _loadingScreen.LoadingScreenCanvasGroup.alpha = startAlpha; while(timer < fadeTime) { timer += Time.deltaTime; _loadingScreen.LoadingScreenCanvasGroup.alpha = Mathf.Lerp(startAlpha, endAlpha, timer / fadeTime); await Awaitable.NextFrameAsync(this.destroyCancellationToken); } _loadingScreen.LoadingScreenCanvasGroup.alpha = endAlpha; } // 현재 RenderSettings.skybox의 _Exposure를 from→to로 보간 // Skybox/Procedural, /Cubemap, /Panoramic 셰이더 공통 프로퍼티 private async Awaitable FadeSkybox(float from, float to, float duration) { var sky = RenderSettings.skybox; if (sky == null || !sky.HasFloat("_Exposure")) return; float timer = 0f; while (timer < duration) { timer += Time.deltaTime; float k = Mathf.Clamp01(timer / duration); sky.SetFloat("_Exposure", Mathf.Lerp(from, to, k)); DynamicGI.UpdateEnvironment(); await Awaitable.NextFrameAsync(this.destroyCancellationToken); } sky.SetFloat("_Exposure", to); DynamicGI.UpdateEnvironment(); } public async Awaitable SetSceneLoadingActive(bool isActive,float alphaTime) { if (isActive) _loadingRoot.SetActive(true); if (alphaTime > 0f) { await FadeLoadingCanvas(!isActive,alphaTime); } if (!isActive) _loadingRoot.SetActive(false); } public void SetSceneLoadingActive(bool isActive) { _ = SetSceneLoadingActive(isActive, 0f); } public void SetSceneLoadingProgressValue(float value) { _loadingScreen.LoadingImage.fillAmount = value; _loadingScreen.LoadingTextMeshProUGUI.text = $"{(value * 100):F0}% 로딩 중..."; } public void SetSceneLoadingProgressValue(float value,string loadingText) { _loadingScreen.LoadingImage.fillAmount = value; _loadingScreen.LoadingTextMeshProUGUI.text = loadingText; } public void RequestSceneChange(string sceneName) { _ = SceneChange(sceneName); } private async Awaitable SceneChange(string sceneName) { try { SetSceneLoadingProgressValue(0f); //스카이 박스 페이드 아웃 로직 await FadeSkybox(_skyboxNormalExposure, 0f, _skyboxFadeTime); // 검게 된 상태에서 머티리얼 교체 (교체 순간이 가려져 깜빡임 없음) RenderSettings.skybox = _runtimeLoadingSkybox; //스카이 박스 페이드 인 로직 await FadeSkybox(0f, _skyboxNormalExposure, _skyboxFadeTime); await SetSceneLoadingActive(true,1f); AsyncOperation op = SceneManager.LoadSceneAsync(sceneName); //자동 전환을 하고 싶지 않을 경우 해당값을 false로 두었다가 true로 바꾸면 그 때 전환됨 op.allowSceneActivation = false; //화면에 보여줄 로딩 수치 float displayProgress = 0f; //op.progress 0.9가 데이터 로딩이 끝난 기준 allowSceneActivation이 트루면 바로 다음으로 넘어가면서 op.isDone이 true가 된다. while (op.progress < 0.9f) { //실제 로딩 수치 float realProgress = Mathf.Clamp01(op.progress / 0.9f); //보여줄 값을 실제값을 향해 부드럽게 이동 displayProgress = Mathf.MoveTowards(displayProgress, realProgress, Time.deltaTime * 0.5f); // UI에 적용 SetSceneLoadingProgressValue(displayProgress); await Awaitable.NextFrameAsync(this.destroyCancellationToken); //자기자신이 파괴될때 토큰에 취소요청을 보냄 } SetSceneLoadingProgressValue(1); // 잠시 대기했다가 전환 await Awaitable.WaitForSecondsAsync(1.0f, this.destroyCancellationToken); await SetSceneLoadingActive(false,1f); //스카이 박스 페이드 아웃 await FadeSkybox(_skyboxNormalExposure, 0f, _skyboxFadeTime); // 검게 된 상태에서 정상 스카이박스로 교체 RenderSettings.skybox = _runtimeStartSkybox; //로딩 끝 op.allowSceneActivation = true; // 씬 활성화가 완전히 끝날 때까지 대기 // allowSceneActivation가 true가 되고 완전히 전환되기까지는 몇프레임 걸림. op.isDone 은 이 과정이 끝난 뒤에 true가 됨. while(!op.isDone) { await Awaitable.NextFrameAsync(this.destroyCancellationToken); } //VR용 로직 //트래킹이 중단되면 안되기 때문에 카메라를 유지해야 한다 _loadingCamTargetTransform = Camera.main.transform; // 새로운 씬의 메인카메라를 따라가게끔 설정 //------------------------------------------------------------------------------- //스카이 박스 페이드 인 await FadeSkybox(0f, _skyboxNormalExposure, _skyboxFadeTime); Debug.Log("씬 전환됨"); } catch (OperationCanceledException) { Debug.Log("씬 전환 작업이 취소됨"); } } }