Files
XRLib/Assets/Scripts/UVC/UI/Loading/UILoading.cs
2025-08-04 20:15:20 +09:00

237 lines
11 KiB
C#

using System.Collections;
using UnityEditor;
using UnityEngine;
using UnityEngine.UI;
/*
* UILoading 스크립트 사용법 예시
*
* 다른 스크립트에서 아래와 같이 간단하게 호출하여 로딩 화면을 표시하거나 숨길 수 있습니다.
*
* public class MyGameManager : MonoBehaviour
* {
* public void LoadNextScene()
* {
* // 로딩 화면 표시
* UVC.UI.Loading.UILoading.Show();
*
* // 코루틴을 사용하여 비동기 씬 로딩 시작
* StartCoroutine(LoadSceneAsync());
* }
*
* private IEnumerator LoadSceneAsync()
* {
* // 씬을 비동기로 로드합니다.
* AsyncOperation asyncLoad = UnityEngine.SceneManagement.SceneManager.LoadSceneAsync("NextSceneName");
*
* // 씬 로딩이 완료될 때까지 대기합니다.
* while (!asyncLoad.isDone)
* {
* yield return null;
* }
*
* // 씬 로딩이 완료되면 로딩 화면을 숨깁니다.
* UVC.UI.Loading.UILoading.Hide();
* }
* }
*/
namespace UVC.UI.Loading
{
/// <summary>
/// 게임 전체에서 사용되는 로딩 UI를 제어하는 클래스입니다.
/// 싱글톤(Singleton)과 유사한 방식으로 구현되어 어디서든 쉽게 접근하고 사용할 수 있습니다.
/// </summary>
[RequireComponent(typeof(CanvasGroup))]
public class UILoading : MonoBehaviour
{
// 로딩 UI 프리팹이 Resources 폴더 내에 위치하는 경로입니다.
// Resources.Load를 통해 동적으로 프리팹을 불러올 때 사용됩니다.
public static string PrefabPath = "Prefabs/UI/Loading/UILoading";
// UILoading 클래스의 유일한 인스턴스를 저장하는 정적(static) 변수입니다.
// 이 변수를 통해 다른 모든 스크립트에서 동일한 로딩 화면 인스턴스에 접근할 수 있습니다.
private static UILoading instance;
/// <summary>
/// 로딩 화면을 표시합니다.
/// 만약 로딩 화면이 아직 생성되지 않았다면, 프리팹을 이용해 새로 생성합니다.
/// </summary>
public static void Show()
{
// root에서 UILoading 인스턴스를 찾습니다.
var loadings = FindObjectsByType<UILoading>(FindObjectsSortMode.None);
if(loadings.Length > 0)
{
if (loadings.Length > 1)
{
for (int i = loadings.Length - 1; i > 0; i++)
{
DestroyImmediate(loadings[i].gameObject);
}
}
// 이미 UILoading 인스턴스가 존재한다면, 첫 번째 인스턴스를 사용합니다.
instance = loadings[0];
}
// instance가 null일 경우, 아직 로딩 화면이 만들어지지 않았다는 의미입니다.
if (instance == null)
{
// Resources 폴더에서 프리팹을 불러옵니다.
GameObject prefab = Resources.Load<GameObject>(PrefabPath);
if (prefab == null)
{
Debug.LogError($"UILoading prefab not found at path: {PrefabPath}");
return;
}
// 불러온 프리팹을 씬에 인스턴스(복제)하여 생성합니다.
GameObject go = Instantiate(prefab);
// 생성된 GameObject의 이름을 "UILoading"으로 설정하여 씬에서 쉽게 식별할 수 있도록 합니다.
go.name = "UILoading";
// 부모를 null로 설정하여 씬의 최상위 계층에 위치시킵니다.
// 이렇게 하면 다른 씬으로 전환될 때 함께 파괴되지 않도록 관리하기 용이합니다. (DontDestroyOnLoad와 함께 사용 가능)
go.transform.SetParent(null, false);
// 생성된 GameObject에서 UILoading 컴포넌트를 찾아 instance 변수에 할당합니다.
instance = go.GetComponent<UILoading>();
}
// 인스턴스의 ShowLoading 메서드를 호출하여 페이드인 애니메이션을 시작합니다.
instance.ShowLoading();
}
/// <summary>
/// 현재 표시되고 있는 로딩 화면을 숨깁니다.
/// </summary>
public static void Hide()
{
// instance가 null이 아닐 경우에만 (즉, 로딩 화면이 존재할 때만) 실행합니다.
if (instance != null)
{
// 인스턴스의 HideLoading 메서드를 호출하여 페이드아웃 애니메이션을 시작합니다.
instance.HideLoading();
}
}
// --- 인스펙터(Inspector)에서 설정할 변수들 ---
[Header("Component References")]
[Tooltip("페이드 효과를 제어할 CanvasGroup 컴포넌트")]
[SerializeField]
private CanvasGroup canvasGroup;
[Tooltip("채우기(Fill) 및 회전 효과를 적용할 Image 컴포넌트")]
[SerializeField]
private Image loadinImage;
// --- 내부 동작을 위한 변수들 ---
[Header("Animation Settings")]
[Tooltip("페이드인/아웃 애니메이션의 지속 시간 (초)")]
[SerializeField]
private float duration = 0.25f;
[Tooltip("로딩 아이콘의 회전 속도")]
[SerializeField]
private float loadingSpeed = -1.5f;
[Tooltip("이미지 채우기(Fill) 효과의 속도")]
[SerializeField]
private float rotationSpeed = -1.0f;
private float target = 0; // 애니메이션의 목표 알파 값 (0: 투명, 1: 불투명)
private bool animatting = false; // 현재 애니메이션이 진행 중인지 여부
private Transform loadingImageTransform; // 회전 애니메이션을 적용할 이미지의 Transform
/// <summary>
/// 스크립트 인스턴스가 처음 로드될 때 호출됩니다.
/// 변수 초기화에 사용됩니다.
/// </summary>
private void Awake()
{
// Image 컴포넌트의 Transform을 미리 찾아 변수에 저장해두어,
// LateUpdate에서 매번 찾는 비용을 절약합니다.
loadingImageTransform = loadinImage.transform;
}
/// <summary>
/// 로딩 화면을 나타나게 하는 애니메이션을 시작합니다.
/// </summary>
public void ShowLoading()
{
// 이미 애니메이션이 진행 중이고, 목표가 '나타나기(target=1)'라면 중복 실행을 방지합니다.
if (animatting && target == 1) return;
target = 1; // 목표 알파 값을 1(불투명)로 설정
animatting = true; // 애니메이션 시작 상태로 변경
StopCoroutine("Animate"); // 이전에 실행 중이던 Animate 코루틴이 있다면 중지
StartCoroutine(Animate()); // Animate 코루틴을 새로 시작
}
/// <summary>
/// 로딩 화면을 사라지게 하는 애니메이션을 시작합니다.
/// </summary>
public void HideLoading()
{
// 이미 애니메이션이 진행 중이고, 목표가 '사라지기(target=0)'라면 중복 실행을 방지합니다.
if (animatting && target == 0) return;
target = 0; // 목표 알파 값을 0(투명)으로 설정
animatting = true; // 애니메이션 시작 상태로 변경
StopCoroutine("Animate"); // 이전에 실행 중이던 Animate 코루틴이 있다면 중지
StartCoroutine(Animate()); // Animate 코루틴을 새로 시작
}
/// <summary>
/// CanvasGroup의 알파 값을 부드럽게 변경하여 페이드인/아웃 효과를 주는 코루틴입니다.
/// </summary>
private IEnumerator Animate()
{
float start = canvasGroup.alpha; // 현재 알파 값을 시작 값으로 설정
float time = 0;
// 경과 시간이 duration에 도달할 때까지 반복합니다.
while (time < duration)
{
// Time.unscaledDeltaTime: Time.timeScale 값에 영향을 받지 않는 시간 간격입니다.
// 로딩 중 Time.timeScale이 0이 되어도 UI 애니메이션은 계속 실행되어야 하므로 사용합니다.
time += Time.unscaledDeltaTime;
// Mathf.Lerp(시작값, 목표값, 진행률): 두 값 사이를 선형 보간합니다.
// 진행률(time / duration)에 따라 부드럽게 알파 값이 변합니다.
canvasGroup.alpha = Mathf.Lerp(start, target, time / duration);
// 다음 프레임까지 대기합니다.
yield return null;
}
// 애니메이션이 끝난 후 목표 알파 값으로 정확히 맞춰줍니다.
canvasGroup.alpha = target;
animatting = false; // 애니메이션 종료 상태로 변경
// 만약 목표가 '사라지기(target=0)'였다면, 애니메이션 종료 후 GameObject를 파괴합니다.
if (target == 0)
{
Destroy(gameObject);
}
}
/// <summary>
/// 모든 Update 함수가 호출된 후에 프레임마다 호출됩니다.
/// 주로 카메라 이동이나 다른 애니메이션이 끝난 후 최종적으로 UI를 업데이트할 때 사용되어,
/// 떨림(Jitter) 현상을 방지하는 데 도움이 됩니다.
/// </summary>
private void LateUpdate()
{
// 로딩 화면이 완전히 불투명할 때(애니메이션이 진행 중일 때)만 실행합니다.
if (canvasGroup.alpha == 1)
{
// 1. 이미지 회전 애니메이션
// Time.unscaledTime: Time.timeScale에 영향을 받지 않는 전체 게임 시간입니다.
// 시간에 비례하는 절대적인 회전 각도를 계산하여 매 프레임 설정합니다.
// 이렇게 하면 프레임 속도 변화에 관계없이 항상 일관되고 부드러운 회전을 보장할 수 있습니다.
float zRotation = Time.unscaledTime * loadingSpeed * 360;
loadingImageTransform.rotation = Quaternion.Euler(0, 0, zRotation);
// 2. 이미지 채우기(Fill) 애니메이션 (필요 시 주석 해제하여 사용)
// Mathf.PingPong(시간, 길이): 0과 '길이' 사이를 왕복하는 값을 반환합니다.
// 여기서는 fillAmount가 0과 1 사이를 계속 오가도록 하여 채워졌다 비워지는 효과를 만듭니다.
loadinImage.fillAmount = Mathf.PingPong(Time.unscaledTime * rotationSpeed, 1);
}
}
}
}