프로젝트에서 UI의 전반적인 관리를 맡아서 팝업 형식의 UI를 제작하던 중, 팝업 형식의 UI는 경우 여러 시스템, 씬에서 사용될 것으로 생각해 팝업식 UI의 호출 방식을 조금 더 확장성 있게 구현하고 싶었다.
때문에 시스템의 구조를 UI 매니저를 통해서 싱글톤으로 원하는 팝업의 정보만 준다면 어디서든지 팝업을 생성하고, 한번 생성된 팝업은 다시 열 때도 계속 사용할 수 있도록 유지되게 만들었다.
아래는 혹시 구조를 다시 참고할 일이 있을지 몰라 기록 용으로 전문을 첨부한다.
public class UIManager : MonoSingleton<UIManager>
{
// 확장성을 고려한 string을 통한 제네릭 메서드 호출 + Awake 에서 런타임에 Resources 파일을 통해 자동으로 등록.
// ShowPopupByName() 에서 string 값을 입력하면 Type을 가져올 수 있게함.
private Dictionary<string, System.Type> popupTypeMap = new();
// 현재 씬 내 팝업 인스턴스 저장
private Dictionary<string, BasePopupUI> popupInstances = new Dictionary<string, BasePopupUI>();
private const string popupPath = "UI/Popup/";
private Transform popupRoot;
// PopUp 형식의 UI Stack
public Stack<BasePopupUI> popupStack = new Stack<BasePopupUI>();
protected override void Awake()
{
RegisterAllPopupsInResources();
}
private void Update()
{
if (Input.GetKeyDown(KeyCode.Escape) && popupStack.Count > 0)
{
BasePopupUI popup = popupStack.Peek();// Close() 메서드 내부에 Pop() 존재.
popup.Close();
}
}
private void OnEnable()
{
SceneManager.sceneLoaded += OnSceneLoaded;
}
private void OnDisable()
{
SceneManager.sceneLoaded -= OnSceneLoaded;
}
private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
popupInstances.Clear();// 씬이 바뀔 때 팝업 인스턴스 초기화
popupStack.Clear();// 팝업 스택 초기화
GameObject popupRootObj = GameObject.Find("Canvas/PopupUI");
if (popupRootObj == null)
{
Debug.Log("PopupUI 의 캔버스가 없습니다.");
popupRoot = null;
}
else
{
popupRoot = popupRootObj.transform;
}
}
public void ShowPopupByName(string popupName)
{
if (!popupTypeMap.TryGetValue(popupName, out var popupType))
{
Debug.LogError($"[UIManager] '{popupName}'에 해당하는 팝업 타입이 없습니다.");
return;
}
MethodInfo showPopupGeneric = typeof(UIManager)
.GetMethod(nameof(ShowPopup), BindingFlags.Public | BindingFlags.Instance);
MethodInfo showPopupTyped = showPopupGeneric.MakeGenericMethod(popupType);
showPopupTyped.Invoke(this, null);
}
/// <summary>
/// 팝업 Open 매서드(컴포넌트 기준)
/// </summary>
public T ShowPopup<T>() where T : BasePopupUI
{
string popupName = typeof(T).Name;
// 이미 인스턴스가 있다면 재사용
if (popupInstances.TryGetValue(popupName, out BasePopupUI existingPoupup))
{
existingPoupup.Open();
existingPoupup.gameObject.transform.SetAsLastSibling(); // 팝업이 열릴 때 가장 위로 오도록 설정
return existingPoupup as T;
}
// Resources에서 프리팹 캐싱
GameObject prefab = Resources.Load<GameObject>($"{popupPath}{popupName}");
if (prefab == null)
{
Debug.LogError($"Resources에 '{popupName}'을 찾을 수 없습니다.");
return null;
}
// 캐싱된 프리팹 생성
GameObject popupObject = Instantiate(prefab, popupRoot);
T popup = popupObject.GetComponent<T>();
if (popup == null)
{
Debug.LogError($"프리팹에 {typeof(T).Name} 컴포넌트가 없습니다.");
Destroy(popupObject);
return null;
}
popupInstances[popupName] = popup;
popup.Open();
popup.gameObject.transform.SetAsLastSibling(); // 팝업이 열릴 때 가장 위로 오도록 설정
return popup;
}
/// <summary>
/// 팝업 Close 매서드
/// </summary>
public void ClosePopup<T>() where T : BasePopupUI
{
string popupName = typeof(T).Name;
if (popupInstances.TryGetValue(popupName, out BasePopupUI popup))
{
popup.Close();
}
}
/// <summary>
/// Resources 폴더에서 모든 팝업 프리팹의 Type을 등록.
/// </summary>
private void RegisterAllPopupsInResources()
{
GameObject[] popupPrefabs = Resources.LoadAll<GameObject>(popupPath);
foreach (GameObject prefab in popupPrefabs)
{
if (prefab.TryGetComponent<BasePopupUI>(out var popup))
{
string popupName = prefab.name;
System.Type popupType = popup.GetType();
if (!popupTypeMap.ContainsKey(popupName))
{
popupTypeMap.Add(popupName, popupType);
// Debug.Log($"[UIManager] 등록됨: {popupName} → {popupType}");
}
}
else
{
Debug.LogWarning($"[UIManager] {prefab.name} 프리팹에 BasePopupUI가 없음 → 등록되지 않음");
}
}
}
}
여기서 팝업 UI를 호출하는 방식은, Awake() 단계에서 Resources 파일에 있는 팝업 오브젝트를 보관한 파일에서 팝업 오브젝트의 정보를 가져와 오브젝트의 이름 + 타입으로 딕셔너리에 저장하고
private Dictionary<string, System.Type> popupTypeMap = new();
이렇게 저장한 딕셔너리의 정보를 통해, 외부에서 팝업을 버튼식 또는 미리 입력받은 text 값으로 원하는 팝업의 타입을 매개변수에 입력해 사용해 주면
ShowPopupByName(string popupName)
이 매서드를 통해 입력한 타입의 오브젝트를
1. 생성 + 생성한 오브젝트 딕셔너리에 저장 을 하거나
2. 이미 생성된 오브젝트를 활성화
하는 방식을 통해 불러오며, 이렇게 불러올 때 효율적으로 팝업 닫기를 하기 위해
public Stack <BasePopupUI> Stack <BasePopupUI> popupStack = new Stack <BasePopupUI>();
if (Input.GetKeyDown(KeyCode.Escape) && popupStack.Count > 0)
{
BasePopupUI popup = popupStack.Peek();// Close() 메서드 내부에 Pop() 존재.
popup.Close();
}
스택을 통해 ESC 키 또는 직접 Pop을 해서 비활성화를 해 줄 수 있게 만들었다.
이렇게 해 주면, 특정 버튼에 string 값을 부여한 상태에서 누를 시 동일한 타입의 팝업을 호출할 수 있고, ESC로 닫을 수 있다.
'C# > 게임 제작 + TIL' 카테고리의 다른 글
일기장 정보 불러오기 (Json 활용) (0) | 2025.04.23 |
---|---|
카드 정보 UI (Dictionary 활용) (0) | 2025.04.21 |
Dotween OnComplete() - 람다에서 루프 변수 참조시 변수 값 캐싱 (0) | 2025.04.08 |
IDragHandler - UI 드래그 및 곡선 화살표 (0) | 2025.04.07 |
Physics2D.OverlapPoint - 2D 상의 겹침의 정도 (0) | 2025.04.02 |