의존성 주입을 통해 Controller에서 View를 초기화해주도록 하지만, View가 Controller의 참조를 가질 수 있도록 하였음.
💡
절제된 싱글톤을 활용한 UI 매니저
싱글톤은 장단점이 명확하고 사용 방법에 있어 많은 논쟁이 있는 패턴입니다.
그중에서도 우리 팀은 장점을 극대화하기보다는 단점을 보완하여 활용하기로 하였습니다.
📌 단점을 보완할 규칙
public class UIManager : MonoBehaviour
{
...
private Dictionary<Type, BaseUI> _uis;
public T GetUI<T>() where T : BaseUI {...}
public T OpenUI<T>() where T : BaseUI {...}
public void CloseUI<T>() where T : BaseUI {...}
...
}
규칙
1. 데이터는 오직 UI 오브젝트만 가질 것
2. 가장 기본적인 동작만을 할 것 (열기, 닫기, 가져오기, etc.)
3. UI에 필요한 초기화는 리턴 값을 활용할 것
이 3가지 규칙을 통하여 의존성을 줄이고 디버깅의 효율을 상승시켰으며 쉽고 편리하게 UI에 전역적인 접근이 가능하게 만들었습니다.
추가적으론 리턴 값을 활용하였기에 UI의 데이터 갱신이 편리합니다.
🔨 실제 사례
UIManager um = Managers.Instance.UIManager;
OptionUI ui = um.OpenUI<OptionUI>();
ui.Init(() => { um.OpenUI<TitleUI>(); });
um.CloseUI<TitleUI>();
💡
클린 아키텍쳐를 활용한 용사 - 토벌 데이터 교환
토벌의 View에서 토벌에 대한 정보(몬스터 풀, 아이템 풀, 페이즈 수 등)도 필요하고, View에 보여줄 용사에 대한 정보도 필요한 상황
클린 아키텍쳐와 의존성 주입을 활용해 시스템을 구현하였음.
📌 디자인 요소 1. 클린 아키텍쳐를 적용한 M-V-C의 관계
View → Controller → Model :
Model은 MonoBehaviour 상속 없이 데이터와 그 데이터의 직접적인 처리 메서드만을 가진다.
Controller는 관리할 Model의 참조를 가진다.
Controller는 Model에 내장된 함수를 실행시키거나, 다른 컨트롤러가 Model의 정보를 필요로 할 때 넘겨주는 역할을 한다
View는 Model의 참조를 가지는 Controller의 참조를 가진다.
View는 사용자의 입력을 Controller에 전달하고, 필요한 시점에 Model을 내려받아 갱신한다.
📌 디자인 요소 2. 어떻게 초기화해줄 것인가?
플레이어 초기화 시 ‘Controller들을 묶어둔 클래스’ 초기화 → Controller 초기화
그 후, UIManager에 ‘Controller들을 묶어둔 클래스’를 인자로 넘겨 Controller를 필요로 하는
UI들에게 Controller들을 의존성 주입 → 하나의 View에서 여러 Controller의 참조를 가지는 게 수월해짐
UIManager의 Init 함수
동적인 데이터(랜덤한 영웅, 스테이지 데이터)의 생성을 위한 Builder
Builder는 해당 Model을 필요로하는 Controller의 참조를 가짐.
사용자의 입력으로 Model이 생성되는 경우(스테이지 데이터) View에게 Builder를 가지고
사용자의 입력 없이 Model이 생성되는 경우(랜덤 용사 생성) Controller가 직접 Builder를 가지게 함.
Builder는 Model 생성에 필요한 데이터를 수집하여 필요한 시점에 Build 해주고, 동시에 Model의 참조를 Controller에게 의존성 주입함.
결과적으로 아래에서 위를 바라보는 구조가 됨
💥트러블 슈팅
💡
Dictionary를 직렬화 가능하게 만들어 스크립터블 오브젝트에 집어넣기
📌 문제의 알고리즘과 분석
아이템에 필요한 스프라이트를 스크립터블 오브젝트에 넣어 관리하려고 하는데,
<int, Sprite> Dictionary를 넣게 되면 해당 아이템의 코드를 키로 넣어 스프라이트를 받아올 수 있어 관리하기 용이함.
하지만 Dictionary는 라이브러리 없이 직렬화가 되지 않기 때문에 Inspector에서 보이지 않아 에디터에서 편집 불가능한 문제가 발생
📌 어떻게 해결할 것인가?
다음과 같이 Dictionary와 ISerializationCallbackReceiver 인터페이스를 상속받아 직렬화와 역직렬화 시의 처리 방법을 직접 구현
코드
public class SerializedDictionary<V> : Dictionary<int, V>, ISerializationCallbackReceiver
{
[Serializable]
public class KeyValue
{
public int Key;
public V Value;
public KeyValue(int key, V value)
{
Key = key;
Value = value;
}
}
[SerializeField]
List<KeyValue> KeyValues = new();
public void OnBeforeSerialize()
{
KeyValues.Clear();
foreach (KeyValuePair<int, V> pair in this)
{
KeyValues.Add(new KeyValue(pair.Key, pair.Value));
}
}
public void OnAfterDeserialize()
{
this.Clear();
for (int i = 0, icount = KeyValues.Count; i < icount; ++i)
{
int key = KeyValues[i].Key;
while (this.ContainsKey(key)) ++key;
this.Add(key, KeyValues[i].Value);
}
}
}
Dictionary의 Key를 int로 한정지은 이유는, 스크립터블 오브젝트의 요소를 추가할 때 기존 요소와 같은 값이 추가되기 때문에 역직렬화가 되지 않기 때문에 int로 한정지어 Key 중복 시 다음 값의 Key를 가지도록 하였음.
📌 결과
아이템 코드와 그 순서에 상관 없이 스크립터블 오브젝트에 값을 추가하여 이미지를 ItemCode로만 접근이 가능하도록 제작하였음.
💡
인터페이스를 통한 건물 상호작용 규격화
📌 문제의 알고리즘과 분석
건물마다 클릭과 상호작용 버튼을 통해 작동하는 기능 구현이 필요
따로따로 구현하면 공통적인 작동 방식이 너무 많은 상황
📌 어떻게 해결할 것인가?
인터페이스로 클릭과 상호작용에 대한 작동방식 규격화
public interface IInteractable
{
public void Interact();
public string GetName();
}
📌 결과
상호작용이 필요한 오브젝트에 IInteractable 인터페이스를 상속받은 컴포넌트를 구현하기만 하면 상호작용이 구현됨
public void Interact()
{
if (PointingObject == null) return;
if (!PointingObject.transform.parent.TryGetComponent(out IInteractable interactable)) return;
interactable.Interact();
}
💡
뭉쳐있는 함수의 기능 분리하고 상황에 맞춰 사용하기
건물의 생성은 하나의 절차로만 생성되는 것이 아닌 순서대로 절차들을 진행해야지 비로소 완성이 됩니다.
하지만 플레이어가 초기에 사용할 건물들을 미리 만들어둘 필요가 생겼고, 현재의 알고리즘으로는 불가능하였습니다.
📌 문제의 알고리즘과 분석
문제점 1.
마우스로 입력하지 않은 값은 건설이 불가능하다.
문제점 2.
건설 가능 여부를 확인 후 청사진은 필수적으로 설치가 된다.
초기에 만들어줄 건물은 마우스로 입력하는 게 아닌 코드로 좌표를 입력해줘야 하며, 건설이 가능한지 판단 후 청사진이 아닌 실제 건물이 생성되어야 했지만 불가능 했습니다.
📌 어떻게 해결할 것인가?
문제점 1 분석.
건설 위치를 선정하는 값은 마우스 입력과 상관없이 Vector3의 값만 있으면 가능하다.
문제점 2 분석.
건설을 시도하기 전에도 건설 가능 여부를 판단할 시점이 존재한다.
두 가지 문제점의 공통적인 부분은 하나의 기능이 만들어낸 값을 오로지 내부에서만 처리하고 처리하는 과정도 전혀 다른 기능이었습니다.
그리하여 값을 전달하는 방식으로 변경하고 하나의 함수의 기능을 나누기로 했습니다.
📌 결과
코드뿐만이 아닌 다른 방식으로도 값만 주면 건설 여부를 판단할 수 있게 되었고, 판단 여부를 콜백을 받고 어떤 행동을 할지도 자유롭게 설정 가능하게 되었습니다.