2026-07-10 비동기 씬전환
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
using UnityEngine.XR.Interaction.Toolkit.Locomotion;
|
||||
|
||||
public class SceneLoadManager : MonoBehaviour
|
||||
{
|
||||
@@ -9,10 +10,20 @@ public class SceneLoadManager : MonoBehaviour
|
||||
[SerializeField] private GameObject _loadingRoot;
|
||||
[SerializeField] private Camera _loadingCam;
|
||||
[SerializeField] private Transform _loadingCamTargetTransform;
|
||||
[SerializeField] private LoadingScreen _loadingScreen;
|
||||
|
||||
private bool _isChangingScene = false;
|
||||
[SerializeField] private Material _sceneSkybox;
|
||||
[SerializeField] private Material _loadingSkybox;
|
||||
|
||||
public bool IsChangingScene => _isChangingScene;
|
||||
[SerializeField, Min(0f)] private float _skyboxFadeTime = 1f;
|
||||
// 정상(밝음) 상태에서의 스카이박스 _Exposure 값. 페이드 인의 도착 지점.
|
||||
[SerializeField, Min(0f)] private float _skyboxNormalExposure = 1f;
|
||||
|
||||
// 씬 전환(로딩) 동안 플레이어를 치워둘 허공 좌표 — 기존 씬 오브젝트가 시야에 안 들어오게
|
||||
[SerializeField] private Vector3 _loadingPlayerPosition = new Vector3(10000f, 0f, 0f);
|
||||
|
||||
private Material _runtimeSceneSkybox;
|
||||
private Material _runtimeLoadingSkybox;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
@@ -24,6 +35,17 @@ private void Awake()
|
||||
{
|
||||
Destroy(gameObject); // 이미 인스턴스가 있으면 자신을 파괴
|
||||
}
|
||||
|
||||
if (_sceneSkybox != null)
|
||||
_runtimeSceneSkybox = new Material(_sceneSkybox);
|
||||
if (_loadingSkybox != null)
|
||||
_runtimeLoadingSkybox = new Material(_loadingSkybox);
|
||||
|
||||
if (_runtimeSceneSkybox != null)
|
||||
{
|
||||
RenderSettings.skybox = _runtimeSceneSkybox;
|
||||
DynamicGI.UpdateEnvironment();
|
||||
}
|
||||
}
|
||||
|
||||
private void Start()
|
||||
@@ -37,6 +59,8 @@ private void OnDestroy()
|
||||
if (Instance == this)
|
||||
{
|
||||
SceneManager.sceneLoaded -= OnSceneLoaded;
|
||||
if (_runtimeSceneSkybox != null) Destroy(_runtimeSceneSkybox);
|
||||
if (_runtimeLoadingSkybox != null) Destroy(_runtimeLoadingSkybox);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,25 +87,102 @@ private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
|
||||
}
|
||||
}
|
||||
|
||||
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로 보간
|
||||
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)
|
||||
{
|
||||
// 여기에 로딩바 UI 연결 예정
|
||||
_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;
|
||||
}
|
||||
|
||||
// 플레이어(XR Origin 루트, Player 태그)를 로딩용 허공 좌표로 옮긴다.
|
||||
// 새 씬이 로드되면 새 씬의 플레이어 스폰 위치가 적용되므로 되돌릴 필요는 없다.
|
||||
private void MovePlayerToLoadingArea()
|
||||
{
|
||||
var player = GameObject.FindWithTag("Player");
|
||||
if (player == null)
|
||||
{
|
||||
Debug.LogWarning("[SceneLoadManager] Player 태그 오브젝트를 찾지 못해 이동 생략");
|
||||
return;
|
||||
}
|
||||
|
||||
// 로딩 동안 이동/회전/중력을 전부 정지 — 허공에서 낙하하지 않게.
|
||||
// (XRI에서 중력도 LocomotionProvider다) 씬이 로드되면 리그가 새 씬 것으로
|
||||
// 교체되므로 다시 켤 필요 없다. HMD 트래킹은 영향받지 않는다.
|
||||
foreach (var provider in player.GetComponentsInChildren<LocomotionProvider>())
|
||||
provider.enabled = false;
|
||||
|
||||
// CharacterController는 켜진 채로 위치를 바꾸면 이동이 씹힐 수 있어 잠깐 끄고 옮긴다
|
||||
var cc = player.GetComponent<CharacterController>();
|
||||
if (cc != null) cc.enabled = false;
|
||||
player.transform.position = _loadingPlayerPosition;
|
||||
if (cc != null) cc.enabled = true;
|
||||
}
|
||||
|
||||
public void RequestSceneChange(string sceneName)
|
||||
{
|
||||
if (_isChangingScene)
|
||||
{
|
||||
Debug.Log("이미 씬 전환 중입니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(sceneName))
|
||||
{
|
||||
Debug.LogWarning("이동할 씬 이름이 비어있습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
_ = SceneChange(sceneName);
|
||||
}
|
||||
|
||||
@@ -89,80 +190,82 @@ private async Awaitable SceneChange(string sceneName)
|
||||
{
|
||||
try
|
||||
{
|
||||
_isChangingScene = true;
|
||||
|
||||
// 로딩바 수치 0으로 설정
|
||||
//로딩바 수치 0으로 설정
|
||||
SetSceneLoadingProgressValue(0f);
|
||||
|
||||
if (_loadingRoot != null)
|
||||
{
|
||||
_loadingRoot.SetActive(true);
|
||||
}
|
||||
//스카이 박스 페이드 아웃
|
||||
await FadeSkybox(_skyboxNormalExposure, 0f, _skyboxFadeTime);
|
||||
|
||||
// 검게 된 상태에서 머티리얼 교체 (교체 순간이 가려져 깜빡임 없음)
|
||||
RenderSettings.skybox = _runtimeLoadingSkybox;
|
||||
|
||||
// 어두운 동안 플레이어를 허공으로 이동 — 로딩 중 기존 씬 오브젝트가 보이지 않게
|
||||
// (로딩 룸은 카메라를 따라다니므로 함께 이동한다)
|
||||
MovePlayerToLoadingArea();
|
||||
|
||||
//스카이 박스 페이드 인
|
||||
await FadeSkybox(0f, _skyboxNormalExposure, _skyboxFadeTime);
|
||||
|
||||
//로딩창을 1초에 걸쳐 나타나게함
|
||||
await SetSceneLoadingActive(true,1f);
|
||||
|
||||
AsyncOperation op = SceneManager.LoadSceneAsync(sceneName);
|
||||
|
||||
if (op == null)
|
||||
{
|
||||
Debug.LogError($"씬 로드 실패: {sceneName}");
|
||||
_isChangingScene = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// 자동 전환을 하고 싶지 않을 경우 false로 두었다가 true로 바꾸면 그 때 전환됨
|
||||
//자동 전환을 하고 싶지 않을 경우 해당값을 false로 두었다가 true로 바꾸면 그 때 전환됨
|
||||
op.allowSceneActivation = false;
|
||||
|
||||
// 화면에 보여줄 로딩 수치
|
||||
//화면에 보여줄 로딩 수치
|
||||
float displayProgress = 0f;
|
||||
|
||||
// op.progress 0.9가 데이터 로딩이 끝난 기준
|
||||
while (op.progress < 0.9f)
|
||||
//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);
|
||||
}
|
||||
|
||||
// 로딩바 수치 1(100%)로 설정
|
||||
SetSceneLoadingProgressValue(1f);
|
||||
//로딩바 수치 1(100%)로 설정 (데이터 로딩은 이미 끝이기 때문에)
|
||||
SetSceneLoadingProgressValue(1);
|
||||
|
||||
// 잠시 대기했다가 전환
|
||||
await Awaitable.WaitForSecondsAsync(1.0f, this.destroyCancellationToken);
|
||||
|
||||
// 로딩바가 1초에 걸쳐 사라짐
|
||||
await SetSceneLoadingActive(false,1f);
|
||||
|
||||
await FadeSkybox(_skyboxNormalExposure, 0f, _skyboxFadeTime);
|
||||
|
||||
// 검게 된 상태에서 정상 스카이박스로 교체
|
||||
RenderSettings.skybox = _runtimeSceneSkybox;
|
||||
|
||||
// 다음씬으로 넘어가도 됨을 알림
|
||||
op.allowSceneActivation = true;
|
||||
|
||||
// 씬 활성화가 완전히 끝날 때까지 대기
|
||||
while (!op.isDone)
|
||||
// allowSceneActivation가 true가 되고 완전히 전환되기까지는 몇프레임 걸림. op.isDone 은 이 과정이 끝난 뒤에 true가 됨.
|
||||
while(!op.isDone)
|
||||
{
|
||||
await Awaitable.NextFrameAsync(this.destroyCancellationToken);
|
||||
}
|
||||
|
||||
// VR용 로직
|
||||
// 트래킹이 중단되면 안되기 때문에 카메라를 유지해야 한다
|
||||
if (Camera.main != null)
|
||||
{
|
||||
_loadingCamTargetTransform = Camera.main.transform;
|
||||
}
|
||||
//VR용 로직
|
||||
//트래킹이 중단되면 안되기 때문에 카메라를 유지해야 한다
|
||||
_loadingCamTargetTransform = Camera.main.transform; // 새로운 씬의 메인카메라를 따라가게끔 설정
|
||||
|
||||
if (_loadingRoot != null)
|
||||
{
|
||||
_loadingRoot.SetActive(false);
|
||||
}
|
||||
|
||||
_isChangingScene = false;
|
||||
await FadeSkybox(0f, _skyboxNormalExposure, _skyboxFadeTime);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Debug.Log("씬 전환 작업이 취소됨");
|
||||
_isChangingScene = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user