며칠전에 UI를 UIPopup, UIBase, UIManager의 구조로 각각 어떤 기능이
들어가면 좋을지 적어봤었다. 그래서 오늘은 이를 토대로 UI 관련 스크립트를
제작했고 현재 어떤 부분이 어떤 기능을 하는지 적어보며 회고하려한다.
1. UI_Base
public class UI_Base : MonoBehaviour
{
protected bool _init = false;
public virtual bool Init()
{
if (_init) return false;
return _init = true;
}
private void Start()
{
if (_init) return;
Init();
}
}
UI_Base에는 UI들이 기본적으로 가지고 있어야 할 공통적인 기능들을 넣고자했다.
먼저 생각났던 것은 UI_Base를 상속받는 UI들의 초기화 부분이었다.
모든 UI스크립트에서 필요한 Init 기능을 만약 UI_Base가아닌 각각 만드는 구조였다면
엄청난 중복코드와 일관성의 부족이 일어날 것이다. 따라서 이 부분을 최우선으로
생각하여 구현했다.
아직 개발 초여서 정확하게 어떤 기능이 공통적으로 많이 쓰이는지 불확실 하기 때문에
팀원들과 논의하여 추가적으로 어떤 기능이 추가돼야 할지 고민해봐야겠다.
2. UI_Popup
public class UI_Popup : UI_Base
{
public override bool Init()
{
if (!base.Init()) return false;
GameManager.Instance.ui_Manager.SetCanvas(gameObject);
return true;
}
}
팝업의 기능을 하는 UI들이 상속받아야 할 스크립트 UI_Popup이다.
UI_Base를 상속받고 Init함수를 오버라이드하여 재정의했다.
부모인 UI_Base의 Init함수가 false면 false를 리턴하여 종료하고
아니라면 SetCanvas를 실행한다.
여기서 UI_Base의 Init함수가 false라는건 현재 UI_Base의 _init변수가 true라는 것이므로
이미 초기화가 진행됐기에 false를 리턴한다.
<SetCanvas>
팝업과 같이 여러번 UI가 켜질 때 이를 올바르게 표시하기 위해서 현재 gameObject 즉, 팝업의
캔버스를 설정하는 함수를 구현해줬다.
3. UI_Scene
public class UI_Scene : UI_Base
{
public override bool Init()
{
if (!base.Init()) return false;
GameManager.Instance.ui_Manager.SetCanvas(gameObject);
return true;
}
}
UI_Popup과 UI_Base로 나눠서 팝업을 하는 UI와 안하는 UI, 이렇게 두가지로 나누려했다.
하지만 생각해보니 각 Scene이 처음부터 담당하는 UI들을 처리하는 방법이 있어야했다.
예를 들어 게임이 진행되면서 동적으로 변해야할 UI인 날짜, 체력, 기력 등 을 말한다.
그래서 이런 UI들을 사용할 UI_Scene을 상속받는 프리팹을 만들어서 이 프리팹 하나로
모든 UI를 관리하는 스크립트를 제작하려고 진행중이다.
이를 위해서 만든 것이 UI_Scene이며 이 또한 여러 UI가 동시에 출력되므로
올바른 UI 표시를 위해 SetCanvas를 진행해주었다.
4. UI_Manager
public class UI_Manager : MonoBehaviour
{
private Stack<UI_Popup> _popupStack = new Stack<UI_Popup>();
private List<UI_Popup> _popupList = new List<UI_Popup>();
public UI_Scene curSceneUI { get; private set; }
public GameObject Root
{
get
{
GameObject root = GameObject.Find("UI_Root");
if (root == null) root = new GameObject { name = "UI_Root" };
return root;
}
}
public void SetCanvas(GameObject go)
{
Canvas canvas = go.GetOrAddComponent<Canvas>();
canvas.sortingOrder = go.GetComponent<Canvas>().sortingOrder;
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
canvas.overrideSorting = true;
}
public T MakeSubItem<T> (string name = null, Transform parent = null) where T : UI_Base
{
if (string.IsNullOrEmpty(name)) name = typeof(T).Name;
GameObject loadPrefab = Resources.Load<GameObject>($"Prefabs/UI/SubItem/{name}");
if (loadPrefab == null)
{
Debug.Log($"Failed to load : Prefabs/UI/SubItem/{name}");
return null;
}
GameObject subItemPrefab = Instantiate(loadPrefab, parent);
T subItemUI = subItemPrefab.GetComponent<T>();
if (subItemUI == null) subItemUI = subItemPrefab.AddComponent<T>();
if (parent != null) subItemPrefab.transform.SetParent(parent);
subItemPrefab.transform.localScale = Vector3.one;
return subItemUI;
}
public T ShowScene<T> () where T : UI_Scene
{
GameObject loadPrefab = Resources.Load<GameObject>($"Prefabs/UI/Scene/{name}");
if (loadPrefab == null)
{
Debug.Log($"Failed to load : Prefabs/UI/Scene/{name}");
return null;
}
GameObject scenePrefab = Instantiate(loadPrefab);
T sceneUI = scenePrefab.GetComponent<T>();
if (sceneUI == null) sceneUI = scenePrefab.AddComponent<T>();
curSceneUI = sceneUI;
scenePrefab.transform.SetParent(Root.transform);
return sceneUI;
}
public T ShowPopup<T>(string name = null, Transform parent = null) where T : UI_Popup
{
if(string.IsNullOrEmpty(name)) name = typeof(T).Name;
foreach (UI_Popup uiPopup in _popupStack)
{
if(uiPopup.name == name) return uiPopup as T;
}
GameObject loadPrefab = Resources.Load<GameObject>($"Prefabs/UI/Popup/{name}");
if (loadPrefab == null)
{
Debug.Log($"Failed to load : Prefabs/UI/Popup/{name}");
return null;
}
GameObject popupPrefab = Instantiate(loadPrefab, parent);
T popupUI = popupPrefab.GetComponent<T>();
if(popupUI == null) popupUI = popupPrefab.AddComponent<T>();
_popupStack.Push(popupUI);
if (parent != null) popupPrefab.transform.SetParent(parent);
else popupPrefab.transform.SetParent(Root.transform);
popupPrefab.transform.localScale = Vector3.one;
return popupUI;
}
public void ClosePopup(UI_Popup popup)
{
if (_popupStack.Count == 0)
{
return;
}
if (_popupStack.Peek() != popup)
{
Debug.Log("Peek != popup");
return;
}
ClosePopup();
}
public void ClosePopup()
{
if (_popupStack.Count == 0)
{
return;
}
UI_Popup popup = _popupStack.Pop();
if (popup != null)
{
Destroy(popup.gameObject);
}
}
public void CloseAllPopup()
{
while (_popupStack.Count > 0) ClosePopup();
}
public void Clear()
{
CloseAllPopup();
curSceneUI = null;
}
}
UIManager의 팝업 부분을 스택으로 관리하는 방법이 많이 보여서 스택으로 한 번 제작해봤다.
내가 생각한 스택의 장점은 팝업의 열고, 닫기 기능이 스택의 구조와 잘 맞아서 관리하기 편했고
스택의 메서드가 추가기능을 구현하는데 용이했다. 하지만 단점으로는 구조적인만큼 유연성이 부족하다.
만약 팝업이 가장 최근 UI가 꺼지는 스택의 LIFO(후입선출)형식이 아니라면 팝업의 데이터가 불안정해지고
메서드가 올바르게 실행되지 않을 것이다.
그래서 나중에 이 부분은 딕셔너리나 리스트로 바꿔서 구조의 유연성을 증가해볼 예정이다.
3 - 1. SetCanvas
public void SetCanvas(GameObject go)
{
Canvas canvas = go.GetOrAddComponent<Canvas>();
canvas.sortingOrder = go.GetComponent<Canvas>().sortingOrder;
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
canvas.overrideSorting = true;
}
이전에 말했듯이 UI가 올바르게 표시되기 위한 과정이다.
캔버스가 이미 있다면 캔버스 컴포넌트를 가져오고 없다면 Add해준다.
새로 생긴 sotringOrder를 기존 sortingOrder로 바꿔주고, ScreenSpaceOverlay로 렌더링을 일관되게 만들어준다.
마지막으로 sortingOrder의 재정의 여부를 true로 바꿔서 부모의 sotringOrder를 무시하고 자체적으로 sortingOrder를
사용할 수 있게 해줬다.
3 - 2. MakeSubItem
public T MakeSubItem<T> (string name = null, Transform parent = null) where T : UI_Base
{
if (string.IsNullOrEmpty(name)) name = typeof(T).Name;
GameObject loadPrefab = Resources.Load<GameObject>($"Prefabs/UI/SubItem/{name}");
if (loadPrefab == null)
{
Debug.Log($"Failed to load : Prefabs/UI/SubItem/{name}");
return null;
}
GameObject subItemPrefab = Instantiate(loadPrefab, parent);
T subItemUI = subItemPrefab.GetComponent<T>();
if (subItemUI == null) subItemUI = subItemPrefab.AddComponent<T>();
if (parent != null) subItemPrefab.transform.SetParent(parent);
subItemPrefab.transform.localScale = Vector3.one;
return subItemUI;
}
SubItem들의 정보를 가져오는 기능이다.
name이 설정되지 않았다면 제네릭 타입을 프로퍼티로 Name속성을 가져와서
프리팹을 로드한다. 그리고 로드한 GameObject 프리팹 데이터를 인스턴스화하여
위치를 parent로 설정해준다.
여기서 주의해야 할 점은 로드한 프리팹 원본데이터를 수정하면 안된다.
예를 들어 Instantiate한 GameObject가 아닌 리소스를 로드한 GameObject를 수정하면
유니티 자체에서 오류가 나게된다. 그렇기 때문에 복사본인 인스턴스화 오브젝트를 사용하여
데이터 수정을 진행해야한다.
3 - 3. ShowScene
public T ShowScene<T> () where T : UI_Scene
{
GameObject loadPrefab = Resources.Load<GameObject>($"Prefabs/UI/Scene/{name}");
if (loadPrefab == null)
{
Debug.Log($"Failed to load : Prefabs/UI/Scene/{name}");
return null;
}
GameObject scenePrefab = Instantiate(loadPrefab);
T sceneUI = scenePrefab.GetComponent<T>();
if (sceneUI == null) sceneUI = scenePrefab.AddComponent<T>();
curSceneUI = sceneUI;
scenePrefab.transform.SetParent(Root.transform);
return sceneUI;
}
Scene에서 사용하는 UI를 관리하는 프리팹을 Root에다 인스턴스화 해주는 기능이다.
MakeSubITem의 메서드와 비슷하며 차이점으로는 SceneUI는 루트 위치에 생성된다.
3 - 4. ShowPopup
public T ShowPopup<T>(string name = null, Transform parent = null) where T : UI_Popup
{
if(string.IsNullOrEmpty(name)) name = typeof(T).Name;
foreach (UI_Popup uiPopup in _popupStack)
{
if(uiPopup.name == name) return uiPopup as T;
}
GameObject loadPrefab = Resources.Load<GameObject>($"Prefabs/UI/Popup/{name}");
if (loadPrefab == null)
{
Debug.Log($"Failed to load : Prefabs/UI/Popup/{name}");
return null;
}
GameObject popupPrefab = Instantiate(loadPrefab, parent);
T popupUI = popupPrefab.GetComponent<T>();
if(popupUI == null) popupUI = popupPrefab.AddComponent<T>();
_popupStack.Push(popupUI);
if (parent != null) popupPrefab.transform.SetParent(parent);
else popupPrefab.transform.SetParent(Root.transform);
popupPrefab.transform.localScale = Vector3.one;
return popupUI;
}
ShowPopup 메서드도 위와 기능은 비슷하다.
다른점으로는 스택으로 팝업UI를 관리하고 있는 부분인데
스택을 탐색하면서 만약 현재 스택에 똑같은 팝업이 존재한다면 기존 팝업을 제네릭 T로 캐스팅하여 리턴해주고
아니라면 프로퍼티 Name속성을 기준으로 프리팹 데이터를 불러와 인스턴스화 해주고
복사한 데이터를 스택에 Push 해준다.
3 - 5. ClosePopup
public void ClosePopup()
{
if (_popupStack.Count == 0)
{
return;
}
UI_Popup popup = _popupStack.Pop();
if (popup != null)
{
Destroy(popup.gameObject);
}
}
현재 스택에 존재하는 팝업이 없다면 종료해주고
존재한다면 스택을 Pop하고 받은 UI_Popup데이터를 Destroy해준다.
<해야할 것>
스택을 리스트로 변환
UI_Base에 추가할 내용 생각해보기
UIManager에서 공통적으로 사용할만한 메서드를 찾아보기
'프로젝트 기록 > Project N' 카테고리의 다른 글
창고 개발 기록 (인벤토리 데이터 불러오기) (0) | 2024.04.04 |
---|---|
인벤토리 개발 기록 (창고와 인벤토리) (0) | 2024.04.03 |
UIManager는 왜 만드는 걸까? (0) | 2024.02.07 |
인벤토리 기능 MVC 적용 (0) | 2024.02.01 |
MVC 패턴에 대한 이해 (1) | 2024.01.30 |