버그 해결을 위한 모든 질문을 던져
+1 vote
525 views
class GameManager

{

  GameUIManager UIManager;

 

  void OnGame()

  {

    //로직실행 도중 상황에 따른 UI on/off 및 초기화

    if(GameEnd)

    {

      UIManager.OpenGameEndUI();

      UIManager.GameEndUI.UpdateData(thisData);

    }

 

    if(InputMenuKey)

    {

      UIManager.OpenMenu();

    }

    else

    {

      UIManager.CloseMenu();

    }

  }

}

저는 이런 식으로 게임매니저에서 UI매니저를 필요한 상황마다 직접 제어하는 방식으로 하고 있었는데

아는 분이 이렇게 게임코드가 UI랑 밀접히 있으면 안된다고 게임코드는 ui코드의 존재도 모르도록 철저히 갈라놔야 한다 그러더군요.

 

그런데 제가 초보자다보니 그게 가능한지도 맞는건지도 잘 모르겠습니다.  

게임논리 클래스에서 적절하게 UI 끄고 켜거나 업데이트하는 것도 게임처리에서 하는게 맞지 않나 싶고..

이벤트가 아니라더라도 직접 UI를 제어할 일이 생길수 있는데

게임코드에서 ui의 존재를 완전히 모르게 할 수가 있나 생각되더라구요.

 

이벤트가 있긴 하던데...

만약 코드를 분리해서 게임매니저는 UI를 모른다쳐도 UI는 이벤트 등록을 위해서라도 어떻게든 게임매니저의 존재를 알아야하는데

그럼 이걸 누가 연결해주나요? 연결할려는 UI 클래스에서 직접 하나요? 아니면 다른 중간클래스에서?

중간클래스를 거쳐서 등록한다해도 그럼 그 중간클래스를 또 따로 만들어야하는 번거로움이 있고

이벤트도 ui 요소별로 일일히 등록, 해제도 해야하는데 그건 어디서 도맡아 하는게 맞는 것이며

각자 업데이트에 필요한 값도 다른데 이것도 어떻게 해결하는지 더 생각만으로도 골치아프더군요.

만약 시점 상관없이 강제 업데이트할때 어떻게 할지도 문제고요.

게임매니저를 참조로 안들고 있으면 강제 업데이트도 빠르게 안될텐데..

 

 

이런저런걸 생각하다보니 로직/UI을 무조건 분리하는게 맞는건지 전혀 모르겠습니다.

만약 분리하는게 맞다면 어떤 방법들이 있는지, 제가 문제라고 생각한 것들의 해결방법이 무엇인지 궁금합니다.

제가 초보자라 도저히 감이 안 잡힙니다..
asked (21 point)
재 태그 , 525 views

2 answers

+4 votes
우수 답변

유니티에서는 적당히 타협한 옵저버 패턴이 사용되곤 하는데요, 패턴까지 들어가지 않더라도 간단히 구조만 잡아본다면...

class TetrisGame {

  public static TetrisGame instance;

  public Action onGameStart;
  public Action onGameEnd;
  public Action onGamePause;
  public Action onGameResume;

  bool isPaused;

  void Init() {
    instance = this;
  }

  void GameSequence() {
  // 게임 로직
  onGameStart.Invoke();
  onGameEnd.Invoke();
  }

  void TogglePause() {
    isPaused = !isPaused;
    if (isPaused) onGamePause.Invoke();
    else onGameResume.Invoke();
  }
}


class InGameUI {
  void Init() {
    TetrisGame.instance.onGameStart += OnGameStart;
    TetrisGame.instance.onGameEnd+= OnGameEnd;
    TetrisGame.instance.onGamePause+= OnGamePause;
    TetrisGame.instance.onGameResume+= OnGameResume;
  }

  void OnGameStart() { OpenMenu(); }
  void OnGameEnd() { CloseMenu(); }

  void OnGamePause() { OpenPauseMenu(); }
  void OnGameResume() { ClosePauseMenu(); }

  void OpenMenu() { }
  void CloseMenu() { }
  void OpenPauseMenu() { }
  void ClosePauseMenu() { }
}

파사드 클래스 없이 직접 연결하여 호출하되 게임 매니저에서는 UI 코드가 존재하지 않게 됩니다. 
UI 클래스를 싹 지우더라도 게임 인스턴스 클래스를 잘 돌아가고 있는거죠.
이걸 보고 '게임 코드'는 UI 코드를 모른다고 표현할 수 있겠지만, 코드를 작성하시는 분께서는 알고 염두에 두면서 구조를 짜게 되는 셈입니다.

이렇게 단순히 UI만 분리할거면 왜 필요한건가에 대한 의문이 들 수 있지만 아래와 같은 식으로 여러 모듈을 붙일수도 있게됩니다.

class LocalSpeaker {
  void Init() {
    TetrisGame.instance.onGameStart += OnGameStart;
    TetrisGame.instance.onGameEnd+= OnGameEnd;
    TetrisGame.instance.onGamePause+= OnGamePause;
    TetrisGame.instance.onGameResume+= OnGameResume;
  }

  void OnGameStart() { LoadBgmAndPlay(); }
  void OnGameEnd() { LoadLobbyMusic(); }

  void OnGamePause() { PauseMusic(); }
  void OnGameResume() { ResumeMusic(); }

  void LoadBgmAndPlay() { }
  void LoadLobbyMusic() { }
  void PauseMusic() { }
  void ResumeMusic() { }
}

극단적인 예시일 수 있지만 게임 인스턴스에서 일어나는 몇몇 이벤트나 함수에 따라 미리 지정된 델리게이트들이 호출되는 방식입니다. 가급적 간단하게 방식을 소개하기 위해 이런 코드를 사용했지만 MVP, Reactive 방식 등 구현체의 종류는 다양합니다. 대표적으로 유니티에서는 단편화된 옵저버패턴을 위해 UniRx가 많이 사용됩니다.

이렇게 'UI는 게임코드를 알고 구독해야하지만, 게임코드는 UI가 뭘 하는지 알 필요 없다' 에 대한 괜찮은 설명이 되었길 빕니다. 

참고로, 유니티에서는 어셈블리 정의 파일을 이용해서 강제적으로 이런 구조를 채택할 수 있습니다. 닷넷 어셈블리간에 Circular Refernece가 안 되므로 Runtime과 UI를 다른 어셈블리로 쪼개게 되면 일방향의 레퍼런스 방식을 가지게 됩니다. 자연스럽게 UI가 Runtime 코드를 참조해서 UI 로직을 작성하게됩니다.

answered (51 point)
선택됨
장문의 답변 정말 감사합니다

예시코드와 설명을 몇번씩 읽으니 대략적인 원하는 답을 얻은것 같습니다.

정말 좋은 설명이 됐습니다. UniRX라는 것도 알아가네요.

실력과 경험이 많이 부족하다는걸 다시 한번 깨닫게 됐습니다.
+2 votes
솔직히 이건 본인이 필요하다고 느낄때까지는 그냥 쓸데없는 짓입니다.

(전 학생때 코드 실행할 때 필요도 없는 코멘트를 왜 달라는지 이해를 못했습니다.ㅋㅋ)

하지만 객체지향부터 시작해서 이런 소프트웨어공학이 발전해온 이유가

다 그런걸 필요로하는 사람들이 많으니까 그런거고,

본인도 경험과 실력이 쌓이면 자연스럽게 필요성을 느끼게 될겁니다.

 

이런식의 디커플링이 가져오는 이점은

코드가 서로 독립적으로 돌아가기 때문에 이쪽에서 수정한게 저쪽에서 문제 일으키고 하는 상황이 줄어든다는게 제일 크고

(그만큼 코드가 거대해져도 관리하기가 수월하고 이해하기가 수월합니다.)

더 나아가면 확장성, 재사용성, 테스트 등등 많이 있습니다.

 

간단하게 테스트를 예로 들어보겠습니다.

애니팡처럼 랜덤하게 생성되는 블럭으로 진행되는 게임이 있다고할 때,

특정 블럭 배열에서만 100%발생하는 버그가 있다고 해봅시다.

디커플링이 잘 된 코드라면 랜덤생성 모듈을 떼고 테스트용 블럭생성 모듈을 붙여서

테스트 하는 동안은 문제있는 배열로만 블럭이 생성되도록 할 수 있습니다.

반대로 커플링이 강한 코드라면 기존 코드를 지우고 테스트용 코드를 넣은다음

버그를 수정하고 다시 원래의 코드로 돌려야하죠.

그런데 커플링이 강하면 기존코드를 지우는 과정에서 다른 모듈들이 영향을 받아서 이상 작동을 하기도 해서

최악의 경우 테스트 코드없이 저 상황 발생할 때까지 게임을 한참 플레이 해야할 수도 있습니다.

뭐 이쯤되면 버그가 저기서 발생하는 것인지 파악하는 것부터 쉽지 않기도 합니다.ㅋ

 

일단은 이런 방식이 있다는 것만 알아두고 경험을 계속 쌓아서 레벨업을 하세요.

레벨이 오르다보면 자연스럽게 안찍어놓은 스킬이 갖고 싶어질겁니다.

애초에 정답이 있는것도 아니라서 지금 100점 맞을 수 있게 공부하고 그대로 쭉 갈수 있는것도 아니에요.ㅋ
answered (218 point)
다크서모너님도 장문의 답변 정말 감사합니다.

저는 이제까지 자신의 정보를 보여주는 뷰는 자신이 직접 제어하고 업데이트한다는 생각으로

코드를 짰는데 답변 예시와 조언을 읽으면서 다시한번 코딩방식을 생각하게 되는 계기가 됐습니다.

버그 해결을 위해 도움을 구하고, 도움을 주세요. 우리는 그렇게 발전합니다.

throw bug 는 프로그래밍에 대한 전분야를 다룹니다. 질문,논의거리,팁,정보공유 모든 것이 가능합니다. 프로그래밍과 관련이 없는 내용은 환영받지 못합니다.

520 질문
675 answers
665 댓글
118,175 users