Unity

#2 Managers 평가 + UI

myjeongjun 2024. 12. 23. 15:44

오랜만에 유니티 글을 써봅니다. 학기중에 개발과 동시에 글을 작성할수가 없어서 프로젝트 제출을 마치고 이제서야 쓰게되었습니다.

 

UI부분을 짚고 넘어가기전에 제가 작성했던 코드인 Managers에 대해 이야기 하고 평가해보고자 합니다.

 

아마 대부분의 게임에는 서로다른 역할을 맡는 Manager를 두고 개발할 거라고 생각합니다. 그렇게 하는것이 가장 효율적 이라고 생각하고요.

 

저또한 역시 이번 개발에서 Manager들을 활용했습니다.

 

 

제가 기존에 하는방식은 모든 Manager에게 각각 싱글톤 패턴을 적용시켜서 접근했다면, 제가 강의를 듣고 찾은 방식은 최상위 Managers에게만 싱글톤을 적용시키고 Managers를 통해 각각의 Manager에 접근하는 방식으로 보였습니다.

 

하지만 이 방식은 무조건적으로 좋아보이진 않았습니다.

1.Manager들이 점점커지면 Managers에게 초기화 부담이 점점 커질것이다.

 

2.Managers에 문제가생기면 모든 개별 Manager가 작동하지 않는다.

이 문제는 Managers의 덩치가 커지면 더욱 부각되는데 문제 발생시 정확한 원인 파악하기가 매우 어려워집니다.

 

3.각 매니저는 전부 Managers에서 초기화 되기때문에 한 매니저에 의존성이 있는 매니저가 있을때 초기화 순서를 잘 해놓지않으면 오류가 발생한다.

 

저는 게임의 규모가 커졌을때를 가정하고 코드를 작성하기 때문에 기존에 사용한 전자의 방식이 관리,디버깅 측면에서 여전히 좋은 방식이 아니었나 생각해봅니다. 

 

+추가

위에 써놓은 3가지 질문에대한 답변을 들었습니다.

1.초기화 작업은 게임에서 딱 1번만 이루어지므로 어느쪽으로 해도 상관없다.

 

2.Managers가 고장났다는 거는 어떠한 매니저에서 오류가 발생했다는 뜻,

개별로 분리했을때 하나라도 고장나면 게임이 정상적으로 돌아가지 않을것이므로 어느쪽으로하든 고쳐야하는건 같다.

 

3.충분히 발생가능한 문제이다. 그러므로 Managers에서 순서제어도 함께 해주어야한다.

 

정도로 이번 개발에서의 저의 코드를 평가해 보았습니다.

아래는 Managers 코드 입니다.

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

public class Managers : MonoBehaviour
{
    static Managers s_instance; // 유일성이 보장된다
    static Managers Instance { get { Init(); return s_instance; } } // 유일한 매니저를 갖고온다


    SceneManagerEX _scene = new SceneManagerEX();
    ResourceManager _resource = new ResourceManager();
    UIManager _ui = new UIManager();
    PoolManager _pool = new PoolManager();
    GameManager _game = new GameManager();

    DataManger _data = new DataManger();

    Soundmanager _sound = new Soundmanager();

    public static ResourceManager Resource { get { return Instance._resource; } }
    public static UIManager UI { get { return Instance._ui; } }
    public static SceneManagerEX Scene { get { return Instance._scene; } }

    public static PoolManager Pool { get { return Instance._pool; } }

    public static GameManager Game { get { return Instance._game; } }

    public static DataManger Data { get { return Instance._data; } }

    public static Soundmanager Sound { get { return Instance._sound; } }
    void Start()
    {

    }

    void Update()
    {

    }

    static void Init()
    {
        if (s_instance == null)
        {
            GameObject go = GameObject.Find("@Managers");
            if (go == null)
            {
                go = new GameObject { name = "@Managers" };
                go.AddComponent<Managers>();
            }

            DontDestroyOnLoad(go);
            s_instance = go.GetComponent<Managers>();

            s_instance._pool.Init();
            s_instance._data.Init();
            s_instance._sound.Init();
        }
    }


    public static void Clear() 
    {
        UI.Clear();
        Pool.Clear();
        Scene.Clear();
        Sound.Clear();

    }
}

 

 

이제 본격적으로 UI에 대해 평가해 보겠습니다. 개인적으로도 풀리지 않은 의문이 많고 다루기도 제일 어려웠습니다.

 

우선 게임속의 UI들이 전부 같은 종류의 UI라고는 할 수 없습니다. 전부 같은 종류라고 가정하고 코드를 짜게되면 UI관리가 매우 어려울것이고 불가능에 수렴할거라고 생각합니다.

 

어떤 UI는 끄고 닫을수있고(PopUp) , 항상 화면에 보여야하거나(Scene), 만약 3D그래픽이라면 World 상에 존재해야하는 UI도 필요합니다(몬스터의 체력바가 몬스터의 머리위에 떠야하는 경우)

 

물론 필요에따라 좀더 세분화 하는것도 가능합니다. 하지만 저에게 주어진 한 달의 시간동안 개발할수잇는 게임의 규모를 생각해봤을때 이정도면 충분해 보였습니다.

 

이제 UI를 어떻게 관리할 것인가에 대해 생각해 보았습니다. 

저는 기존에 UI를 한 Scene에 전부 미리 만들어놓은 다음 false시킨고 필요에 따라 true로 만든는 방식으로 알고있었습니다. 유튜브에있는 유니티 강의에서도 대부분 그렇게 설명하고요, 

 

그렇게 되었을때 UI가 많아지면 당연히 부하도 많이 가지않을까? 라는 의문점이 발생했습니다. 사용하진않아도 존재하는것만으로도 메모리를 사용하니까요

만약 Scene을 여러개 사용한다고 가정하고 모든 UI를 DontDestroyOnLoad를 사용해서 끌어온다면 이 함수는 특정 Scene에서는 안끌어오고 이런 기능은 없습니다.어떤 Scene에선 필요하지 않은 UI인데도 끌어왔을때 그냥 가지고있자니 메모리낭비가되고 삭제시키자니 한 둘이 아니면 그만큼 자원이 소비되는 문제가 발생합니다.

 

그렇기때문에 이것은 동적으로 UI를 할당하는 해결책으로 연결됩니다.

 

저도 강의를듣고 만든거라 아직 구조를 완벽하게 이해하지 못했다고 생각합니다. 그렇지만 최대한 이해한선까지 설명해보겠습니다.

 

일단 한 개의 Canvas에는 서로 연관되어있는 UI들로만 구성되게 만들어 놓습니다.

UI분류
PopUp UI폴더 안

 

그리고 해당 UI를 조종하는 스크립트도 이름을 같게합니다.

 

이제 UI_Base에서 Bind함수를 사용하는데, 우선 플레이어의 상태를 나타내주는 PlayerInfo UI를 예시로 설명해보겠습니다.

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

public class UI_PlayerInfo : UI_PopUp
{
    Image go;
    Image exp;
    Text expText;
    Text LVText;

    enum Images 
    {
        UI_PlayerIcon,
        UI_PlayerIcon_Frame,
        UI_PlayerHP_Bar,
        UI_PlayerHP_Frame,
        UI_EXP_Bar

    }
    enum GameObjects 
    {
        UI_Player_SKill,
    }
    enum Texts 
    {
        UI_EXP_Text,
        UI_LV_Text
    }

    private void Start()
    {
        Init();
    }


    public override void Init() 
    {
        base.Init();

        Bind<Image>(typeof(Images));
        Bind<Text>(typeof(Texts));

        go = GetImage((int)Images.UI_PlayerHP_Bar);
        exp = GetImage((int)Images.UI_EXP_Bar);
        expText = Get<Text>((int)Texts.UI_EXP_Text);
        LVText = Get<Text>((int)Texts.UI_LV_Text);
    }

    private void Update()
    {
        float HPratio = PlayerInfo.Instance.CurrentHP / PlayerInfo.Instance.MaxHP;
        go.fillAmount = HPratio;

        float EXPratio = PlayerInfo.Instance.EXP / 1000f;
        exp.fillAmount = EXPratio;
        float EXPratioTopercent = EXPratio * 100;
        expText.text = EXPratioTopercent.ToString() + "%";

        LVText.text = "LV." + PlayerInfo.Instance.LV;
    }

}

 

여기서 Bind 함수의 구조를 들여다보자면

    Dictionary<Type, UnityEngine.Object[]> _objects = new Dictionary<Type, UnityEngine.Object[]>();
    protected void Bind<T>(Type type) where T : UnityEngine.Object
    {
        string[] names = Enum.GetNames(type); //넣어준 Enum안에있는 이름들이 들어감

        UnityEngine.Object[] objects = new UnityEngine.Object[names.Length];
        //enum안에 있는 이름들이 우리가 넣고싶은 UI들이다.
        _objects.Add(typeof(T), objects); //딕셔너리에 등록

        for (int i = 0; i < names.Length; i++)
        {
            if (typeof(T) == typeof(GameObject))
                objects[i] = Util.FindChild(gameObject, names[i], true);
            else
                objects[i] = Util.FindChild<T>(gameObject, names[i], true);


            if (objects[i] == null)
                Debug.Log($"Failed to Bind!{names[i]}");
            //자식들을 순회하면서 이름이 같은걸 매칭시킨다. 근데 이기능은
            //다른 곳에서도 유용하니 따로 Util에서 관리시킨다.
        }
    }

유니티의 reflection 기능을 사용합니다. 이 기능은 " Reflection은 프로그램 실행 중에 타입, 메서드, 필드 등의 메타데이터를 동적으로 검사하고 수정할 수 있는 기능을 제공합니다"라고 서술하는데, 클래스,구조체,enum등에서 존재하는 메서드나 속성에 접근이 가능합니다,

 

즉, 위 코드에서 Enum.GetNames(type)을 사용하면 넣어준 Enum의 값들을 가져올수있게 됩니다. PlayerInfo에서 Images를 Bind해줬으니 UI_PlayerIcon,UI_PlayerIcon_Frame,UI_PlayerHP_Bar... 등이 전부 string 배열에 반환됩니다. 이제 그 이름과 일치시키는 오브젝트를 미리 만들어놓은 FindChild함수를 사용해서  UnityEngine.Object 타입으로 넣어놓습니다.딕셔너리를 사용해서 오브젝트를 저장해두고요, 결국 최종적으로 원하는것은 

PlayerInfo라는 Canvas 안에 자식으로 존재하는 LV,EXP같은 UI 들의 변경사항이 존재할때마다 조정해줘야 하기 위해서 미리 오브젝트를 등록해 주는 작업이라는 겁니다.

 

이제 PlayerInfo에서 만약 몬스터를 처치해서 경험치를 얻어 UI_EXP_Bar도 당연히 올라가야하기때문에 사용을 해줘야합니다.(지금까지 자식오브젝트로 UI_EXP_Bar를 등록하는 작업)

 

위 스크립트를 보시면 GetImage또는 Get<Text>가 보이실겁니다.

이제 등록되어있는 오브젝트를 꺼내오는 작업을하는데 기존에 등록을 시킬때  UnityEngine.Object로 등록을 시켜놨습니다. 형변환을 시도할때에는 만약 Image라면 UnityEngine.UI.Image컴포넌트가 부착되어있어야지한 형변환을 성공하고 반환합니다. 이를 검사하기위해서 TryGetValue를 사용해서 형식검사를 한후 반환합니다.

 

글의 길이가 너무 길어지니 중간에 한번 끊고 다음 글에서는 PopUp,World,Scene UI를 유형별로 어떻게 설정해주었는지를 작성하고 UI부분을 마치겠습니다.

 

또한 처음 이 방식을 작성할때 어려워서 이것저것 찾아보다가 reflection기능을 되도록 쓰지마라는 내용과 강의에서 가르친 방식이 별로 좋지 않다는 글도 발견을했었는데 어째서 그러한 의견을 냈는지도 분석하여서 번외편으로 만들어보겠습니다.

'Unity' 카테고리의 다른 글

#3. 데이터 관리  (0) 2024.12.25
#2.1 UI 설정  (0) 2024.12.23
#1. 기본 세팅 (Unity 프로젝트)  (0) 2024.11.15