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 { /// /// 게임 전체에서 사용되는 로딩 UI를 제어하는 클래스입니다. /// 싱글톤(Singleton)과 유사한 방식으로 구현되어 어디서든 쉽게 접근하고 사용할 수 있습니다. /// [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; /// /// 로딩 화면을 표시합니다. /// 만약 로딩 화면이 아직 생성되지 않았다면, 프리팹을 이용해 새로 생성합니다. /// public static void Show() { // root에서 UILoading 인스턴스를 찾습니다. var loadings = FindObjectsByType(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(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(); } // 인스턴스의 ShowLoading 메서드를 호출하여 페이드인 애니메이션을 시작합니다. instance.ShowLoading(); } /// /// 현재 표시되고 있는 로딩 화면을 숨깁니다. /// 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 /// /// 스크립트 인스턴스가 처음 로드될 때 호출됩니다. /// 변수 초기화에 사용됩니다. /// private void Awake() { // Image 컴포넌트의 Transform을 미리 찾아 변수에 저장해두어, // LateUpdate에서 매번 찾는 비용을 절약합니다. loadingImageTransform = loadinImage.transform; } /// /// 로딩 화면을 나타나게 하는 애니메이션을 시작합니다. /// public void ShowLoading() { // 이미 애니메이션이 진행 중이고, 목표가 '나타나기(target=1)'라면 중복 실행을 방지합니다. if (animatting && target == 1) return; target = 1; // 목표 알파 값을 1(불투명)로 설정 animatting = true; // 애니메이션 시작 상태로 변경 StopCoroutine("Animate"); // 이전에 실행 중이던 Animate 코루틴이 있다면 중지 StartCoroutine(Animate()); // Animate 코루틴을 새로 시작 } /// /// 로딩 화면을 사라지게 하는 애니메이션을 시작합니다. /// public void HideLoading() { // 이미 애니메이션이 진행 중이고, 목표가 '사라지기(target=0)'라면 중복 실행을 방지합니다. if (animatting && target == 0) return; target = 0; // 목표 알파 값을 0(투명)으로 설정 animatting = true; // 애니메이션 시작 상태로 변경 StopCoroutine("Animate"); // 이전에 실행 중이던 Animate 코루틴이 있다면 중지 StartCoroutine(Animate()); // Animate 코루틴을 새로 시작 } /// /// CanvasGroup의 알파 값을 부드럽게 변경하여 페이드인/아웃 효과를 주는 코루틴입니다. /// 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); } } /// /// 모든 Update 함수가 호출된 후에 프레임마다 호출됩니다. /// 주로 카메라 이동이나 다른 애니메이션이 끝난 후 최종적으로 UI를 업데이트할 때 사용되어, /// 떨림(Jitter) 현상을 방지하는 데 도움이 됩니다. /// 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); } } } }