본문 바로가기
Unity

유니티 시스템 프로그래밍 Pt.1 - 4주차

by argentdarae 2024. 9. 29.

개요

4주차 학습을 진행하였다

 

학습 내용은 다음과 같다

  • Lobby 씬 구조 및 디자인 패턴 (LobbyManager, LobbyUIController)
  • 게임 설정 (UserSettingsData, SettingsUI)
  • 뒤로가기(ESC/BACK) 처리 (LobbyUIController, ConfirmUI)
  • 유저 재화 관리 및 커스텀 메뉴 (UserGoodsData, GoodsUI, CustomTools)

 

 

내용

5. Lobby 시스템 제작

Lobby 씬 구조 및 디자인 패턴 (LobbyManager, LobbyUIController)

강사의 핵심 아이디어는 다음과 같다

LobbyManager
- 비지니스 로직 제어 및 데이터 주입
LobbyUIController
- 사용자 인터렉션 관리 및 주입된 데이터를 유저에게 보여주는 로직 처리

 

예시 동작 플로우는 다음과 같다

  1. LobbyManager가 LobbyUIController 초기화
  2. LobbyUIController가 실제 Lobby에서 사용될 UI들을 초기화, 그리고 UI의 동작을 구현하여 UnityEvent로 연결
  3. 이후 유저의 인풋이 들어오면 LobbyUIController가 들어온 동작에 따라 UI 팝업

위 순서로 동작하게 된다

 

이렇게 구현한 의도는 다음과 같다고 한다

  • 로비의 주요 로직은 LobbyManager가 처리
  • UI와 사용자의 인터랙션은 LobbyUIController에 위임하여 데이터 처리와 시각적 요소에 대한 처리가 분리되게 만드는 것
  • 핵심은 LobbyUIController와 UIManager를 분리하여 UI관리의 유연성과 안정성을 확보하는 것

강사가 로비 씬에서의 구조를 나눈 이유는 전역적인 UI와 특정 씬(UIManager vs. LobbyUIController)의 UI를 분리함으로써 유지보수와 기능 확장이 용이하게끔 하기 위함이다

이를 통해 게임의 다른 UI들과 로비에서만 쓰이는 UI의 혼동을 줄이고, 각 역할을 명확히 분리하려는 의도로 보인다

 

그런데 '디자인 패턴'이라는 소제목이 조금 모호하게 느껴진다

MVCMVP의 그 어딘가로 보이기는 하는데.. 내가 설계한 구조는 이렇고 이런 장점이 있다~ 느낌인데

명확히 어떤 문제를 해결하고자 어떤 패턴을 사용했는지에 대한 설명이 부족해 보인다. 프로젝트에 어떻게 적용했는지, 그리고 각 역할이 어떻게 구체적으로 나뉘는지를 명확히 구분해 설명했으면 더 효과적이었을 것 같다

 

또한, 'UIManager'와 'LobbyUIController'의 네이밍이 직관적으로 혼동을 줄 수 있다

비슷한 역할을 하는데 왜 한쪽은 매니저이고, 한쪽은 UIController인가?

각 UI의 참조를 관리하는 추상화된 UIManager와 실제 동작을 담당하는 UIController로 더 명확히 분리하여, 공통 UI(Common), 게임 내 UI(InGame), 로비 UI(OutGame)로 나눠 구조를 통일시켜주는 것이 혼란을 줄이고 유지보수를 더 원활하게 할 수 있는 방향일 것 같다

 

 

게임 설정(UserSettingsData, SettingsUI)

게임 설정을 관장하는 UserSettingsData와 SettingsUI를 구현하는 내용

구현 자체는 어느 게임에서나 볼 수 있는 평범한 Settings였고, 구현과는 별개로 몇가지 생각해볼 점이 있었다

 

1. 인디 게임 만들 때 보안 정책이라던지 준수해야 할 문서가 있다면 반드시 링크 추가하기

 

2. 강사의 코드를 보다보니 early return이 습관화 되지 않은 건지, 강의를 위해 그냥 프리하게 쓰는 건지 잘 모르겠는 데 개선 여지가 보인다

네트워크에서 받아오는 데이터가 아니라면 무조건 데이터가 들어와야 하는데 if문으로 null검사를 굳이 한번 더 해서 연산을 낭비할 필요가 있을까?

assert문을 걸어놓고 삭제해서 최적화를 꾀하는 게 더 좋은 방향성이라고 생각한다

 

3. 반복되는 코드 모듈화 했으면 좋겠다

다만 강의 목표 자체가 설계를 어떻게 했는지 보여주는 것이며 코드는 쉽게 이해시키기 위해 풀어 쓰는 것이라면 이해가 된다

 

4. 각 매니저 클래스가 비슷한 메서드를 구현하고 별개로 호출되는 형태로 구현이 되고 있는데, 차라리 이벤트 하나를 만들고 그 이벤트를 구독하는 형태로 바꾸는게 구현도 편하고 협업도 편해지지 않을까?

예를들어

// TitleManager.cs

private void Start()
{
    //유저 데이터 로드
    UserDataManager.Instance.LoadUserData();

    //저장된 유저 데이터가 없으면 기본값으로 세팅 후 저장
    if(!UserDataManager.Instance.ExistsSavedData)
    {
        UserDataManager.Instance.SetDefaultUserData();
        UserDataManager.Instance.SaveUserData();
    }

    AudioManager.Instance.OnLoadUserData();
    StartCoroutine(LoadGameCo());
}

 

이런 코드가 있었는데, UserDataManager와 AudioManager 둘 다 LoadUserData, OnLoadUserData를 구현하고 있다

강의 상에서 구현된 코드를 보면 무조건 Audiomanager.OnLoadUserData는 UserDataManager.LoadUserData 이후 실행되어야 한다

 

여기서 나는 두 가지 문제가 발생할 수 있다고 생각한다

  1. 협업자의 입장에서 코드를 보고 헷갈릴 여지가 있다
  2. 다른 클래스에서 LoadUserData, OnLoadUserData등의 메서드를 구현할 때마다 TitleManager에 코드가 추가되어야 한다

그래서 내가 제안하는 해결책은 다음과 같다

 

// EventManager.cs

private event Action _beforeLoadUserDataEvent;
public void InvokeBeforeLoadUserData() 
{
    _beforeLoadUserDataEvent?.Invoke();
}
public void SubscribeBeforeLoadUserData(Action action)
{
    _beforeLoadUserDataEvent += action;
}

private event Action _afterLoadUserDataEvent;
public void InvokeAfterLoadUserData()
{
    _afterLoadUserDataEvent?.Invoke();
}
public void SubscribeAfterLoadUserData(Action action)
{
    _afterLoadUserDataEvent += action;
}

// ==================================================================

// TitleManager.cs

private void Start()
{
    var eventManager = EventManager.Instance;

    eventManager.InvokeBeforeLoadUserData();

    // ...

    eventManager.InvokeAfterLoadUserData();
    StartCoroutine(LoadGameCo());
}

// ==================================================================

// UserDataManager.cs

protected override Init()
{
    base.Init();
    
    EvnetManager.Instance.SubscribeBeforeLoadUserData(LoadUserData);
}

// AudioManager.cs

protected override Init()
{
    base.Init();
    
    EvnetManager.Instance.SubscribeAfterLoadUserData(OnLoadUserData);
}

 

이런식으로 어느 타이밍에 호출이 필요한지 클래스 스스로가 결정하게 만들고, TitleManager를 복잡하지 않게 만드는 방법이

다른 사람이 코드를 봤을 때 알아보기 쉽고 새로운 UI도 초기화하기 쉬우며 문제가 생긴 부분을 찾기에도 편할 것이라 생각한다

 

뒤로가기 처리

뒤로가기 UI 로직을 구현하는 내용이었다

 

별 다른 점은 없고, 보면서 느꼈던 것이지만 Update에서 Input을 풀링하는 방식으로 구현하는 것은 연산에 비효율적이라 생각한다

이벤트 형식이 구현도 쉽고 최적화도 이점이 있기에 가능하다면 이벤트 쪽으로 가는 것이 맞는 것 같다

내가 속한 프로젝트에는 모두 Input System을 사용하도록 건의하거나 직접 구현하고 있다

 

또한, LobbyUIController.HandleInput 메서드의 if문 내부에서 데이터를 셋업하고 있는데 이 부분도 데이터를 관리하는 부분을 한 곳으로 모으고 거기에서 한번에 데이터를 관리하는 방향으로 가는 게 좋지 않을까 생각한다

개인적으로 데이터가 여러군데 퍼져있을 때 관리하기 어려웠던 경험이 있기 때문이다

차선책으로 현재 구조에선 LobbyUIController에 GetQuitUI를 구현하고, 이 메서드를 통해 가져오게 만드는 게 좋아보인다

 

유저 재화 관리 및 커스텀 메뉴

유저 재화인 Gem/Gold를 관리하는 GoodsUI를 구현하는 내용이다

 

GoodsUI 클래스를 구현하면서 강사는 BaseUI를 상속받지 않고 Monobehaviour로만 구현하였는데, 이러한 설계 포맷을 벗어난 예외를 만드는 것은 별로라고 생각한다. 개인적으로는 BaseUI와 BaseUIData를 이용하여 만드는 게 합리적이라고 생각한다

 

또한 앞서 3주차에서 생각했던 우려가 그대로 강의에서 나왔다

// 이전 코드
// 새로 팝업된 UI를 그냥 맨 마지막 인덱스로 할당해주면 끝났다
var siblingIndex = UICancasTr.childCount;

// 이번 파트에서 새로 바뀐 코드
// 항상 GoodsUI가 맨 위로 올라가야 하기 때문에 새로 팝업되는 UI의 인덱스를 조정해주는 모습이다
var siblingIndex = UICancasTr.childCount - 1;

 

GoodsUI의 경우 무조건 가장 위에 그려져야 해서 SiblingIndex를 관리하는 로직이 복잡해졌는데 이 경우 주석을 추가로 써야 하며, 코드를 이해하는 데 시간이 더 걸릴 것이다

 

따라서 SortingLayer 조정을 통하여 제일 위에 그려지게 만드는 방법이 좋아보인다

 

그 외로, UIManager.EnableGoodsUI 메서드도 리팩토링 할 수 있을 것 같다

 

// UIManager.cs

public void EnableGoodsUI(bool value)
{
    _goodsUI.gameObject.SetActive(value);

    if (value)
    {
        _goodsUI.SetValues();
    }
}

 

현재 UIManager가 EnableGoodsUI의 구현까지 담당하고 있는데, 이러면 UIManager가 하는 일이 많아지고, 클래스 코드가 길어질 것 같다

코드가 무조건 길어지는게 나쁜건 아니지만, 이 로직을 굳이 UIManager가 가지고 있어야 할까?

 

// UIManager.cs

public void EnableGoodsUI(bool value) => _goodsUI.SetActiveUI(value);


// GoodsUI.cs

public void SetActiveUI(bool value)
{
    gameObject.SetActive(value);

    if (!value) return;
    
    _goodsUI.SetValues();
}

 

이런식으로 스스로 상태를 관리하게 만들고, UIManager의 코드를 간략화 함으로써 가독성을 향상시킬 수 있다고 본다

 

 

마무리

배울점도 많고, 내 코드와 설계를 어떻게 하면 더 발전시킬 수 있을까 생각해볼 수 있었다

 

이전에 짰던 코드를 내가 왜 이렇게 짜기 시작했을까, 어떤 장단점이 있었지? 하며 되돌아 볼 수 있었으며

다른사람들을 설득해야 하는 상황이 생긴다면 예시 코드를 토대로 의견을 주장할 수 있을 것 같다

 

이번 스터디를 진행하면서 설계쪽에 대해 좀 더 깊은 생각을 해보고, 기준을 점검할 수 있어서 굉장히 좋은 것 같다

 

현재 내가 설계 및 코드를 짜는 기준은 다음과 같다

  • 다른사람이 읽고 한번에 이해가 가는가?
  • 기능을 추가하기 편한가?
  • 유지보수가 편한가?
  • 변경했을 때 다른곳에서 사이드 이펙트를 일으키지는 않는가?
  • 최선의 최적화인가?

파고들면 더 있겠지만 대전제는 위와 같다

 

앞으로도 피드백 할 때 위의 기준으로 점검하고 다른사람의 작업물을 보며 배울점을 습득하고 나의 작업물을 피드백하며 협업하기 편하고 필요한 업무를 빠르게 해낼 수 있는 소프트웨어 엔지니어가 되고 싶다