유니티 2D 개발 공부 6장(UI 시스템 구현)

김동운·2025년 2월 17일

유니티

목록 보기
9/11
post-thumbnail

요번엔 UI매니저를 만들어볼꺼다

사실 기존 UI시스템이 있지만 사실상 하드코딩덩어리에 확장성이 낮은편이라 새로 재개편하고 각잡고 체계적으로 만들어 볼려한다

구조는 대략적으로 이와 같다

아직까지는 컨트롤러 부분은 UI쪽만 만든 상태다,, 나중에 차자 나머지도 만들어야지.

1. 구조설명

  • 옵저버,핸들러,트리거는 각 UI작동의 이벤트트리거가 된다 즉 이 명령을 받고 해당되는 UI작업을 실행하게 된다.
  • 매니저의 경우는 각 명령을 관제하고 배분한다, 또한 UI명령에 필요한 작업순서나 프리팹,오브젝트나 필요자원의 주소를 저장하게된다.

  • 업데이터의 경우는 매니저의 저장된 자원의 값을 갱신하거나 수정하는 역할을 하게된다 예를들자면 UI의 prefabs나 캔버스의 위치등등

  • 우측은 UI컨트롤러에 속한다 좌측에서 해당명령을 내리면 매니저에서 명령을 중계해서 해당 컨트롤러의 명령을 수행하게 된다
    또한 해당명령을 실행했다는상태를 매니저에 전달한다.

대략적인 구조는 패턴은 원래 MVP 패턴 갈려다 상황봐서 패턴구조를 살짝 꼬았다.
설명은 일단 여기까지다 이제 만들어보자.

2. UImanager.cs

2.1 singleton 선언

일단 싱글턴 구조를 만들어야한다. 매니저는 값을 가지고 씬마다 계속 돌아다녀야하기때문이다. 그래야 중계역활을 하기때문이다

먼저클래스를 만들어주자

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class UImanager : MonoBehaviour
{
    private static UImanager instance;
    
}

일단 첫번째로 이 싱글턴을 저장할 인스턴스를 만들어주자, 클래스의 인스턴스를 한 번만 생성하고, 이를 프로그램 전반에서 재사용하는 데 사용된다 외부에서 접근을 못하게 private와 동시에 재사용을 위해 정적선언도 해준다.

이제 void Awake()함수를 사용할꺼다 이 함수는 이 컴포넌트가 달려있는 오브젝트가 활성화시 작동한다
Awake() 함수에 다음과 같이 작성하자.

void Awake()
    {
        
        if (instance != null && instance != this)
        {
            Destroy(gameObject);
        }
        else
        {
            instance = this;
            DontDestroyOnLoad(gameObject);
        }
    }

조건문은 다음과 같다 위에 선언한 인스턴스가 null이아니거나 인스턴스에 저장된게 이 컴포넌트가 아니라면 그 오브젝트는 삭제한다.

이는 싱글턴의 중복생성을 방지하기위한 코드이다 이게없으면 아마 Awake()함수가 매니저를 무한증식시킬 문제가 생긴다.

뭐 아니라면 처음생성이니 이 객체를 변수에 넣는다, 또한 DontDestroyOnLoad(gameObject) 를 통해 씬 전환 시에도 사라지지 않는다.

2.2 접근자 선언

이제 싱글턴을 만들면 해당 싱글턴을 접근을 해야한다, 물론 인스펙터에 주렁주렁 달아서 접근해도되지만 그럼 개발과정이 심히 끔찍해진다.

물론 같은 씬에있다면 클래스간 간단히 연결이 가능하다.(여러개있으면 문제생김 ) 문제는 접근을 해주는 일종의 도킹베이가 필요하다.

그럼이제 접근자를 만들어주자 물론 싱글턴 구조이기때문에 인스턴스를 유일하게 존재시킬려면 정적선언또한 해주자

public static UImanager manager
    {
        get 
        {
            if(instance == null) instance = FindObjectOfType<UImanager>(); <--- 이건 왠만하면 자주 안쓰는거 추천
            return instance; 
        }
    }

구조를 보면 public static 선언이 되었기때문에 다른 클래스에서 접근할수있다 이를통해 안전하게 인스턴스를 가져와서 사용이 가능하다.

조건문을 통해 만약 인스턴스가 사라졌다면 재배치를 하도록 한상태다, 언제나 예외는 있을수도 있으니깐.. 물론 코드가 좀 길다 굳이 이리 길게 할필요도 없는데말이지..

public static UImanager manager
    { 
        get => instance ?? (instance = FindObjectOfType<UImanager>());
    }

그럴땐 간단히 람다식으로 때워도된다 물론 그냥 인스턴스 겟에 리턴시켜도된다.솔직히 날라갈일이 없을거같기도 하고..

2.2 UImanager.cs의 기능처리 매서드

매니저는 매서드를 통해 명령을 중계한다 일단은 업데이트와 창 열기,닫기를 구현해보자 ,, 라고 하기전에

팝업ui특성상 작업순서가 있어야한다 팝업은 일단 스택처럼 창위에 계속 덮어지기때문이다, 물론 요즘은 더 진보된 방식도 많지만 기본적인
수준을 구현할 예정이다.(예를들자면 뒤에있는 창 클릭해서 끌어온다음 작업하는거도있고,, 아무튼 많긴함. )

작업이랑 자원 저장할 변수는 다음과같이 설정해주자 아직은 팝업만 만들어서 양이 적다

public Dictionary<string, GameObject> popupUIDictionary = new();
public Stack currentPopupUI = new Stack();
[HideInInspector] public Canvas canvas;

물론 이를 위해선 작업을 저장할곳이 필요하다 또한 팝업올릴 패널도 필요하고,,

일단 패널을 준비해보자!


일단 패널은 여기다 저장할 예정이다, 각용도에 맞게 적절히 이름을써주도록하자.

패널을 만드는 법은 다음과 같다.


하이리키에서 우클릭을 통해 ui에서 패널을 만들어주자. 캔버스가 없는상태면 알아서 캔버스가만들어지고 그안에서 만들어질꺼다.


해당 패널이 완성된 상태다. 일단 어디로 옴기지말고 적절한 이름 정하고 캔버스 그룹 컴포넌트를 추가해주자, 물론 안해도되긴하지만 나중작업을 위해 일단 나는 깔아둔 상태다.


버튼을 추가해주자 이름은 딱히 상관없다
이걸로 버튼에 글을 수정할수있다 물론 구분을 위해 이름을 바꿔도 된다.

이제 적절하게 만들었다면 하이리키에서 해당 폴더로 드래그앤 드랍을 해주자 파란색으로 해당 패널이름 파란색 파일이생겼다면 성공이다

여기까지 준비물은 끝났다 이제 3 장으로 가자. 업데이터를 통해 패널을 등록해줘야한다.

3. UIUpdater.cs

뭐 다양한 기능을 할꺼긴할텐데 핵심은 UI 리소스를 관리하고 업데이트하는 클래스이다.

설명하기엔 뭐 아직은 규모가 너무작아서 한번에 올리면 다음과 같다

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class UIUpdater : MonoBehaviour
{
    // Start is called before the first frame update
    private static UIUpdater instance;
    public static UIUpdater Updater
    {
        get 
        {
            if(instance == null) instance = FindObjectOfType<UIUpdater>();
            return instance; 
        }
    }
    public void RegisterPopupUI () 
    {
        var manager = UImanager.manager;
        GameObject[] popupUIs = Resources.LoadAll<GameObject>("PopupUIs");
        foreach (GameObject popupUI in popupUIs)
        {
            if (!manager.popupUIDictionary.ContainsKey(popupUI.name)) 
            {
                manager.popupUIDictionary.Add(popupUI.name, popupUI);
            }
        }
    }
    
}

위쪽은 뭐 전에도 하다싶이 객체를 저장하고 접근자를 생성했다. 물론 시작할때 등록은 안되있을테니 이프문으로 대강땜빵했다
관건은 아래이다. 해당코드는 특정폴더에있는 오브젝트를 딕셔너리에 저장할꺼다 , 우리가 저장한 패널 프리팹을 메모리에 저장할 용도이다 .

var manager = UImanager.manager;

접근용 함수이다 물론 안만들어도되지만 저걸 그대로쓰자니 코드가 드럽게 길어질게 뻔하다

GameObject[] popupUIs = Resources.LoadAll("PopupUIs");

게임오브젝트를 담는 배열이다, Resources.LoadAll("PopupUIs")를 통해 해당 폴더에있는 GameObject를 모두 찾아서 저장한다.

foreach (GameObject popupUI in popupUIs)
{
        if (!manager.popupUIDictionary.ContainsKey(popupUI.name)) 
        {
            manager.popupUIDictionary.Add(popupUI.name, popupUI);
        }
}

배열내 자원수만큼 반복한다, 딕셔너리에 중복된 이름에 패널이 없으면 매니저에있는 딕셔너리에 해당패널이름을 키로, GameObject를 값으로 저장한다.

이제 매니저에서 해당방법으로 호출한다.

public void SceenUpdate()
{
    UIUpdater.Updater.NewSceenUpdate();
}

물론 매니저에서 해당추가코드를 바로 할수있지만 단일책임법칙을 위배하면 코드가 상당히 난해해진다. 이를 해결하기위해 역할간 클래스를 분리했다.

현재까지 한 코드

UImanager.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class UImanager : MonoBehaviour
{
    private static UImanager instance;
    public static UImanager manager
    { 
        get => instance ?? (instance = FindObjectOfType<UImanager>());
    }
    void Awake()
    {
        
        if (instance != null && instance != this)
        {
            Destroy(gameObject);
        }
        else
        {
            instance = this;
            DontDestroyOnLoad(gameObject);
            UIUpdater.Updater.RegisterPopupUI();
        }
    }

    public Dictionary<string, GameObject> popupUIDictionary = new();
    public Stack<GameObject> currentPopupUI = new Stack<GameObject>(); 
    [HideInInspector] public Canvas canvas;
    
    
    public void SceenUpdate()
    {
        UIUpdater.Updater.NewSceenUpdate();
    }
}

UIUpdater.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class UIUpdater : MonoBehaviour
{
    private static UIUpdater instance;
    public static UIUpdater Updater
    {
        get 
        {
            if(instance == null) instance = FindObjectOfType<UIUpdater>();
            return instance; 
        }
    }
    public void RegisterPopupUI () 
    {
        var manager = UImanager.manager;
        GameObject[] popupUIs = Resources.LoadAll<GameObject>("PopupUIs");
        foreach (GameObject popupUI in popupUIs)
            {
                if (!manager.popupUIDictionary.ContainsKey(popupUI.name)) 
                {
                    manager.popupUIDictionary.Add(popupUI.name, popupUI);
                }
            }
    }
    public void NewSceenUpdate() <--이건 한줄짜리라.. 뭐 전에 설명했던거 응용이라 어렵진 않을꺼에요..
    {
        UImanager.manager.canvas = FindObjectOfType<Canvas>();
        
    }
}

다음에 마저 핸들러와 컨트롤러, 옵저버를 설명하겠다.

0개의 댓글