개발 조각글

Unity Refactoring - 달팽이키우기 구조 재설계

BaekNohing 2022. 7. 29. 00:30

a cute cat snail

 

달팽이 키우기 리팩토링 목차

  1. 리팩토링 준비
  2. 구조 재설계
  3. UI Manager 설계
  4. 클래스간 관계
  5. Action 분리하기
  6. 핵심루프 설계
  7. 유닛 테스트

Unity Refactoring - 달팽이키우기 구조 재설계


당장 코드를 짜서 클래스를 나누는 것 보다. 이 게임의 구조가 어떻게 되어있는지. 그리고 어떤 형태가 되어야 하는게 좋을지 먼저 설계를 하고 들어가는게 낫겠다고 생각했다. 

Old Structur

이전 설계

먼저 기존의 달팽이 키우기 구조는 다음과 같이 되어있었는데, 왜냐하면 당시 게임을 제작할 때 위에서 아래로 정의된 기능들을 추가하듯이 작성했기 때문이다. 

한 기능을 구현한 다음에는 바로 다음 기획을 구현하고 그 다음엔 그 다암 기획을 구현하는 식으로 만들었기 때문에 Data, GameAction, UI가 모두 한 클래스에서 관리되고 있었다.

몇가지 스탯이 일정 주기마다 떨어지고, 유저는 버튼클릭을 통해 스탯을 회복할 수 있고. 스탯이 일정 수치 아래로 내려간 채 일정 시간이 지나면 죽되, 죽지 않은 상태를 유지하면 다른 달팽이로 진화하는. 4가지가 전부인 간단한 게임이었기 때문에 저렇게 짜도 어찌어찌 출시까지 만들어 낼 수 있었지만.. 다시 손을 대는 입장에서는, 저 구조를 방치하면 안된다는 생각을 했고. 다음과 같이 고치기로 했다. 


New Structur

새 설계

먼저 Data, Action, UI를 세 파트로 나누고. 나뉜 세 클래스는 Manager라는 오브젝트에 집어넣어 this.getComponent<>()로 서로를 찾을 수 있게 한다. (이렇게 하면 싱글톤을 쓰지 않을 수 있다..!, 세 기능을 분리시키는 작업이기 때문에. 전역적인 상태의 특성 상 암묵적으로 결합도가 높이는 싱글톤은 이 상황에서 좋은 방식이 아니라고 생각했다. 하지만 싱글톤은 빠르게 무언가를 만들기에는 여전히 좋은 솔루션이다. 지금은 별로 그렇게까지 빠르게 만들 필요가 없기도 했다.)


데이터

유저의 데이터는 PlayerPerf가 아니라, ScriptableObject에 저장하기로 했다. Playerperf는 값이 레지스트에 저장되기 때문에. 어떤 값이 저장되었는지 확인하기 영 불편해서, Project폴더와 인스펙터에서 값을 확인할 수 있는 ScriptableObject가 더 나은 방식이라고 판단했기 때문이다. 

액션

그리고 게임 내 모든 Action을 관리하는 클래스를 만들기로 했는데, 다른 클래스는 Action클래스에 작동해야 하는 기능을 등록하고. Action에서는 매 틱마다 등록된 액션을 실행하는 방식으로 만들기로 했다. 이렇게 딱 한 지점에서만 Action과 다른 클래스가 닿아있다면. 내가 UI클래스를 어떻게 수정하든, ActionRegist를 계속 유지시키는 것으로 전체적인 구조에 문제가 없음을 보장할 수 있다고 생각했기 때문이다. 

public struct OptionalAction
{
    public string actionKey;
    public bool isDisposable;

    public SnailStatusObject.SingleStatus target;
    public Condition condition;
    public float value;
    public System.Action action;
    
    OptionalAction
        (string actionKey, 
            SnailStatusObject.SingleStatus target, 
            Condition condition, 
            float value, 
            System.Action action, 
            bool isDisposable = false)
        {
            this.actionKey = actionKey;
			...
        }
    ...
    
}

그리고 값을 비교하는 등의 데이터나 조건이 필요한 Action을 등록할 때는, Action을 넘길 때 필요한 인자들을 구조체로 선언해서 넘겨줄 수 있도록 했다(이 때 데이터를 확인하기 필요한 SnailStatusObject.SingleStatus => ScriptableObject도 직접 넘겨줌으로써 전역상태의 참조가 일어나지 않도록 했다). 

그리고 구조체의 생성자를 정의하는 것으로, Action을 넘기기 위한 구조체에서 어떤 값들을 넘겨받길 기대하는지 명시해 두었다. 

UI

UI는 Data, Action과 상호작용할 수 있도록 UI 관리 클래스를 다른 클래스처럼 Manager안에 집어넣되. 모든 UI에서 해당 관리 클래스를 찾을 수 있도록 어디서나 접근할 수 있는 Centeral UI Component를 Static하게 딱 하나 만들어 두었다.

물론 각 UI 클래스마다 직접 UI관리 클래스를 캐싱하고 있는 방법도 있을 수 있지만, UI에서는 하나가 켜질 때 다른 창들을 비활성화 한 뒤 타겟이 되는 창을 활성화 시키는 작업을... 앗 아니다, 생각해보니 여기서도 전역상태를 둘 필요가 없구나, 어차피 UiManager가 Awake 시점에 find로 pnl과 btn들을 찾고 init를 해줄텐데 그때 Manager를 넘겨주면 되겠구나..?! 음,. UI쪽은 좀 더 생각해봐야겠다.