개요
GoF 디자인 패턴중 생성 패턴에 해당하는 Builder패턴을 이용하여 팝업UI 시스템을 구현한 것을 기록한다.
GoF디자인 패턴은 아래 블로그에 자세하게 설명해주셨다.
Builder패턴은
동일한 프로세스를 거쳐 다양한 구성의 인스턴스를 만드는 방법이다.
만들어야할 객체가 다른옵션 한두개만 가지고있다면
생성자 혹은 따로 Init()함수를 만들어서 파라미터로 옵션을 넘겨줘도 될것이다.
하지만,
옵션이 점점 늘어나게 된다면
가독성도 떨어지고 생성할때도 매개변수를 하나하나보고 옵션을 선택해줘야한다.
이렇게 생성할 객체의 옵션이 많을때 유용한 패턴이다.
이 글에서는 PopupUI에 적용한 과정을 기록한다.
참고한 블로그
본문
만들 스크립트는 4가지이고 다음과 같다.
1. PopupInfo
2. Popup
3. PopupButton
4. PopupManager
1. PopupInfo
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
public class PopupInfo
{
public string Title { get; private set; }
public string Content { get; private set; }
public bool PauseScene { get; private set; }
public Enums.PopupButtonType[] ButtonTypes { get; private set; }
public Action<Enums.PopupButtonType> Listener { get; private set; }
private PopupInfo(Builder builder)
{
Title = builder.Title;
Content = builder.Content;
PauseScene = builder.PauseScene;
ButtonTypes = builder.ButtonTypes;
Listener = builder.Listener;
}
// 하위 Builder클래스
public class Builder
{
public string Title { get; private set; }
public string Content { get; private set; }
public bool PauseScene { get; private set; }
public Enums.PopupButtonType[] ButtonTypes { get; private set; }
public Action<Enums.PopupButtonType> Listener { get; private set; }
public Builder()
{
Title = string.Empty;
Content = string.Empty;
ButtonTypes = null;
Listener = null;
PauseScene = false;
}
// 제목 세팅
public Builder SetTitle(string title)
{
this.Title = title;
return this;
}
// 컨텐츠(본문 내용) 세팅
public Builder SetContent(string content)
{
this.Content = content;
return this;
}
// 어떤 버튼들이 들어갈지 세팅
public Builder SetButtons(params Enums.PopupButtonType[] buttonTypes)
{
this.ButtonTypes = buttonTypes;
return this;
}
// 콜백 세팅
public Builder SetListener(Action<Enums.PopupButtonType> listener)
{
this.Listener = listener;
return this;
}
// 팝업이 켜졌을때 시간을 멈출지 세팅
public Builder SetPauseScene(bool isPause)
{
this.PauseScene = isPause;
return this;
}
// 최종 빌드
public PopupInfo Build()
{
return new PopupInfo(this);
}
}
}
그리고 Enum타입인 PopupButtonType은 따로 Enum을 보관하는 클래스에 아래와 같이 정의했다.
public enum PopupButtonType
{
None,
Yes, // 예
No, // 아니오
Confirm, // 확인
Close, // 닫기
Replay, // 다시하기
GoHome, // 홈으로
FinishGame, // 게임종료
Start // 시작
}
이제 위 PopupInfo와 Builder클래스를 사용해 팝업의 옵션을 세팅해서 넘기게 구현을 하게 되는데
사용법을 미리 보자면 이렇다.
var info = new PopupInfo.Builder().SetContent("테스트 입니다.")
.SetButtons(Enums.PopupButtonType.Close, Enums.PopupButtonType.Confirm)
.SetListener((type) =>
{
switch (type)
{
case Enums.PopupButtonType.Close:
currentActivePopup.Hide();
break;
case Enums.PopupButtonType.Confirm:
currentActivePopup.Hide();
break;
}
})
.Build();
PopupManager.Instance.ShowPopup(info);
2. Popup
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using Lean.Gui;
using System;
[RequireComponent(typeof(LeanWindow))]
public class Popup : MonoBehaviour
{
[SerializeField]
protected Text contentText;
[SerializeField]
protected Transform buttonParent;
[SerializeField]
protected PopupButton buttonPrefab;
protected LeanWindow myLeanWindow;
protected Action<Enums.PopupButtonType> buttonClickedAction;
protected List<PopupButton> myButtons = new List<PopupButton>();
#region 프로퍼티
public bool IsShow
{
get
{
if (myLeanWindow == null)
myLeanWindow = GetComponent<LeanWindow>();
return myLeanWindow.On;
}
}
#endregion
public void Init(PopupInfo info)
{
// 컨텐츠 세팅
contentText.text = info.Content;
// 콜백 세팅
buttonClickedAction = info.Listener;
// 버튼 세팅
SetButtons(info.ButtonTypes);
}
protected virtual void SetButtons(Enums.PopupButtonType[] buttonTypes)
{
myButtons = new List<PopupButton>();
for (int i = 0; i < buttonTypes.Length; i++)
{
// 버튼 생성
var clone = Instantiate(buttonPrefab, buttonParent);
var info = PopupManager.Instance.GetButtonInfo(buttonTypes[i]);
clone.Init(info.ButtonText, buttonTypes[i], this);
myButtons.Add(clone);
}
}
public void Show()
{
if (myLeanWindow == null)
myLeanWindow = GetComponent<LeanWindow>();
myLeanWindow.On = true;
}
public void Hide()
{
if (myLeanWindow == null)
myLeanWindow = GetComponent<LeanWindow>();
foreach(var btn in myButtons)
{
Destroy(btn.gameObject);
}
PopupManager.Instance.OnPopupClose(this);
myLeanWindow.On = false;
}
public void OnButtonClicked(PopupButton btn)
{
// 콜백 실행
buttonClickedAction?.Invoke(btn.ButtonType);
}
}
설명에 앞서, 프로젝트에 바로 적용한 코드로 주석만 달고 기록중이라서 에셋을 쓴 부분이 있다.
LeanWindow클래스는 LeanGUI에셋의 클래스인데
켜고 끄는 기능만 쓰고있어서 gameObject.SetActive()로 대체 가능하다.
팝업 프리팹은 다음과 같이 구성했다.
버튼의 부모 오브젝트인 Buttons는
Button오브젝트를 자식으로 추가했을 때 알아서 간격이 잡힐 수 있게
Horizontal Layout Group을 추가해줬다.
3. PopupButton
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
// UnityEngine.UI.Button 컴포넌트를 상속
public class PopupButton : Button
{
[SerializeField]
private Text buttonText;
[SerializeField]
private Enums.PopupButtonType buttonType;
public Enums.PopupButtonType ButtonType { get => buttonType; }
public Popup ParentPopup { get; set; }
// 초기화 함수 => 버튼에 들어갈 String, 버튼 타입, 부모 팝업을 변수로 받는다
public void Init(string buttonStr, Enums.PopupButtonType buttonType, Popup parentPopup)
{
if (buttonText == null)
buttonText = GetComponentInChildren<Text>();
buttonText.text = buttonStr;
this.buttonType = buttonType;
this.ParentPopup = parentPopup;
}
// 버튼이 클릭됐을때 실행된다.
public override void OnPointerClick(PointerEventData eventData)
{
base.OnPointerClick(eventData);
ParentPopup.OnButtonClicked(this);
}
}
PopupButton은 Unityengine.UI.Button컴포넌트를 상속받아 작성했고,
인스펙터에서 따로 세팅해줄것은 없다.
이 스크립트를 붙여서 프리팹으로 만들어둔다.
4. PopupManager
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using Sirenix.OdinInspector;
using UnityEngine.UI;
// Dictionary를 인스펙터에서 보기 위해서 "Odin Inspector"에셋의 "SerializedMonoBehaviour"를 상속 받았다.
public class PopupManager : SerializedMonoBehaviour
{
public static PopupManager Instance { get; private set; }
// 팝업 프리팹
[SerializeField]
private Popup popupPrefab;
// 팝업이 나타났을때 어둡게 만들어줄 배경 이미지
[SerializeField]
private Image darkBg;
// 버튼타입에대한 정보를 저장할 딕셔너리 에셋을 사용해서 인스펙터에서 보이게 만든 후 세팅
[SerializeField]
Dictionary<Enums.PopupButtonType, PopupButtonInfo> buttonInfoDict;
// 팝업을 재사용할 풀 리스트
private List<Popup> popupPool = new List<Popup>();
// 현재 켜진 팝업
private Popup currentActivePopup;
private void Awake()
{
if(Instance == null)
Instance = this;
}
// 테스트용 팝업 코드
private void Update()
{
if(Input.GetKeyDown(KeyCode.Space))
{
var info = new PopupInfo.Builder().SetContent("테스트 입니다.")
.SetButtons(Enums.PopupButtonType.Close, Enums.PopupButtonType.Confirm)
.SetListener((type) =>
{
switch (type)
{
case Enums.PopupButtonType.Close:
currentActivePopup.Hide();
break;
case Enums.PopupButtonType.Confirm:
currentActivePopup.Hide();
break;
}
})
.Build();
ShowPopup(info);
}
}
// 팝업 띄우기
public void ShowPopup(PopupInfo info)
{
// TODO: 다중으로 띄워야 한다면 고치기 => currentActivePopup을 List로
if (currentActivePopup != null)
return;
Popup popup = null;
foreach(var pop in popupPool)
{
if(!pop.IsShow)
{
popup = pop;
break;
}
}
if (popup == null)
{
popup = Instantiate(popupPrefab, transform);
popupPool.Add(popup);
}
popup.Init(info);
popup.Show();
currentActivePopup = popup;
darkBg.enabled = true;
}
// 딕셔너리에서 value받아오기 (외부에서 호출 용도)
public PopupButtonInfo GetButtonInfo(Enums.PopupButtonType type)
{
if (!buttonInfoDict.ContainsKey(type))
return null;
return buttonInfoDict[type];
}
// 팝업이 닫혔을 때 실행 (Popup클래스의 Hide()함수와 연결돼있다)
public void OnPopupClose(Popup pop)
{
darkBg.enabled = false;
if (pop == currentActivePopup)
currentActivePopup = null;
}
// 외부 클래스에서 현재 활성화된 팝업을 끄기위해 호출
public void CloseCurrentActivePopup()
{
if (currentActivePopup == null)
return;
currentActivePopup.Hide();
}
}
/*
* 버튼 정보를 저장할 클래스
* 더 확장하면 버튼 컬러나 버튼의 Sprite등을 저장해두면 된다
*/
[Serializable]
public class PopupButtonInfo
{
[SerializeField]
private string buttonText;
public string ButtonText { get => buttonText; }
}
대망의 PopupManager 클래스이다.
팝업을 켜고 끄는것을 담당하고, 그밖에도 버튼 타입에따른 버튼의 설정을 딕셔너리에 저장한다.
나는 딕셔너리를 인스펙터에서 보기위해 "Odin Inspector"라는 에셋을 사용하였는데
유료에셋이기 때문에 혹시나 이걸보고 구현해볼려는 분이 계시다면
무료에셋중에 딕셔너리를 인스펙터에 띄워주는 에셋을 찾으셔야 합니다.
세팅은 이렇게 Canvas를 따로 만들어줬다.
팝업 프리팹과, 어둡게할 배경, 버튼 정보 딕셔너리를 손으로 세팅하면 된다.
나중에 확장을하면 PopupButtonInfo 클래스에
Color, Sprite등의 속성도 추가해도 좋을것 같다.
// 테스트용 팝업 코드
private void Update()
{
if(Input.GetKeyDown(KeyCode.Space))
{
var info = new PopupInfo.Builder().SetContent("테스트 입니다.")
.SetButtons(Enums.PopupButtonType.Close, Enums.PopupButtonType.Confirm)
.SetListener((type) =>
{
switch (type)
{
case Enums.PopupButtonType.Close:
currentActivePopup.Hide();
break;
case Enums.PopupButtonType.Confirm:
currentActivePopup.Hide();
break;
}
})
.Build();
ShowPopup(info);
}
}
이제 위의 코드로 팝업을 생성해보면
동적으로 생성된 팝업이 나오게 된다.
결론
1. 사람이할 일은 줄이면서
2. 사용법은 쉽게
그렇게 만드는것이 좋은 모듈이라고 생각한다.
이 Builder패턴을 사용한 팝업시스템은
개인적인 생각으로는 위 2가지 조건을 모두 만족하는 좋은 모듈인것 같다.
유명한 디자인 패턴들은 확실히 써보면 왜 이걸 이제야 알았을까싶다.
틈틈히 다른 디자인 패턴들도 익혀둬야겠다.
'Unity > C#' 카테고리의 다른 글
[Unity] 짧은 팁 - 사용중인 시스템 메모리 용량 구하기 (0) | 2024.04.29 |
---|---|
[C#] DateTime에서 남은시간 계산하기 (0) | 2023.02.25 |
[Unity] GoogleSheetsToUnity에셋 활용 동적 스크립터블 오브젝트 생성기 제작 (0) | 2022.10.25 |
[Unity, C#] 오브젝트 풀링 (0) | 2022.10.04 |
[Unity, C#] 박스 콜라이더안에 랜덤포인트 구하기 (0) | 2022.10.01 |