[Unity] UniTask로 씬 이동하기

muchan
muchan
∙∙조회 17
[Unity] UniTask로 씬 이동하기

UniTask를 활용하여 페이드 인/아웃을 통한 씬 이동 구현하기

흐름

css
버튼 클릭
  │
  ▼
① 페이드 아웃 (화면 까매짐, 완료까지 대기) ─ 약 1초
  │   ← 이 시점엔 아직 씬 A, 로드 시작 안 함
  ▼
③ 씬 B 비동기 로드 시작 (까만 화면 뒤에서)
  │   progress 0.9 도달 + 최소시간 대기
  ▼
   allowSceneActivation = true → 씬 A 내려가고 씬 B 활성화
  │   ← 화면은 여전히 까맣게 덮여 있음
  ▼
④ 페이드 인 (씬 B가 서서히 드러남)

FadeController

csharp
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.InputSystem;

public class FadeController : MonoBehaviour
{
    public CanvasGroup fadeCanvasGroup;

    public float fadeDuration = 1f;

    private static FadeController instance;

    private void Awake()
    {
        if (instance != null && instance != this)
        {
            Destroy(gameObject);
            return;
        }
        instance = this;

        DontDestroyOnLoad(gameObject);

        fadeCanvasGroup.alpha = 0f;
    }

    private void OnDestroy()
    {
        if (instance == this)
        {
            instance = null;
        }
    }

    public async UniTask FadeOut(float duration = -1f)
    {
        if (duration < 0f)
            duration = fadeDuration;

        await FadeAsync(fadeCanvasGroup, 0f, 1f, duration);
    }

    public async UniTask FadeIn(float duration = -1f)
    {
        if (duration < 0f)
            duration = fadeDuration;

        await FadeAsync(fadeCanvasGroup, 1f, 0f, duration);
    }

    private async UniTask FadeAsync(CanvasGroup canvasGroup, float from, float to, float duration)
    {
        canvasGroup.alpha = from;

        float elapsed = 0f;
        while (elapsed < duration)
        {
            elapsed += Time.deltaTime;
            float t = elapsed / duration;
            canvasGroup.alpha = Mathf.Lerp(from, to, t);
            await UniTask.Yield();
        }

        canvasGroup.alpha = to;
    }
}
  • 화면 Fade 연출을 담당하는 싱글톤
  • Awake에서 Instance체크 후 DontDestroyOnLoad로 씬이 바뀌어도 살아남는다
  • 씬 전환 중에도 화면을 계속 덮고 있어야하기 때문
  • Fadeout/Fadein: FadeAsync를 호출하는 래퍼함수
ini
while (elapsed < duration)
{
    elapsed += Time.deltaTime;
    float t = elapsed / duration;
    canvasGroup.alpha = Mathf.Lerp(from, to, t);
    await UniTask.Yield();
}

여기서 await UniTask.Yield()는 코루틴의 yield return null 과 같다

SceneLoadController

csharp
using System;
using System.Threading.Tasks;
using Cysharp.Threading.Tasks;
using TMPro;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;

public class SceneLoadController : MonoBehaviour
{
    public Button loadSceneButton;
    public Slider progressSlider;
    public TMP_Text progressText;

    public FadeController fadeController;

    public string targetSceneName;
    public float minimumLoadTime = 2f;
    private bool isLoading = false;

    private void Start()
    {
        loadSceneButton.onClick.AddListener(OnLoadSceneClicked);
        fadeController = FindFirstObjectByType<FadeController>();
        gameObject.SetActive(false);
    }

    private void OnLoadSceneClicked()
    {
        OnLoadSceneWithFadeAsync().Forget();
    }

    private async UniTask OnLoadSceneWithFadeAsync()
    {
        progressSlider.value = 0f;
        progressText.text = "로딩 중... 0%";

        await fadeController.FadeOut();

        gameObject.SetActive(true);
        isLoading = true;
        loadSceneButton.interactable = false;

        await LoadSceneAsync();

        await fadeController.FadeIn();

        isLoading = false;
        // loadSceneButton.interactable = true;
        // gameObject.SetActive(false);
    }

    private async UniTask LoadSceneAsync()
    {
        var progress = Progress.Create<float>(p =>
        {
            progressSlider.value = p;
            progressText.text = $"로딩 중... {(int)(p * 100)}%";
            Debug.Log($"로딩 중... {(int)(p * 100)}%");
        });

        var asyncOp = SceneManager.LoadSceneAsync(targetSceneName);
        asyncOp.allowSceneActivation = false; // 얘를 true로 하면 씬이 바로 넘어감 나중에 바꿀때 true 전환

        float startTime = Time.time;

        while (asyncOp.progress < 0.9f)
        {
            progress.Report(Mathf.Clamp01(asyncOp.progress / 0.9f));
            await UniTask.Yield();
        }

        float elapsed = Time.time - startTime;
        if (elapsed < minimumLoadTime)
        {
            await UniTask.Delay(TimeSpan.FromSeconds(minimumLoadTime - elapsed));
        }

        progress.Report(1.0f);
        asyncOp.allowSceneActivation = true;
        await asyncOp.ToUniTask();
    }
}
  • 실제 씬 전환을 페이드와 묶어서 처리
  • 로딩 UI는 비활성화 했다가 전환 시점에만 켜짐
  • 페이드 아웃 → 로딩 UI 켜기 → 씬 로드 → 페이드 인
  • allowSceneActivation = false일 때 asyncOp.progress는 0.9에서 멈춰서 나머지 0.1은 활성화에 쓰이는 구간이라 false면 도달을 안함

결과

잘 보이진 않지만 씬 A에서 페이드 아웃 되면서 로딩창이 뜨고 씬 B로 이동하는 것을 볼 수 있다.