[25.04.09] TIL( 프로젝트 개발 - UI 자동화 )

설민우·2025년 4월 9일

내일배움캠프 - Unity

목록 보기
18/85

UI 자동화 작업

UI 는 어떻게 보면 배정 업무 중 가장 쉬워보일 수 있습니다. 단순히, UI의 컴포넌트와 함수를 연결하고 예쁘게 배치하는것이 끝일 수 있기 때문입니다. 하지만 여기서 더 나아가서 UI 자동화 작업을 진행해봄으로써 불필요한 UI 연결 시간을 단축해 보았습니다.

시작점

일반적으로 유니티에서의 기초적인 UI 관리는 위의 사진과 같이 컴포넌트 창에서 버튼과 같은 특정 컴포넌트와 스크립트의 함수를 연결해 주는 작업으로 진행됩니다. 지금처럼 간단한 캐쥬얼 게임의 경우에는 무리 없이 작업할 수 있지만, 더 큰 프로젝트 및 더 많은 인원들과 협업하게 된다면 위와 같은 방법은 시간도 오래걸리고 안전하지 않을 수 있습니다. 이를 위해 UI와 스크립트를 자동으로 연결하는 시스템을 구성했습니다.

구조

  • 먼저, UI 자동화와 관련된 스크립트는 위와 같습니다.
  • C# 스크립트의 특성인 객채지향을 위해 최대한 상속 구조를 유지하려고 노력했습니다

    상숙 구조는 아래와 같습니다
    UI_Base -> UI_Popup -> UI_SettingPopup
               ㄴ> UI_Scene -> UI_ShowBUIutton
                                  ㄴ> UI_StartScene

  • 위의 구조중 UI_Popup은 팝업창으로 출력되는 UI를 의미합니다
  • UI_Scnen 은 처음부터 표시되어지는 기본 UI를 의미합니다

세부 내역

< UI_Base >

    protected void Bind<T>(Type type) where T : UnityEngine.Object
    {
        //타입 이름 배열
        string[] names = Enum.GetNames(type);
        UnityEngine.Object[] objects = new UnityEngine.Object[name.Length];
        this.objects.Add(typeof(T), objects);

        for (int i = 0; i < names.Length; i++)
        {
            // 컴포넌트 찾기
            objects[i] = FindChild<T>(gameObject, names[i], true);
            if (objects[i] == null)
                Debug.Log($"Failed to bind{names[i]}");
        }
    }
    // 원하는 컴포넌트를 딕셔너리에서 추출
    protected T Get<T>(int idx) where T : UnityEngine.Object
    {
        UnityEngine.Object[] obj = null;
        if (objects.TryGetValue(typeof(T), out obj) == false)
            return null;

        return obj[idx] as T;
    }
  • UI_Base 에서 가장 중요한 두 함수는 Bind 와 Get 입니다.
  • Bind는 스크립트에 작성한 enum(열거형) 값을 읽어 이와 이름이 같고, 원하는 컴포넌트 T(제네릭) 를 가진 오브젝트를 가져와 딕셔너리로 등록합니다. 딕셔너리의 Key는 해당 컴포넌트 Type, Value는 오브젝트 배열입니다
  • Get은 Bind로 생성된 오브젝트에서 enum값의 idx를 가져와 딕셔너리에서 찾고 값을 반환해 줍니다.

< UI_Popup & UI_Scene >

public class UI_Scene : UI_Base
{
    public override void Init()
    {
       UIManager.instance.SetCanvas(gameObject, false,canvasLayer);
    }

}
public class UI_Popup : UI_Base
{
    public override void Init()
    {
        UIManager.instance.SetCanvas(gameObject, true, canvasLayer);
    }

    public virtual void ClosePopUI()
    {
        UIManager.instance.ClosePopupUI(this);
    }

    protected void SetPivot(Vector3 pos, RectTransform pivot)
    {
        pivot.position += pos;
    }
}
  • UI_Base 를 상속받는 두가지 타입의 Popup 과 Scene은 그 구조가 더 단순합니다
  • 각각 캔버스 설정을 위한 Init이 있고 Popup은 켜진 팝업을 종료하기 위한 ClosePopupUI가 싱글톤으로 생성된 UIManager와 연결되어있습니다

< 사용예 >

public class UI_StartScene : UI_Scene
{
    enum Texts
    {
        TitleText
    }
    enum Buttons
    {
        NomalModeButton,
        HardModeButton,
        QuitButton,
    }
    public override void Init()
    {
        base.Init();

        Bind<Button>(typeof(Buttons));
        Bind<Text>(typeof(Texts));

        Get<Button>((int)Buttons.NomalModeButton).onClick.AddListener(OnNomalButtonClick);
        Get<Button>((int)Buttons.HardModeButton).onClick.AddListener(OnHardButtonClick);
        Get<Button>((int)Buttons.QuitButton).onClick.AddListener(OnQuitButtonClick);

    }
    protected void OnNomalButtonClick()
    {
        SceneManager.LoadScene("Game");
    }
    protected void OnHardButtonClick()
    {
        SceneManager.LoadScene("Game");

    }
    protected void OnQuitButtonClick()
    {
        Application.Quit();
    }
}
  • 사용하는 방법은 두가지 타입 모두 같습니다
  • 먼저 Bind하기 원하는 UI들의 이름에 맞춰 enum을 설정합니다
  • 그 다음 Init 에서 초기화시, Bind와 Get을 각각 진행합니다
  • 단순히 Bind 뿐만 아니라 함수 연결도 진행하고 싶다면 AddListener 로 함수와 버튼을 연결해줍니다.

그러면 위와 같이 이름이 같은 오브젝트를 찾아 원하는 "Button" 컴포넌트가 있는지 찾고 이를 저장했다가, Get일 시도 할 시 원하는 값이 존재한다면 반환해줍니다.

< UIManager >

    public GameObject ShowPopupUI(string name = null)
    {
        Time.timeScale = 0f;

        GameObject prefab = Resources.Load<GameObject>($"Prefabs/UI/Popup/{name}");
        GameObject ui = Instantiate(prefab);
        if (ui == null)
            return null;

        UI_Popup popup = ui.GetComponent<UI_Popup>();
        _popupStack.Push(popup);

        ui.transform.SetParent(Root.transform, false);
        return ui;
    }

    // 오버라이딩
    public void ClosePopupUI()
    {
        if (_popupStack.Count == 0) return;

        UI_Popup popup = _popupStack.Pop();
        Destroy(popup.gameObject);
        popup = null;

        _order--;

        Time.timeScale = 1.0f;
    }
  • 한편 별도의 싱글톤 패턴으로 존재하는 UIManager에서는 UI의 캔버스를 설정하고, Popup을 켜고 끄는 입력을 받습니다
  • 이 중에서 가장 특징적인 것은 Stack 구조를 이용한 Popup의 사용입니다
  • Popup 창은 먼저 켜진것이 가장 나중에 꺼지는, 선입 선출 형태를 지니므로, 이와 연관된 Stack 을 이용하여 켜고 끄는것을 조정합니다

결론

  • 지금 작업하는 카드 뒤집기 게임의 파생형 프로젝트의 경우 UI를 거의 사용하지 않습니다. 때문에 위와 같이 직접 연결하지 않고 스크립트로 조작하는것이 오히려 일을 키우는 것 처럼 보이기도 합니다. 하지만 이런 구조를 작성해두면 간단하게 상속받아 추가적인 형태의 UI를 생성하는 것도 쉽고, 프로젝트를 관리하는것도 쉬워집니다.
  • 이런 구조는 비단, 현재 프로젝트에서 뿐만 아니라 많은 다른 프로젝트에서 기본적으로 사용 할 수 있을 것이라 생각되기에, 따로 패키지 형태로 추출해놓는것도 좋아보입니다.
profile
클라이언트 개발자를 지망하고 있습니다.

0개의 댓글