개요
프로젝트에서 UI 시스템을 어떻게 설계하고 사용하였는지 회고하고 소개하는 글
내용
구현된 UI 계층 구조
위와 같은 UI 계층 구조를 기반으로 ViewController, View, Presenter의 역할을 명확히 구분하여 UI를 설계했다.
- ViewController: UI의 카테고리를 구분하는 역할
- View, Presenter: UI의 기능별 역할을 분리
- Model - View - Presenter(MVP) 패턴 적용: 특정 데이터가 변경되면 자동으로 UI가 갱신되도록 구현
이러한 방식으로 UI를 구성하면, 필요한 UI 요소를 체계적으로 설계할 수 있고, 추상 클래스를 활용하여 일관된 구조로 빠르게 구현할 수 있다. 그리고 유지보수를 용이하게 했다
UI 시스템 초기화 시퀀스
프로젝트에서 모든 데이터는 적절한 Model 클래스가 관리한다
Presenter는 사용할 Model 클래스의 참조를 끌어와 필요하다면 데이터를 가공하고, 최종적으로 데이터를 View에게 넘긴다
View는 넘겨받은 데이터를 바탕으로 자기 자신을 업데이트 한다
즉 다음의 시퀀스로 초기화 된다
UI Manager
UI 매니저는 초기화가 필요한 ViewController List를 관리하고 DataManager를 전달받아 각 ViewController의 초기화를 수행한다
public class UIManager
{
private readonly List<ViewController> _viewControllers = new List<ViewController>();
public void AddViewController(ViewController viewController)
{
_viewControllers.Add(viewController);
}
public void Init(DataManager dataManager)
{
foreach (var elem in _viewControllers)
{
elem.Init(dataManager);
}
}
}
View Controller
View Controller는 UIManager에 자신을 전달하여 DataManager의 참조를 받아오고, Presenter와 View의 초기화를 담당한다
// ViewController 추상 클래스
public abstract class ViewController : MonoBehaviourBase
{
protected readonly CompositeDisposable disposable = new CompositeDisposable();
[SerializeField] private RootManager _rootManager;
private void Awake()
{
ValidateReferences();
RegisterToUIManager();
}
protected abstract void ValidateReferences();
private void RegisterToUIManager()
{
AssertHelper.NotNull(typeof(ViewController), _rootManager);
_rootManager.UIManager.AddViewController(this);
}
public abstract void Init(DataManager dataManager);
protected override void OnDestroy()
{
disposable.Dispose();
base.OnDestroy();
}
}
// 예시: 상속받은 VC 클래스 중 하나
public sealed class VC_UnitSpawn : ViewController
{
[SerializeField] private VW_UnitSpawn _vwUnitSpawn;
private readonly PR_UnitSpawn _prUnitSpawn = new PR_UnitSpawn();
[SerializeField] private VW_Currency _vwSpawnNeededGold;
private readonly PR_SpawnNeededGold _prSpawnNeededGold = new PR_SpawnNeededGold();
protected override void ValidateReferences()
{
AssertHelper.NotNull(typeof(VC_UnitSpawn), _vwUnitSpawn);
AssertHelper.NotNull(typeof(VC_UnitSpawn), _vwSpawnNeededGold);
}
public override void Init(DataManager dataManager)
{
_prUnitSpawn.Init(dataManager, _vwUnitSpawn, disposable);
_prSpawnNeededGold.Init(dataManager, _vwSpawnNeededGold, disposable);
}
}
Presenter
Presenter는 Model과 View를 이어주는 역할을 한다. 중간에 데이터 가공이 필요한 경우 Presenter에 비즈니스 로직을 구현한다
public abstract class Presenter
{
public abstract void Init(DataManager dataManager, View view, CompositeDisposable disposable);
}
// 예시: 상속받은 Presenter 클래스 중 하나
public sealed class PR_UnitSpawn : Presenter
{
private MDL_Unit _mdlUnit;
private MDL_Currency _mdlCurrency;
public override void Init(DataManager dataManager, View view, CompositeDisposable disposable)
{
AssertHelper.NotNull(typeof(PR_UnitSpawn), dataManager);
_mdlUnit = dataManager.Unit;
AssertHelper.NotNull(typeof(PR_UnitSpawn), _mdlUnit);
_mdlCurrency = dataManager.Currency;
AssertHelper.NotNull(typeof(PR_UnitSpawn), _mdlCurrency);
VW_UnitSpawn vwUnitSpawn = view as VW_UnitSpawn;
AssertHelper.NotNull(typeof(PR_UnitSpawn), vwUnitSpawn);
vwUnitSpawn!.btnSpawn.OnClickAsObservable()
.Subscribe(TrySpawnUnit)
.AddTo(disposable);
}
private void TrySpawnUnit(UniRx.Unit _)
{
if (!IsPossibleSpawn()) return;
ConsumeSpawnCost();
SUnitSpawnRequestData data = new SUnitSpawnRequestData(GetRandomGrade(), GetRandomType(), EPlayerSide.South);
_mdlUnit.SpawnUnit(data);
}
// ...
}
View
UI 컴포넌트를 소유한 클래스다. Presenter에게서 데이터를 전달받고 이를 UI에 표시한다
혹은 UI에서 이벤트를 받고(클릭 등) 이를 Presenter를 통해 Model에게 전달하는 역할을 한다
// 형 변환하여 사용하기 위한 추상 클래스
public abstract class View : MonoBehaviourBase
{
}
// 예시: View를 상속받은 클래스 중 하나
public sealed class VW_UnitSpawn : View
{
[SerializeField] internal Button btnSpawn;
private void Awake()
{
AssertHelper.NotNull(typeof(VW_UnitSpawn), btnSpawn);
}
}
UI On/Off 기능 구현
운빨존많겜의 인게임 UI는 중첩되어 열리는 것이 없다. 따라서 Stack에 Canvas를 쌓는 구조는 사용하지 않았다
내가 사용한 방식은 ReactiveProperty로 UI 구조의 on/off를 관리하도록 구현하는 방법이다
도박 패널 오픈 버튼으로 예를 들어보겠다
public class MDL_GameSystem
{
// 도박 패널의 on/off 여부를 나타내는 Rx.
private readonly ReactiveProperty<bool> _gamblePanelVisible = new ReactiveProperty<bool>(false);
public IReactiveProperty<bool> GamblePanelVisible => _gamblePanelVisible;
public void SetGamblePanelVisible(bool value) => _gamblePanelVisible.Value = value;
}
public sealed class PR_GambleButton : Presenter
{
public override void Init(DataManager dataManager, View view, CompositeDisposable disposable)
{
AssertHelper.NotNull(typeof(PR_GambleButton), dataManager);
VW_GambleButton vw = view as VW_GambleButton;
AssertHelper.NotNull(typeof(PR_GambleButton), vw);
MDL_GameSystem mdl = dataManager.GameSystem;
AssertHelper.NotNull(typeof(PR_GambleButton), mdl);
vw!.gambleBut.OnClickAsObservable()
.Where(_ => !mdl.GamblePanelVisible.Value)
.Subscribe(_ => mdl.SetGamblePanelVisible(true))
.AddTo(disposable);
}
}
public sealed class VW_GambleButton : View
{
[SerializeField] internal Button gambleBut;
private void Awake()
{
AssertHelper.NotNull(typeof(VW_GambleButton), gambleBut);
}
}
위와 같은 흐름처럼 이벤트를 발행하는 방식으로 구현하였다
패널 Presenter는 발행된 이벤트를 View에게 그대로 전달해주기만 한다
public sealed class PR_GamblePanel : Presenter
{
public override void Init(DataManager dataManager, View view, CompositeDisposable disposable)
{
// ...
mdlSystem.GamblePanelVisible
.Subscribe(vw.SetGamblePanelVisible)
.AddTo(disposable);
// ...
}
// ...
}
그리고 Panel UI는 Background Exit Panel 이라는 알파값 0의 이미지를 최상단에 깔아 혹시라도 화면 바깥을 클릭하면 패널이 바로 꺼지게 만들어 오동작을 방지하였다
마무리
이번 프로젝트에서는 UI Root - ViewController - View 계층 구조로 UI 게임 오브젝트를 세팅하고, MVP (Model-View-Presenter) 패턴을 적용하여 UI 시스템을 설계했다
- ViewController를 통해 UI의 카테고리를 구분하고,
- Presenter를 활용하여 Model과 View 사이에서 데이터 변화를 관리하며,
- View에서 UI 업데이트와 사용자 입력을 처리하는 방식으로 구조를 정리했다
또한, ReactiveProperty를 활용한 UI On/Off 관리 방식을 도입하여 UI의 상태 변경을 이벤트 기반으로 처리했다
이를 통해 UI가 중첩되는 문제를 방지하고, 명확한 흐름으로 관리할 수 있도록 했다
이러한 UI 시스템을 구축하면서 일관된 구조와 유지보수의 용이성을 고려하였고, 추후 기능 확장이 필요한 경우에도 쉽게 추가할 수 있도록 설계했다
이번 구현을 바탕으로, 다음 프로젝트에서는 UI 최적화 및 동적 UI 관리 방식을 더욱 발전시킬 수 있을 것으로 본다
'프로젝트 > Lucky Defense (InGame Clone)' 카테고리의 다른 글
운빨존많겜 인게임 클론 프로젝트 소개 (0) | 2025.03.14 |
---|---|
AI Player 동작 구현 (0) | 2025.03.14 |
유닛 배치 필드 구현 (0) | 2025.03.14 |
에너미와 유닛 재활용을 위한 풀링 시스템 (0) | 2025.03.14 |
에너미와 유닛 프리팹 구조 (0) | 2025.03.14 |