로딩화면은 게임에서 필요한 데이터를 불러오는 동안에 보이는 화면입니다. 게임이 시작될 때, 새로운 씬으로 이동할 때 필요합니다. 로딩화면이 없으면 데이터를 불러오는 동안 화면이 깜빡이거나 멈추는 현상이 발생할 수 있으며, 이는 유저들에게 불편을 초래합니다. 따라서 로딩화면은 게임의 원활한 진행을 위해 필수적입니다.

 

📺 미리보기

 

📖 구현 내용

  • 다른 신을 로딩할 때 로딩 화면을 보여주어 얼마큼 로딩이 되었는지 볼 수 있습니다.
  • 로딩 화면에 설명란을 이용하여 게임 플레이에 도움이 되는 내용을 제공할 수 있습니다.
  • 씬 로드 완료 후 Start보다 한 프레임 느린 LateStart를 정의하여 호출해 씬이 완전히 로드된 후 특정 초기화 및 함수를 호출할 수 있습니다.

 

✅ 구현

  • LoadingSceneController스크립트와 UI를 구현합니다.

· Script

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
using TMPro;
using System;

public class LoadingSceneController : MonoBehaviour
{
    #region Singleton

    private static LoadingSceneController instance;
    public static LoadingSceneController Instance
    {
        get
        {
            if (instance == null)
            {
                LoadingSceneController sceneController = FindObjectOfType<LoadingSceneController>();
                if (sceneController != null)
                {
                    instance = sceneController;
                }
                else
                {
                    // 인스턴스가 없다면 생성
                    instance = Create();
                }
            }

            return instance;
        }
    }

    #endregion

    private static LoadingSceneController Create()
    {
        // 리소스에서 로드
        return Instantiate(Resources.Load<LoadingSceneController>("LoadingUI"));
    }

    private void Awake()
    {
        if (Instance != this)
        {
            Destroy(gameObject);
            return;
        }

        DontDestroyOnLoad(gameObject);
    }

    [SerializeField] private CanvasGroup mCanvasGroup;
    [SerializeField] private Image mProgressBar;
    [SerializeField] private TextMeshProUGUI mToolTipLabel;
    [SerializeField][TextArea] string[] mToolTips;

    private string mLoadSceneName;

    Action? mOnSceneLoadAction;

    public void LoadScene(string sceneName, Action? action = null)
    {
        gameObject.SetActive(true);
        SceneManager.sceneLoaded += OnSceneLoaded;
        mOnSceneLoadAction = action;

        mLoadSceneName = sceneName;

        mToolTipLabel.text = mToolTips[UnityEngine.Random.Range(0, mToolTips.Length - 1)];

        StartCoroutine(CoLoadSceneProcess());
    }

    private IEnumerator CoLoadSceneProcess()
    {
        mProgressBar.fillAmount = 0.0f;

        //코루틴 안에서 yield return으로 코루틴을 실행하면.. 해당 코루틴이 끝날때까지 대기한다
        yield return StartCoroutine(Fade(true));

        //로컬 로딩
        AsyncOperation op = SceneManager.LoadSceneAsync(mLoadSceneName);

        op.allowSceneActivation = false;

        float process = 0.0f;

        //씬 로드가 끝나지 않은 상태라면?
        while (!op.isDone)
        {
            yield return null;

            if (op.progress < 0.9f)
            {
                mProgressBar.fillAmount = op.progress;
            }
            else
            {
                process += Time.deltaTime * 5.0f;
                mProgressBar.fillAmount = Mathf.Lerp(0.9f, 1.0f, process);

                if (process > 1.0f)
                {
                    op.allowSceneActivation = true;
                    yield break;
                }
            }
        }
    }

    private void OnSceneLoaded(Scene arg0, LoadSceneMode arg1)
    {
        if (arg0.name == mLoadSceneName)
        {
            StartCoroutine(Fade(false));
            SceneManager.sceneLoaded -= OnSceneLoaded;
        }
    }

    private IEnumerator CoLateStart()
    {
        yield return new WaitForEndOfFrame();

        // 예약된 함수 실행
        mOnSceneLoadAction?.Invoke();
    }

    private IEnumerator Fade(bool isFadeIn)
    {
        float process = 0f;

        if (!isFadeIn)
            StartCoroutine(CoLateStart());

        while (process < 1.0f)
        {
            process += Time.unscaledDeltaTime;
            mCanvasGroup.alpha = isFadeIn ? Mathf.Lerp(0.0f, 1.0f, process) : Mathf.Lerp(1.0f, 0.0f, process);

            yield return null;
        }

        if (!isFadeIn)
            gameObject.SetActive(false);
    }
}

 

#region Singleton

private static LoadingSceneController instance;
public static LoadingSceneController Instance
{
    get
    {
        if (instance == null)
        {
            LoadingSceneController sceneController = FindObjectOfType<LoadingSceneController>();
            if (sceneController != null)
            {
                instance = sceneController;
            }
            else
            {
                // 인스턴스가 없다면 생성
                instance = Create();
            }
        }

        return instance;
    }
}

#endregion
  • 싱글턴 디자인으로 구현하여 편리하게 사용할 수 있도록 합니다.
  • 만약에 오브젝트가 씬 내에 없다면 Create 함수를 이용하여 인스턴스 합니다.

 

private static LoadingSceneController Create()
{
    // 리소스에서 로드
    return Instantiate(Resources.Load<LoadingSceneController>("LoadingUI"));
}
  • Resouces.Load를 이용하여 불러옵니다.
  • 이렇게 구성하면 모든 씬에서 특별한 설정 없이 바로 불러올 수 있습니다.
  • 이 로드를 단 한 번만 하면 다음부터는 싱글턴을 이용하여 더 이상 로드를 하지 않아도 됩니다.

 

private void Awake()
{
    if (Instance != this)
    {
        Destroy(gameObject);
        return;
    }

    DontDestroyOnLoad(gameObject);
}
  • 어떠한 경우에 인스턴스가 두 개 이상 있다면, 해당 인스턴스를 제거합니다.
  • 이 오브젝트는 신을 로드할 때 파괴되지 않게 설정하여 추후에도 같은 인스턴스를 재사용할 수 있도록 합니다.

 

[SerializeField] private CanvasGroup mCanvasGroup;
  • 페이드 인 아웃 효과를 주기 위한 캔버스 그룹입니다.

 

[SerializeField] private Image mProgressBar;
  • 로딩이 얼마나 되었는지 Fill을 하여 보여주기 위한 이미지입니다.

 

[SerializeField] private TextMeshProUGUI mToolTipLabel;
  • 로딩 화면 도중 정보를 텍스트로 제공하기 위한 툴팁 라벨입니다.

 

[SerializeField][TextArea] string[] mToolTips;
  • 제공할 툴팁들을 미리 지정합니다.

 

Action? mOnSceneLoadAction;
  • 로딩을 할 때 함수를 인자로 추가로 전달했다면, 해당 함수를 호출하기 위한 Action입니다.

 

public void LoadScene(string sceneName, Action? action = null)
{
    gameObject.SetActive(true);
    SceneManager.sceneLoaded += OnSceneLoaded;
    mOnSceneLoadAction = action;

    mLoadSceneName = sceneName;

    mToolTipLabel.text = mToolTips[UnityEngine.Random.Range(0, mToolTips.Length - 1)];

    StartCoroutine(CoLoadSceneProcess());
}
  • 싱글턴을 이용하여 외부에서 호출하여 씬을 로드합니다.
  • action을 추가하여 씬을 완전히 로드한 후 한 프레임이 지난 LateStart 시점에 함수를 호출할 수 있습니다.

 

private IEnumerator CoLoadSceneProcess()
{
    mProgressBar.fillAmount = 0.0f;

    //코루틴 안에서 yield return으로 코루틴을 실행하면.. 해당 코루틴이 끝날때까지 대기한다
    yield return StartCoroutine(Fade(true));

    //로컬 로딩
    AsyncOperation op = SceneManager.LoadSceneAsync(mLoadSceneName);

    op.allowSceneActivation = false;

    float process = 0.0f;

    //씬 로드가 끝나지 않은 상태라면?
    while (!op.isDone)
    {
        yield return null;

        if (op.progress < 0.9f)
        {
            mProgressBar.fillAmount = op.progress;
        }
        else
        {
            process += Time.deltaTime * 5.0f;
            mProgressBar.fillAmount = Mathf.Lerp(0.9f, 1.0f, process);

            if (process > 1.0f)
            {
                op.allowSceneActivation = true;
                yield break;
            }
        }
    }
}
  • yield return StartCoroutine(Fade(true));를 호출하여 Fade가 우선으로 일어나게 합니다.
  • AsyncOperation op = SceneManager.LoadSceneAsync(mLoadSceneName);를 이용하여 비동기로 씬을 로드하도록 합니다. op의 progress값을 이용하여 얼마나 로딩이 되었는지 확인할 수 있으며, 이 값을 fillAmount에 활용합니다.
  • mProgressBar.fillAmount = op.progress;를 통해 로딩바의 진척도를 갱신합니다.
  • if (op.progress < 0.9f)로 약 90% 정도는 실제 진척도만큼의 로딩바를 보여주고, 나머지 10%는 자연스럽게  차오르는 연출로 구성합니다.
  • 로딩이 완료되면 op.allowSceneActivation = true;을 호출하여 씬을 활성화합니다.

 

private void OnSceneLoaded(Scene arg0, LoadSceneMode arg1)
{
    if (arg0.name == mLoadSceneName)
    {
        StartCoroutine(Fade(false));
        SceneManager.sceneLoaded -= OnSceneLoaded;
    }
}
  • 씬이 로드되었을 때 StartCoroutine(Fade(false));를 호출하여 현재 로딩화면을 서서히 사라지게 합니다.

 

private IEnumerator CoLateStart()
{
    yield return new WaitForEndOfFrame();

    // 예약된 함수 실행
    mOnSceneLoadAction?.Invoke();
}
  • 씬이 로드되면 Awake, Start 등 씬 내에 배치되어 있는 컴포넌트들이 초기화를 진행합니다.
  • 이 상태에서 한 프레임을 쉰 후 예약된 함수를 호출하면 Awake, Start가 모두 실행된 후에 호출되기에 스크립트 실행 순서에 상관없이 가장 마지막에 호출되는 점을 이용하여 안정적인 함수 구성이 가능해집니다.

 

· UI

  • 기본적인 구성입니다.
  • 툴팁을 표시할 라벨과 로딩바 이미지가 포함되어 있습니다.

 

  • 캔버스 프리팹을 이용하여 UI를 구성합니다.
  • LoadingSceneController.cs를 컴포넌트로 사용하고 내부 멤버변수들을 등록시킵니다.
  • ToolTips에 표시할 툴팁들을 적어줍니다.

 

✅ 사용

public void BTN_Load()
{
    DialogBoxController dialogBox = DialogBoxGenerator.Instance.CreateSimpleDialogBox(
        "게임 불러오기", $"{SlotId + 1}번째 게임을 불러옵니까?", "예", "YES",
        (controller, eventArg) =>
        {
            switch (eventArg)
            {
                case "YES":
                    {
                        string fromJson = File.ReadAllText(GameDataSaveLoadManager._FILE_PATH + SlotId);
                        GameDataCore dataCore = JsonUtility.FromJson<GameDataCore>(fromJson);

                        LoadingSceneController.Instance.LoadScene(dataCore.gameBaseInfo.sceneName, () =>
                        {
                            GameDataSaveLoadManager.Instance.LoadGameData(SlotId);
                        });
                        break;
                    }
                case "NO":
                    {
                        break;
                    }
            }

            controller.DestroyBox();
        },
        220, 130, 30, 30);

    dialogBox.AddButton(null, true, "아니오", "NO");
    dialogBox.ModifyBottomLayoutPadding(32, 32, 5, 5, 15);
}

 

LoadingSceneController.Instance.LoadScene(dataCore.gameBaseInfo.sceneName, () =>
{
    GameDataSaveLoadManager.Instance.LoadGameData(SlotId);
});
  • 씬을 불러옵니다. dataCore.gameBaseInfo.sceneName은 불러올 씬의 이름입니다.
  • 두 번째 인자인 람다 함수는 LateStart 시점에 LoadGameData를 호출하여 게임의 데이터를 불러온 게임의 데이터로 적용하도록 합니다.
bonnate