로딩화면은 게임에서 필요한 데이터를 불러오는 동안에 보이는 화면입니다. 게임이 시작될 때, 새로운 씬으로 이동할 때 필요합니다. 로딩화면이 없으면 데이터를 불러오는 동안 화면이 깜빡이거나 멈추는 현상이 발생할 수 있으며, 이는 유저들에게 불편을 초래합니다. 따라서 로딩화면은 게임의 원활한 진행을 위해 필수적입니다.
📺 미리보기
📖 구현 내용
- 다른 신을 로딩할 때 로딩 화면을 보여주어 얼마큼 로딩이 되었는지 볼 수 있습니다.
- 로딩 화면에 설명란을 이용하여 게임 플레이에 도움이 되는 내용을 제공할 수 있습니다.
- 씬 로드 완료 후 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);
}
- 게임을 불러오는 기능에서 함수를 호출하여 로딩 화면을 이용하면서 게임을 로딩할 수 있습니다.
- 게임을 불러오는 기능 구현은 "[유니티] 게임 저장 시스템(1) - 슬롯 구성"에서 확인할 수 있습니다.
LoadingSceneController.Instance.LoadScene(dataCore.gameBaseInfo.sceneName, () =>
{
GameDataSaveLoadManager.Instance.LoadGameData(SlotId);
});
- 씬을 불러옵니다. dataCore.gameBaseInfo.sceneName은 불러올 씬의 이름입니다.
- 두 번째 인자인 람다 함수는 LateStart 시점에 LoadGameData를 호출하여 게임의 데이터를 불러온 게임의 데이터로 적용하도록 합니다.
'unity game modules' 카테고리의 다른 글
[유니티] 오브젝트의 중심을 기준으로 회전 (1) | 2023.04.08 |
---|---|
[유니티] 데미지 표시 (Damage Indicator) (0) | 2023.04.03 |
[유니티] 게임 저장 시스템(3) - 매니저 (0) | 2023.04.02 |
[유니티] 게임 저장 시스템(2) - 저장 및 불러오기 예시 (0) | 2023.04.02 |
[유니티] 게임 저장 시스템(1) - 슬롯 구성 (0) | 2023.04.02 |