개발 조각글

Unity Refactoring - 달팽이키우기 UIManager 설계

BaekNohing 2022. 8. 2. 23:49
a cute cat snail


달팽이 키우기 리팩토링 목차

  1. 리팩토링 준비
  2. 구조 재설계
  3. UI Manager 설계
  4. 클래스간 관계
  5. Action 분리하기
  6. 핵심루프 설계
  7. 유닛 테스트

Unity Refactoring - 달팽이키우기 UIManager 설계요약
UI요소의 On/Off를 UIManager에서 일괄적으로 관리할 수 있도록 했으며, 활성화된 패널번호를 스택에 넣어서 Stack.Pieek()을 이용해 맨 위부터 순차적으로 꺼질 수 있도록 했다.

지난번에 정리하면서 번뜩였던 부분을 토대로 UI부분을 정리해봤다. 가장 먼저 정리한 부분은 Btn과 Popup 부분인데, ViewStack을 만들어서, 버튼을 눌러 팝업을 열면 열려있는 팝업을 ViewStack에서 관리할 수 있도록 하고 싶었다 (그래서, 백스페이스 등을 눌렀을 때. 순서대로 하나씩 닫을 수 있도록)


UIManager의 작동방식은 다음과 같은데.

  1. 먼저, 정의된 SelfManageButton : Button과 UIPanels : Monobehaviour 클래스를 씬 내부의 버튼들과 팝업창들에 컴포넌트로 추가해준다.
  2. 그 다음, FindAllT<T> 를 이용해 해당 클래스를 전부 긁어준다. 이때 제네릭 한정자 (where T : ) 부분을 class로 해두어야 범용성있게 긁어올 대상을 정할 수 있다.
  3. 그리고 긁어온 SelfManageButton과 UIPanels들을 LinkBtnPnl을 이용해 SerializableDictionary<SelfManageButton, UIPanels>에 하나씩 짝지어서 넣어준다. (SerializableDictionary는 인스펙터에 내용물을 보여주는 Dictionary로써 나머지는 Dictionary와 동일하다) (이 때, 짝지어진 순서에 따라 각 UIPanels들은 자신의 index번호를 갖게 된다.)[각주:1]
  4. 짝을 지어주는 순간에 버튼에 타겟이 되는 패널을 여는 액션을 넣어준다.
위의 규칙에 따라 짝지어진 버튼과 패널들

아래의 소스코드를 보면 알 수 있지만, On/Off를 하는 액션 자체는 Dictionary를 참조하지 않기때문에 (패널의 Index만 가지고 FintAllT<>를 통해 가져온 pnlList에 접근한다) Dictionary가 필요해보이지 않을 수 있지만, 내가 Dictionay를 사용한 이유는 다음과 같다.
먼저 Dictionary는 특성상 중복된 키를 가질 수 없기 때문에, 한 버튼이 > 두개의 패널을 여는 일을 자연스럽게 막을 수 있다. (또한 값을 차지하는 패널은 중복될 수 있으므로, 한 패널이 두개 이상의 버튼으로부터 열리는 것은 가능하다 = 숏컷 등 의외로 필요한 상황이 있다.) 즉, UI버튼을 만들 때 자연스럽게 규격외 상황을 걸러주는 프레임으로써 유용하다고 생각했고.
두번째로는 SerializableDictionary를 사용한다면, 굳이 Debug를 걸고 Break Point를 찍어서 런타임에 값을 확인하지 않아도, 자연스럽게 시각적으로 어떤 버튼들과 패널들이 짝지어졌는지 확인할 수 있기 때문에 생산성을 높이는데에도 도움이 된다고 생각했다.

특히 게임 시작단에서 값을 버리고 새롭게 값을 초기화 하는 등, inspector상에서의 수정이 인게임 데이터에 직접적으로 영향을 주지 않는 항목들이라면, 무조건 숨기는 것 보단, 인스펙터에 노출해두어 시각적으로 확인할 수 있도록 하는게 작업속도 향상에 도움이 된다고 생각한다.
UIManager.cs ↓

더보기
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using System.Linq;

public class UIManager : MonoBehaviour
{
    ActionManager actionManager;

    [SerializeField]
    Canvas mainCanvas;
    [SerializeField]
    List<UIPanels> uiPanels = null;
    [SerializeField]
    List<SelfManageButton> btnList = null;

    [SerializeField]
    SerializableDictionary<SelfManageButton, UIPanels> btnDict = new SerializableDictionary<SelfManageButton, UIPanels>();
    
    private void Awake()
    {
        mainCanvas = GameObject.Find("MainCanvas").GetComponent<Canvas>();
        actionManager = this.GetComponent<ActionManager>();
        actionManager.RegistKeyAction(KeyCode.Escape, HideAllPanel);

		//버튼과 패널을 찾아오는 부분 
        foreach(UIPanels pnl in ComponentUtility.FindAllT<UIPanels>(mainCanvas.transform))
            RegistPanel(pnl);
        uiPanels.ForEach(x => x.LinkManager(this));
        btnList = ComponentUtility.FindAllT<SelfManageButton>(mainCanvas.transform);

		//찾은 패널을 여기에서 짝지어짐
        //******* Stage *********//
        ComponentUtility.LinkBtnPnl("talk", btnDict, uiPanels, btnList);

        //******* TopPanel *********//
        ComponentUtility.LinkBtnPnl("option", btnDict, uiPanels, btnList);
            ComponentUtility.LinkBtnPnl("credits", btnDict, uiPanels, btnList);
            ComponentUtility.LinkBtnPnl("developplan", btnDict, uiPanels, btnList);

        ComponentUtility.LinkBtnPnl("book", btnDict, uiPanels, btnList);
        ComponentUtility.LinkBtnPnl("stamina", btnDict, uiPanels, btnList);
        ComponentUtility.LinkBtnPnl("shop", btnDict, uiPanels, btnList);

        //******** Bottom *********//
        ComponentUtility.LinkBtnPnl("food", btnDict, uiPanels, btnList);
        ComponentUtility.LinkBtnPnl("play", btnDict, uiPanels, btnList);

        actionManager.initFlag[nameof(UIManager)] = true;
    }

    public void RegistPanel(UIPanels uiPanel)
    {
        uiPanels.Add(uiPanel);
        uiPanel.LinkManager(this);
        uiPanel.SetIndex(uiPanels.IndexOf(uiPanel));
    }
    
    [SerializeField]
    Stack<int> indexStack = new Stack<int>();

    public void ShowPanel(int index, List<UIPanels.textFactor> factor = null)
    {
        if (index < 0 || index >= uiPanels.Count)
            return;
        foreach (UIPanels pnl in ComponentUtility.FindAllT<UIPanels>(uiPanels[index].transform))
            HidePanel(pnl);
        uiPanels[index].gameObject.SetActive(true);
        uiPanels[index].Init(factor);
        indexStack.Push(index);
    }

    public void HidePanel(UIPanels pnl)
        => HidePanel(pnl.thisIndex);

    public void HidePanel(int index){
        if (index < 0 || index >= uiPanels.Count)
            return;
        if (indexStack.Count <= 0 || index != indexStack.Peek())
            return;
        uiPanels[index].gameObject.SetActive(false);
        indexStack.Pop();
    }

    public void HideAllPanel(){
        foreach (UIPanels panel in uiPanels)
            panel.gameObject.SetActive(false);
        indexStack.Clear();
    }
}
  1. SerializableDictionary는 이곳의 도움을 받았다 ***링크*** [본문으로]