오늘 특강으로 너무 많은게 머리에 들어와서 정리하는데 시간을 많이 사용했다.
그래서 가장 매력적이었던 UI시스템을 한번 에셋처럼 틀을 만들어보기로 했다.
(TIL겸 앞으로 사용하면서 개선도 하며 이 글을 계속 볼고 수정할 것이다.)
우선 제네릭 싱글톤을 만들자.
using UnityEngine;
public class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
private static T _instance;
public static T Instance
{
get
{
if (_instance == null)
{
_instance = FindFirstObjectByType<T>();
if (_instance == null)
{
GameObject singletonObject = new GameObject(typeof(T).Name);
_instance = singletonObject.AddComponent<T>();
DontDestroyOnLoad(singletonObject);
}
else
{
DontDestroyOnLoad(_instance.gameObject);
}
}
return _instance;
}
}
protected virtual void Awake()
{
if (_instance == null)
{
_instance = this as T;
DontDestroyOnLoad(this.gameObject);
}
else if (_instance != this)
{
Destroy(gameObject);
}
}
}
그리고 UIManager를 만들어주자
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
public class UIManager : Singleton<UIManager>
{
// 현재 활성화된 캔버스를 참조하기 위한 변수
private Canvas _currentCanvas;
private void OnEnable()
{
// 씬 변경 시 이벤트 등록
SceneManager.sceneLoaded += OnSceneLoaded;
}
private void OnDisable()
{
// 이벤트 핸들러 해제
SceneManager.sceneLoaded -= OnSceneLoaded;
}
private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
// 기존 UI 캐시 정리
ResourceManager.Instance.ClearCache();
// 씬에 대응하는 캔버스 로드
LoadCanvasForScene(scene.name);
}
private void LoadCanvasForScene(string sceneName)
{
// 씬에 대응하는 캔버스 프리팹 로드
Canvas canvasPrefab = Resources.Load<Canvas>($"UI/Canvas_{sceneName}");
if (canvasPrefab != null)
{
// 기존 캔버스가 있다면 제거
if (_currentCanvas != null)
{
Destroy(_currentCanvas.gameObject);
}
// 새로운 캔버스 인스턴스화
_currentCanvas = Instantiate(canvasPrefab);
}
else
{
Debug.LogWarning($"현재 씬에 대응하는 Canvas 프리팹이 없음: {sceneName}. 새로운 프리팹을 생성하겠습니다.");
// 기본 캔버스 생성
if (_currentCanvas == null)
{
GameObject canvasObject = new GameObject("DefaultCanvas");
_currentCanvas = canvasObject.AddComponent<Canvas>();
_currentCanvas.renderMode = RenderMode.ScreenSpaceOverlay;
canvasObject.AddComponent<CanvasScaler>();
canvasObject.AddComponent<GraphicRaycaster>();
DontDestroyOnLoad(canvasObject);
}
}
}
public T Show<T>() where T : UIBase
{
if (_currentCanvas == null)
{
Debug.LogError("현재 캔버스가 설정되지 않았습니다.");
return null;
}
T ui = ResourceManager.Instance.LoadUI<T>();
if (ui != null)
{
// UI의 부모를 현재 캔버스로 설정
ui.transform.SetParent(_currentCanvas.transform, false);
ui.gameObject.SetActive(true);
ui.Initialize();
}
return ui;
}
public void Hide<T>() where T : UIBase
{
T ui = ResourceManager.Instance.GetCachedUI<T>();
if (ui != null)
{
ui.gameObject.SetActive(false);
}
}
}
다음으로 ResourceManager를 만들어주자
using System.Collections.Generic;
using UnityEngine;
public class ResourceManager : Singleton<ResourceManager>
{
// UI 캐시 딕셔너리
private Dictionary<string, UIBase> _uiCache = new Dictionary<string, UIBase>();
// UI 로드 함수
public T LoadUI<T>() where T : UIBase
{
string uiName = typeof(T).Name;
if (_uiCache.ContainsKey(uiName))
{
return _uiCache[uiName] as T;
}
T uiPrefab = Resources.Load<T>($"UI/{uiName}");
if (uiPrefab != null)
{
T uiInstance = Instantiate(uiPrefab);
_uiCache.Add(uiName, uiInstance);
return uiInstance;
}
Debug.LogError($"UI 프리팹을 찾을 수 없음 : {uiName}");
return null;
}
// 캐싱된 UI 가져오기
public T GetCachedUI<T>() where T : UIBase
{
string uiName = typeof(T).Name;
if (_uiCache.ContainsKey(uiName))
{
return _uiCache[uiName] as T;
}
return null;
}
// 캐시 비우기 (씬 변경 시 호출 가능)
public void ClearCache()
{
foreach (var ui in _uiCache.Values)
{
Destroy(ui.gameObject);
}
_uiCache.Clear();
}
}
using UnityEngine;
public abstract class UIBase : MonoBehaviour
{
// UI에서 공통적으로 필요한 기능들을 여기에 정의할 수 있음
public virtual void Initialize() { }
public virtual void Close()
{
// 기본적으로 UI를 비활성화하고, 필요하면 추가적인 정리 작업을 추가
gameObject.SetActive(false);
}
}
여기까지가 거의 동일한 틀이다.
이제부터 사용하기만 하면 된다.
게임을 일시정지 하는 기능을 구현한다고 생각하고 만들어보자.
using UnityEngine;
// ExampleUsage: UIManager를 사용하여 UI를 불러오고 사용하는 예시 클래스
public class ExampleUsage : MonoBehaviour
{
private void Start()
{
// 게임 중 일시정지 메뉴를 동적으로 불러오기
PauseMenuUI pauseMenu = UIManager.Instance.Show<PauseMenuUI>();
// 일시정지 메뉴 초기화
pauseMenu.Initialize();
// 게임을 일시정지 상태로 전환
Time.timeScale = 0f; // 게임 시간 정지
}
}
대충 start에 구현했지만 버튼을 누른거라고 생각하자
하지만 이 코드에서 볼것은 있다.
PauseMenuUI pauseMenu = UIManager.Instance.Show<PauseMenuUI>();
이부분을 호출하게 되면 Show를 호출하는데 이것은 리소스를 불러오지 않았다면 불러오고
만약 불러왔다면 캐싱된것을 사용하고 있다.
그러니 리소스를 불러오는걸 로딩에서 처리한다면 항상 캐싱된 UI를 사용하니 오브젝트 풀링과 비슷한 개념이라고 생각하면 된다.
그 아래의 Initialize부분도 보면 초기화가 필요한 부분을 초기화해주고 있다. 이부분도 로딩때 미리 해준다면 아주 좋다.
using UnityEngine;
using UnityEngine.UI;
public class PauseMenuUI : UIBase
{
public Button ResumeButton;
public Button SettingsButton;
public Button MainMenuButton;
public override void Initialize()
{
// 버튼 이벤트 초기화
ResumeButton.onClick.RemoveAllListeners();
ResumeButton.onClick.AddListener(OnResumeClicked);
SettingsButton.onClick.RemoveAllListeners();
SettingsButton.onClick.AddListener(OnSettingsClicked);
MainMenuButton.onClick.RemoveAllListeners();
MainMenuButton.onClick.AddListener(OnMainMenuClicked);
}
private void OnResumeClicked()
{
// UI를 닫는 로직 처리
Close(); // Close() 메서드 호출
}
private void OnSettingsClicked()
{
// 설정 UI를 여는 로직을 여기에 추가할 수 있음
}
private void OnMainMenuClicked()
{
// 메인 메뉴로 이동하는 로직을 여기에 추가할 수 있음
}
public override void Close()
{
base.Close(); // 부모 클래스의 Close() 호출로 UI 비활성화
// 게임 시간을 재개
Time.timeScale = 1f;
}
}
이제 사용에 문제가 없을것이다.
Close를 분리해놓은건 혹시 모를 확장을 고려하여 분리를 해두었다.
물론 문제가 없는 곳에는 그냥 BaseUI집어넣어서 오버라이드도 안하고 꺼버려도 된다.ㅋ
프리팹은 "Resources/UI/{name}" 방식으로 넣어주자.
캔버스는 "Canvas_{SceneName}"