✅ 기능
Windows 및 MacOS 등 운영체제에서 사용자에게 정보를 전달하거나 동작선택을 위해서 다이얼로그박스를 보여주는 경우가 많습니다. 게임 또한 같은 이유로 제공할 필요가 있습니다. 다이얼로그박스 시스템을 구축하여 스크립트에서 동적으로 생성하고, 이벤트를 받고 이벤트를 처리할 수 있도록 하였습니다.
✅ 이 기능의 특징
1. 간단한 알림성 다이얼로그박스를 매우 쉽게 만들 수 있습니다.
2. 다이얼로그박스에 여러 요소를 집어넣으면 드래그바가 활성화되어 모든 콘텐츠를 볼 수 있습니다.
3. 다이얼로그박스에 프리팹으로 만들어놓은 다른 UI요소를 추가하고 이벤트를 제어할 수 있습니다.
4. 이벤트를 제어하여 복잡한 기능 또한 제공할 수 있습니다 (사용 예시, 실제 회원가입/로그인 서비스)
✅ 사용 예시
- 깃허브 링크에 기능이 포함된 샘플 프로젝트를 업로드했습니다. 다운로드하여 사용할 수 있습니다!
📃 전체 스크립트(더보기)
더보기
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TMPro;
using System;
using UnityEngine.UI;
namespace DialogBox
{
#region 열거형 옵션
/// <summary>
/// 정렬 옵션
/// </summary>
public enum Align
{
/// <summary>
/// 왼쪽
/// </summary>
LEFT,
/// <summary>
/// 오른쪽
/// </summary>
RIGHT,
/// <summary>
/// 가운데 (크기는 변하지 않음)
/// </summary>
CENTER,
/// <summary>
/// 가운데 확장 (크기를 좌우로 최대로 늘림)
/// </summary>
EXPAND,
}
/// <summary>
/// 입력필드 이벤트
/// </summary>
public enum InputFieldEvent
{
/// <summary>
/// 입력값이 변경될 때
/// </summary>
OnValueChanged,
/// <summary>
/// 변경이 끝난경우 (포커스 해제)
/// </summary>
OnEndEdit,
/// <summary>
/// 선택될 때
/// </summary>
OnSelect,
/// <summary>
/// 선택이 끝날 때
/// </summary>
OnDeselct,
}
#endregion
public class DialogBoxController : MonoBehaviour
{
private const int BORDER_GAP = 20; //보더에 의해 콘텐츠 영역에서 차지하는 크기
public const string RESERVED_EVENT_CLOSE = "RESERVED_EVENT_CLOSE";
public const string RESERVED_EVENT_ON_DESTROY = "RESERVED_EVENT_ON_DESTROY";
[Space(50)]
[Header("다이얼로그 박스 루트(캔버스 자식)")]
[SerializeField] private GameObject mDialogBoxRoot;
[Space(50)]
[Header("타이틀 라벨")]
[SerializeField] private TextMeshProUGUI mTitleLabel;
[Space(50)]
[Header("추가할 텍스트 라벨 프리팹")]
[SerializeField] private GameObject mTextLabelPrefab;
[Header("추가할 버튼 프리팹")]
[SerializeField] private GameObject mButtonPrefab;
[Header("추가할 보더(이미지) 프리팹")]
[SerializeField] private GameObject mBorderPrefab;
[Header("추가할 입력필드 프리팹")]
[SerializeField] private GameObject mInputFieldPrefab;
[Space(50)]
[Header("상단(타이틀) 영에 추가될 오브젝트들의 부모 트랜스폼")]
[SerializeField] private RectTransform mTopContentsParent;
[Header("콘텐츠 영역에 추가될 오브젝트들의 부모 트랜스폼")]
[SerializeField] private RectTransform mCenterContentsParent;
[Header("하단 상호작용 영역에 추가될 오브젝트들의 부모 트랜스폼")]
[SerializeField] private RectTransform mBottomContentsParent;
[Space(50)]
[Header("콘텐츠 영역의 최상위 부모 트랜스폼")]
[SerializeField] private RectTransform mContentsScrollViewRoot;
private Vector2Int mDialogBoxRootSize; //활성화 된 다이얼로그박스의 사이즈
private Action<DialogBoxController, string> mEventAction; //이 다이얼로그박스가 호출할 이벤트
private Dictionary<string, DialogBoxController> mDestroyCallEvents = new Dictionary<string, DialogBoxController>(); //다이얼로그박스가 파괴될때 호출할 이벤트
private Dictionary<string, DialogBoxController> mReferenceDialogBoxes = new Dictionary<string, DialogBoxController>(); //참조할 다이얼로그 박스
#region 인스턴스된 오브젝트
private Dictionary<string, GameObject> mInstantiatedObjects = new Dictionary<string, GameObject>(); //인스턴스된 입력필드들
#endregion
/// <summary>
/// 다이얼로그박스를 초기화합니다.
/// </summary>
/// <param name="boxWidth">가로크기 (0 ~ 800)</param>
/// <param name="boxHeight">세로크기 (0 ~ 600)</param>
/// <param name="eventAction">다이얼로그박스 요소들이 이벤트를 호출할경우 전달될 대리자</param>
public void InitDialogBox(int boxWidth, int boxHeight, Action<DialogBoxController, string> eventAction = null)
{
//크기 설정
RectTransform rootRectTransform = mDialogBoxRoot.GetComponent<RectTransform>();
rootRectTransform.sizeDelta = mDialogBoxRootSize = new Vector2Int(boxWidth, boxHeight);
RefreshBoxSize();
ToggleDialogBox(true);
//이벤트 버튼 설정 (있는경우)
if (eventAction != null)
{
mEventAction += eventAction; //이벤트 등록
}
}
#region 요소 추가
/// <summary>
/// 텍스트 레이블 추가
/// </summary>
/// <param name="key">레퍼런스를 위한 키</param>
/// <param name="isEnableWhenStart">다이얼로그박스가 활성화될때 같이 활성화되는가?</param>
/// <param name="text">입력할 텍스트</param>
/// <param name="fontSize">폰트 크기</param>
/// <param name="alignOption">텍스트 정렬 옵션</param>
/// <returns></returns>
public TextMeshProUGUI AddText(string key, bool isEnableWhenStart, string text, float fontSize, TextAlignmentOptions alignOption = TextAlignmentOptions.Left)
{
GameObject newText = Instantiate(mTextLabelPrefab, Vector3.zero, Quaternion.identity, mCenterContentsParent);
TextMeshProUGUI currentText = newText.GetComponent<TextMeshProUGUI>();
//텍스트 설정
currentText.text = text;
currentText.fontSize = fontSize / 1.45f; //1.45는 평균적인 텍스트의 크기보정
currentText.alignment = alignOption;
//텍스트의 크기를 현재 콘텐츠영역 크기에 맞춤
newText.GetComponent<RectTransform>().sizeDelta = new Vector2Int(mDialogBoxRootSize.x - BORDER_GAP, 0);
//딕셔너리에 삽입
AddInstantiatedObject(key, newText);
//텍스트 활성화
newText.SetActive(isEnableWhenStart);
return currentText;
}
/// <summary>
/// 버튼을 추가
/// </summary>
/// <param name="key">레퍼런스를 위한 키</param>
/// <param name="isEnableWhenStart">다이얼로그박스가 활성화될때 같이 활성화되는가?</param>
/// <param name="text">버튼에 쓰여질 텍스트</param>
/// <param name="eventID">버튼이 눌리면 호출될 이벤트 ID</param>
/// <param name="targetTransform">어떤 트랜스폼의 자식으로 위치할것인가? (null, 콘텐츠영역)</param>
/// <returns></returns>
public Button AddButton(string key, bool isEnableWhenStart, string text, string eventID, Transform targetTransform = null)
{
GameObject newButton = Instantiate(mButtonPrefab, Vector3.zero, Quaternion.identity, targetTransform == null ? mBottomContentsParent : targetTransform);
//컴포넌트 획득
Button currentButton = newButton.GetComponent<Button>();
newButton.GetComponentInChildren<TextMeshProUGUI>(true).text = text;
currentButton.onClick.AddListener(() => EventTrigger(eventID));
//딕셔너리에 삽입
AddInstantiatedObject(key, newButton);
//활성화
newButton.SetActive(isEnableWhenStart);
return currentButton;
}
/// <summary>
/// 요소들 간 간격을 추가
/// </summary>
/// <param name="key">레퍼런스를 위한 키</param>
/// <param name="isEnableWhenStart">다이얼로그박스가 활성화될때 같이 활성화되는가?</param>
/// <param name="height">높이</param>
/// <param name="isCenter">콘텐츠 영역에 추가하는가? (false 하단 영역)</param>
/// <returns></returns>
public GameObject AddBorder(string key, bool isEnableWhenStart, int height, bool isCenter)
{
GameObject newBorder = Instantiate(mBorderPrefab, Vector3.zero, Quaternion.identity, isCenter ? mCenterContentsParent : mBottomContentsParent);
Image borderImage = newBorder.GetComponent<Image>();
borderImage.sprite = Sprite.Create(new Texture2D(height, height, TextureFormat.RGB24, false), new Rect(0, 0, height, height), new Vector2(0f, 0f), 100.0f);
borderImage.type = Image.Type.Simple;
newBorder.SetActive(isEnableWhenStart);
//딕셔너리에 삽입
AddInstantiatedObject(key, newBorder);
return newBorder;
}
/// <summary>
/// 이미지를 추가
/// </summary>
/// <param name="key">레퍼런스를 위한 키</param>
/// <param name="isEnableWhenStart">다이얼로그박스가 활성화될때 같이 활성화되는가?</param>
/// <param name="spriteImage">인스턴스 할 스프라이트 이미지</param>
/// <param name="imageHeight">이미지의 높이</param>
/// <param name="alignOption">이미지의 위치 정렬 옵션</param>
/// <returns></returns>
public Image AddImage(string key, bool isEnableWhenStart, Sprite spriteImage, int imageHeight, Align alignOption)
{
GameObject newBorder = AddBorder(null, isEnableWhenStart, imageHeight, true); //이미지를 사용하기위해 보더 생성
//이미지 영역 생성
GameObject newImage = Instantiate(mBorderPrefab, Vector3.zero, Quaternion.identity, mCenterContentsParent);
newImage.transform.SetParent(newBorder.transform);
//컴포넌트 획득
Image currentImage = newImage.GetComponent<Image>();
RectTransform currentTransform = newImage.GetComponent<RectTransform>();
//가져올 이미지의 사이즈를 기반으로 동적으로 크기 보정
float sizeCorrectionDelta = spriteImage.rect.size.y / imageHeight;
currentTransform.sizeDelta = spriteImage.rect.size / sizeCorrectionDelta;
currentImage.sprite = spriteImage;
//이미지 위치 조정
switch (alignOption)
{
case Align.LEFT:
{
currentTransform.pivot = new Vector2(0, 0.5f);
currentTransform.localPosition = Vector3.zero;
break;
}
case Align.RIGHT:
{
currentTransform.pivot = new Vector2(1, 0.5f);
currentTransform.localPosition = new Vector2(mDialogBoxRootSize.x - BORDER_GAP, 0f);
break;
}
case Align.CENTER:
{
currentTransform.pivot = new Vector2(0.5f, 0.5f);
currentTransform.localPosition = new Vector2((mDialogBoxRootSize.x - BORDER_GAP) * 0.5f, 0f);
break;
}
case Align.EXPAND:
{
currentTransform.pivot = new Vector2(0.5f, 0.5f);
currentTransform.localPosition = new Vector2((mDialogBoxRootSize.x - BORDER_GAP) * 0.5f, 0f);
currentTransform.sizeDelta = new Vector2Int(mDialogBoxRootSize.x - BORDER_GAP, imageHeight);
break;
}
}
//딕셔너리에 삽입
AddInstantiatedObject(key, newImage);
//이미지 활성화
currentImage.color = Color.white;
newImage.SetActive(true);
return currentImage;
}
/// <summary>
/// 입력필드 추가
/// </summary>
/// <param name="key">레퍼런스를 위한 키</param>
/// <param name="isEnableWhenStart">다이얼로그박스가 활성화될때 같이 활성화되는가?</param>
/// <param name="widthPercentile">콘텐츠 영역에서 가로 길이의 비율 (0 ~ 1f)</param>
/// <param name="height">높이</param>
/// <param name="targetTransform">어떤 트랜스폼의 자식으로 위치할것인가? (null, 콘텐츠영역)</param>
/// <param name="contentType">입력필드의 콘텐츠 타입</param>
/// <param name="alignOption">입력필드 위치 정렬 옵션</param>
/// <returns></returns>
public TMP_InputField AddInputField(string key, bool isEnableWhenStart, float widthPercentile, int height, Transform targetTransform = null, TMP_InputField.ContentType contentType = TMP_InputField.ContentType.Standard, Align alignOption = Align.LEFT)
{
GameObject newInputField = Instantiate(mInputFieldPrefab, Vector3.zero, Quaternion.identity, targetTransform == null ? mCenterContentsParent : targetTransform);
//컴포넌트 획득
RectTransform currentTransform = newInputField.GetComponent<RectTransform>();
TMP_InputField currentInputField = newInputField.GetComponent<TMP_InputField>();
//사이즈 조절
widthPercentile = Mathf.Clamp(widthPercentile, 0f, 1f);
currentTransform.sizeDelta = new Vector2Int((int)((mDialogBoxRootSize.x - BORDER_GAP) * widthPercentile), height);
//입력필드 위치 조정
switch (alignOption)
{
case Align.LEFT:
{
currentTransform.pivot = new Vector2(0, 0.5f);
currentTransform.localPosition = Vector3.zero;
break;
}
case Align.RIGHT:
{
GameObject newBorder = AddBorder(null, true, height, true);
currentTransform.SetParent(newBorder.transform);
currentTransform.pivot = new Vector2(1, 0.5f);
currentTransform.localPosition = new Vector2(mDialogBoxRootSize.x - BORDER_GAP, 0f);
break;
}
case Align.CENTER:
{
GameObject newBorder = AddBorder(null, true, height, true);
currentTransform.SetParent(newBorder.transform);
currentTransform.pivot = new Vector2(0.5f, 0.5f);
currentTransform.localPosition = new Vector2((mDialogBoxRootSize.x - BORDER_GAP) * 0.5f, 0f);
break;
}
case Align.EXPAND:
{
currentTransform.pivot = new Vector2(0.5f, 0.5f);
currentTransform.localPosition = new Vector2((mDialogBoxRootSize.x - BORDER_GAP) * 0.5f, 0f);
currentTransform.sizeDelta = new Vector2Int(mDialogBoxRootSize.x - BORDER_GAP, height);
break;
}
}
//입력필드 옵션 설정
currentInputField.contentType = contentType;
//딕셔너리에 삽입
AddInstantiatedObject(key, newInputField);
//입력필드 활성화
newInputField.SetActive(isEnableWhenStart);
return currentInputField;
}
/// <summary>
/// 콘텐츠 영역에 가로레이아웃을 추가
/// </summary>
/// <param name="key">레퍼런스를 위한 키</param>
/// <param name="isEnableWhenStart">다이얼로그박스가 활성화될때 같이 활성화되는가?</param>
/// <param name="height">높이</param>
/// <param name="spacing">레이아웃 내부에서 요소들 간 간격</param>
/// <param name="isAutoAlign">자동으로 크기를 정렬하는가?</param>
/// <returns></returns>
public Transform AddHorizontalLayout(string key, bool isEnableWhenStart, int height, int spacing, bool isAutoAlign)
{
GameObject newHorizontalLayoutBorder = AddBorder(null, isEnableWhenStart, height, true);
//컴포넌트 획득
HorizontalLayoutGroup currentLayout = newHorizontalLayoutBorder.AddComponent<HorizontalLayoutGroup>();
//레이아웃 설정
currentLayout.childControlWidth = isAutoAlign; //내부 요소들을 자동으로 정렬하는가?
currentLayout.spacing = spacing; //간격 설정
newHorizontalLayoutBorder.GetComponent<RectTransform>().sizeDelta = new Vector2Int(mDialogBoxRootSize.x - BORDER_GAP, height); //크기 설정
//딕셔너리에 삽입
AddInstantiatedObject(key, newHorizontalLayoutBorder);
return newHorizontalLayoutBorder.transform; //트랜스폼을 리턴
}
/// <summary>
/// 프리팹으로 미리 지정해둔 UI요소를 다이얼로그박스에 동적으로 추가
/// </summary>
/// <param name="key">레퍼런스를 위한 키</param>
/// <param name="isEnableWhenStart">다이얼로그박스가 활성화될때 같이 활성화되는가?</param>
/// <param name="prefab">인스턴스 할 프리팹</param>
/// <param name="isDeliverEvent">프리팹에서 호출하는 이벤트를 받을것인가?</param>
/// <returns></returns>
public DialogBoxPrefabDeliver AddExistsPrefab(string key, bool isEnableWhenStart, DialogBoxPrefabDeliver prefab, bool isDeliverEvent)
{
//이벤트 리시버 설정
if (isDeliverEvent)
{
prefab.EventReciever = this;
}
//컴포넌트 획득
RectTransform currentRectTransform = prefab.GetComponent<RectTransform>();
//크기 조정수치 구하기
float scaleCorrection = mDialogBoxRootSize.x / currentRectTransform.sizeDelta.x;
//프리팹의 크기를 기반으로 보더 생성
GameObject newBorder = AddBorder(null, isEnableWhenStart, (int)(currentRectTransform.sizeDelta.y * scaleCorrection), true);
//프리팹 위치 및 크기 조절
RectTransform newPrefab = Instantiate(prefab, Vector3.zero, Quaternion.identity, newBorder.transform).GetComponent<RectTransform>();
newPrefab.localScale = Vector3.one * scaleCorrection;
newPrefab.localPosition = new Vector3((mDialogBoxRootSize.x * 0.5f) - BORDER_GAP * 0.5f, 0f);
//프리팹 활성화
prefab.gameObject.SetActive(true);
//활성화 여부에 따른 부모오브젝트인 보더 활성화
newBorder.SetActive(isEnableWhenStart);
//딕셔너리에 삽입
AddInstantiatedObject(key, newPrefab.gameObject);
return prefab;
}
#endregion
#region 유틸리티
/// <summary>
/// 박스를 활성화/비활성화
/// </summary>
/// <param name="isEnable">활성화 할것인가?</param>
public void ToggleDialogBox(bool isEnable)
{
gameObject.SetActive(isEnable);
}
/// <summary>
/// 박스를 파괴
/// </summary>
public void DestroyBox()
{
foreach (KeyValuePair<string, DialogBoxController> caller in mDestroyCallEvents)
{
caller.Value.EventTrigger(caller.Key);
}
Destroy(gameObject);
}
/// <summary>
/// 다이얼로그박스에서 레퍼런스할 UI요소를 획득
/// </summary>
/// <param name="key">레퍼런스의 키</param>
/// <typeparam name="T">어떤 타입을 획득할것인가?</typeparam>
/// <returns></returns>
public T GetInstantiatedObject<T>(string key)
{
return mInstantiatedObjects[key].GetComponent<T>();
}
/// <summary>
/// 입력필드에서 발생하는 이벤트를 추가
/// </summary>
/// <param name="inputField">이벤트를 호출할 입력필드</param>
/// <param name="eventID">호출할 이벤트 ID</param>
/// <param name="eventType">이벤트 타입</param>
public void AddInputFieldEvent(TMP_InputField inputField, string eventID, InputFieldEvent eventType)
{
string argument = null;
switch (eventType)
{
case InputFieldEvent.OnDeselct:
inputField.onDeselect.AddListener((argument) => EventTrigger(eventID));
break;
case InputFieldEvent.OnEndEdit:
inputField.onEndEdit.AddListener((argument) => EventTrigger(eventID));
break;
case InputFieldEvent.OnSelect:
inputField.onSelect.AddListener((argument) => EventTrigger(eventID));
break;
case InputFieldEvent.OnValueChanged:
inputField.onValueChanged.AddListener((argument) => EventTrigger(eventID));
break;
}
}
/// <summary>
/// 다이얼로그박스에서 다른 다이얼로그박스를 레퍼런스 할 경우 딕셔너리에 추가
/// </summary>
/// <param name="dialogBoxKey">추가할 다이얼로그박스의 키</param>
/// <param name="dialogBox">추가할 다이얼로그박스</param>
public void AddReferenceDialogBox(string dialogBoxKey, DialogBoxController dialogBox)
{
mReferenceDialogBoxes.Add(dialogBoxKey, dialogBox);
}
/// <summary>
/// 다이얼로그박스에서 다른 다이얼로그박스를 획득
/// </summary>
/// <param name="dialogBoxKey">지정한 다이얼로그박스의 키</param>
/// <returns></returns>
public DialogBoxController GetReferenceDialogBox(string dialogBoxKey)
{
return mReferenceDialogBoxes[dialogBoxKey];
}
/// <summary>
/// 파괴시 호출할 이벤트 리스너를 등록 'RESERVED_EVENT_ON_DESTROY'가 호출
/// </summary>
/// <param name="eventReciever">누가 이벤트를 호출할것인가? (null: 자기자신)</param>
public void AddDestroyListener(DialogBoxController eventReciever = null)
{
mDestroyCallEvents.Add(RESERVED_EVENT_ON_DESTROY, eventReciever == null ? this : eventReciever);
}
/// <summary>
/// 상단 영역의 높이를 조절
/// </summary>
/// <param name="height">설정할 높이</param>
public void SetTopBoxHeight(int height)
{
//크기 설정
mTopContentsParent.sizeDelta = new Vector2(0, height);
//Height가 0보다 크면(존재하는경우) 활성화
mTopContentsParent.gameObject.SetActive(height > 0);
//글자 크기는 높이의 75%
mTitleLabel.fontSizeMax = height * 0.75f;
//박스 크기 조절
RefreshBoxSize();
}
/// <summary>
/// 상단 영역의 텍스트를 설정
/// </summary>
/// <param name="text">텍스트</param>
/// <param name="alignOption">정렬 옵션</param>
public void SetTitleBox(string text, TextAlignmentOptions alignOption = TextAlignmentOptions.Center)
{
TextMeshProUGUI titleLabel = mTitleLabel.GetComponent<TextMeshProUGUI>();
titleLabel.text = text;
titleLabel.alignment = alignOption;
}
/// <summary>
/// 하단 영역의 높이를 조절
/// </summary>
/// <param name="height">설정할 높이</param>
public void SetBottomBoxHeight(int height)
{
mBottomContentsParent.sizeDelta = new Vector2(0, height);
mBottomContentsParent.gameObject.SetActive(height > 0);
RefreshBoxSize();
}
#endregion
#region 내부 클래스
/// <summary>
/// 동적으로 생성한 인스턴스를 딕셔너리에 삽입
/// </summary>
private void AddInstantiatedObject(string key, GameObject obj)
{
if (key == null) { return; }
mInstantiatedObjects.Add(key, obj);
}
/// <summary>
/// 콘텐츠 박스 사이즈를 조절
/// </summary>
private void RefreshBoxSize()
{
mContentsScrollViewRoot.sizeDelta = new Vector2(0, mDialogBoxRootSize.y - mTopContentsParent.sizeDelta.y - mBottomContentsParent.sizeDelta.y);
mContentsScrollViewRoot.anchorMax = new Vector2(1.0f, 0.5f);
mContentsScrollViewRoot.localPosition = new Vector3(-mDialogBoxRootSize.x * 0.5f, (mBottomContentsParent.sizeDelta.y - mTopContentsParent.sizeDelta.y) * 0.5f, 0f);
}
#endregion
#region UI 이벤트 트리거
/// <summary>
/// 동적으로 생성한 UI요소들에서 이벤트를 받음
/// </summary>
/// <param name="eventID"></param>
public void EventTrigger(string eventID)
{
switch (eventID) //호출한 이벤트가 예약되어있는 이벤트인가?
{
case RESERVED_EVENT_CLOSE: //단순히 닫는 다이얼로그박스인경우
{
DestroyBox();
break;
}
}
mEventAction.Invoke(this, eventID); //이벤트 호출
}
#endregion
}
}
더보기
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TMPro;
using DialogBox;
public class DialogBoxGenerator : Singleton<DialogBoxGenerator>
{
[Header("다이얼로그 박스 프리맵의 최상위 부모")]
[SerializeField] private GameObject mDialogBoxPrefab;
/// <summary>
/// 비어있는 다이얼로그박스를 생성
/// </summary>
/// <returns></returns>
public DialogBoxController CreateEmptyDialogBox()
{
DialogBoxController controller = Instantiate(mDialogBoxPrefab, Vector3.zero, Quaternion.identity).GetComponent<DialogBoxController>();
return controller;
}
/// <summary>
/// 단순한 다이얼로그박스를 생성
/// </summary>
/// <param name="title">타이틀 (상단 제목)</param>
/// <param name="context">콘텐츠 (콘텐츠 글 내용)</param>
/// <param name="buttonText">버튼 (하단 버튼 내용)</param>
/// <param name="eventAction">이벤트 대리자</param>
/// <param name="buttonEventID">기본 생성되는 버튼이 호출할 이벤트 (null: 단순 종료)</param>
/// <param name="width">가로길이</param>
/// <param name="height">세로길이</param>
/// <param name="titleHeight">상단 영역의 높이</param>
/// <param name="buttonHeight">하단 영역의 높이</param>
/// <returns></returns>
public DialogBoxController CreateSimpleDialogBox(string title, string context, string buttonText, System.Action<DialogBoxController, string> eventAction = null, string buttonEventID = DialogBoxController.RESERVED_EVENT_CLOSE, int width = 200, int height = 150, int titleHeight = 30, int buttonHeight = 30)
{
//다이얼로그박스 생성
DialogBoxController controller = CreateEmptyDialogBox();
//이벤트 호출 환경에서 대리자가 없는경우?
if(buttonEventID != DialogBoxController.RESERVED_EVENT_CLOSE && eventAction == null)
{
Debug.LogWarningFormat("{0}has no reciever!", gameObject.name);
}
//다이얼로그박스 크기 설정
controller.InitDialogBox(width, height, eventAction);
//사이즈 조절
controller.SetTopBoxHeight(titleHeight);
controller.SetBottomBoxHeight(buttonHeight);
//타이틀
controller.SetTitleBox(title);
//콘텍스트 텍스트 생성
controller.AddText(null, true, context, 20, TextAlignmentOptions.Center);
//나가기 버튼 생성
controller.AddButton(null, true, buttonText, buttonEventID);
//생성한 컨트롤러 리턴
return controller;
}
}
더보기
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace DialogBox
{
[System.Serializable]
public struct UiElement
{
public string key;
public GameObject go;
}
public class DialogBoxPrefabDeliver : MonoBehaviour
{
[Header("전달할 엘리먼트들")]
[SerializeField] private UiElement[] mElements; //런타임 도중 사용할 엘리먼트
private Dictionary<string, GameObject> mElementsDictionary = new Dictionary<string, GameObject>(); //런타임 도중 찾을 요소를 저장한 딕셔너리
/// <summary>
/// 전달자가 이벤트를 전달할 리시버, null일경우 이벤트를 전달하지 않음
/// </summary>
[HideInInspector] public DialogBoxController EventReciever = null; //Deliver 프리팹에서 호출되는 이벤트를 호출할 다이얼로그박스 컨트롤러
private void Awake()
{
//인스펙터에서 설정한 요소들을 집어넣는다.
foreach (UiElement element in mElements) { mElementsDictionary.Add(element.key, element.go); }
}
public T GetElement<T>(string key) { return mElementsDictionary[key].GetComponent<T>(); }
public void Event_Deliver(string eventID)
{
if (EventReciever != null) { EventReciever.EventTrigger(eventID); }
}
}
}
💡 설명
> DialogBoxController
public const string RESERVED_EVENT_CLOSE = "RESERVED_EVENT_CLOSE";
public const string RESERVED_EVENT_ON_DESTROY = "RESERVED_EVENT_ON_DESTROY";
- 이벤트를 호출할 때 이벤트 이름을 예약한 상수 문자열입니다.
private Action<DialogBoxController, string> mEventAction; //이 다이얼로그박스가 호출할 이벤트
- 다이얼로그박스의 이벤트를 호출하기 위한 액션 함수입니다.
private Dictionary<string, DialogBoxController> mDestroyCallEvents = new Dictionary<string, DialogBoxController>(); //다이얼로그박스가 파괴될때 호출할 이벤트
- 다이얼로그박스가 파괴될 때 대상 다이얼로그박스에 이벤트를 호출하도록 합니다.
- 이곳에 있는 다이얼로그박스에 string값인 eventID가 호출되도록 합니다.
private Dictionary<string, DialogBoxController> mReferenceDialogBoxes = new Dictionary<string, DialogBoxController>(); //참조할 다이얼로그 박스
- 다이얼로그박스에서 또 다른 다이얼로그박스를 생성할 수 있습니다.
- 생성된 다이얼로그박스에서 생성시킨 부모 다이얼로그박스에 접근하여 값을 변경할 수 있기에 참조할 수 있도록 합니다.
private Dictionary<string, GameObject> mInstantiatedObjects = new Dictionary<string, GameObject>(); //인스턴스된 입력필드들
- 다이얼로그박스에서 생성한 여러 요소들(UI요소)에 접근하기 위해 사용합니다.
- 예시로, 입력필드에 어떤 값이 들어있는지 확인하거나 토글이 켜져 있는지 확인하는 등. 자유로운 구축이 가능해집니다.
public void InitDialogBox(int boxWidth, int boxHeight, Action<DialogBoxController, string> eventAction = null)
- 다이얼로그박스를 초기화합니다.
- 박스의 크기와 박스에 포함되어 있는 UI요소들이 이벤트를 호출할 경우, 해당 이벤트를 처리할 액션함수를 포함합니다.
public TextMeshProUGUI AddText(string key, bool isEnableWhenStart, string text, float fontSize, TextAlignmentOptions alignOption = TextAlignmentOptions.Left)
- 다이얼로그박스에 텍스트를 추가합니다.
- 콘텐츠 영역에 텍스트가 추가됩니다.
public Button AddButton(string key, bool isEnableWhenStart, string text, string eventID, Transform targetTransform = null)
- 다이얼로그박스에 버튼을 추가합니다.
- 버튼을 추가할 때 eventID를 등록하면 버튼을 누를 때 action함수에 eventID로 호출하게 됩니다.
public GameObject AddBorder(string key, bool isEnableWhenStart, int height, bool isCenter)
- 다이얼로그박스에 간격을 추가합니다.
- isCenter가 true이면 콘텐츠 영역에, false이면 하단영역에 추가됩니다.
public Image AddImage(string key, bool isEnableWhenStart, Sprite spriteImage, int imageHeight, Align alignOption)
- 다이얼로그박스에 이미지를 추가합니다.
- 이미지는 콘텐츠 영역에 추가되며, alignOption을 통해 정렬할 수 있습니다.
public TMP_InputField AddInputField(string key, bool isEnableWhenStart, float widthPercentile, int height, Transform targetTransform = null, TMP_InputField.ContentType contentType = TMP_InputField.ContentType.Standard, Align alignOption = Align.LEFT)
- 다이얼로그박스에 입력필드를 추가합니다.
- 입력필드는 콘텐츠 영역에 추가되거나 특정 트랜스폼의 자식(targetTransform)으로 추가될 수 있습니다.
- contentType을 통해 입력필드의 속성을 설정할 수 있습니다 (예, 비밀번호를 위한 Passowrd 옵션)
public Transform AddHorizontalLayout(string key, bool isEnableWhenStart, int height, int spacing, bool isAutoAlign)
- 콘텐츠 영역 내에 가로 레이아웃을 추가합니다.
- 가로 레이아웃을 트랜스폼을 리턴하여 다른 요소를 추가할 때 targetTransform을 통해 해당 영역에 추가할 수 있습니다.
public DialogBoxPrefabDeliver AddExistsPrefab(string key, bool isEnableWhenStart, DialogBoxPrefabDeliver prefab, bool isDeliverEvent)
- 프리팹 형태로 기존에 존재하는 UI를 다이얼로그박스에 추가합니다.
- 크기는 기존 프리팹 형태의 크기에서 비율을 조절하여 다이얼로그박스 가로길이에 동적으로 크기가 조절되어 들어갑니다.
public void DestroyBox()
- 박스를 파괴합니다.
- 박스가 파괴될 때 mDestroyCallEvents에 들어있는 값들을 모두 찾아 이벤트를 호출하게 됩니다.
public void AddInputFieldEvent(TMP_InputField inputField, string eventID, InputFieldEvent eventType)
- 입력필드에 접근하여 해당 입력필드에 이벤트를 추가할 수 있습니다.
- 이벤트는 네 가지(선택해제, 수정완료, 선택, 수정)로 호출할 수 있으며 해당 이벤트가 발생할 때마다 action에 eventID로 호출되게 됩니다.
> DialogBoxGenerator
[SerializeField] private GameObject mDialogBoxPrefab;
- mDialogBoxPrefab을 생성하여 관리하게 됩니다.
- 해당 프리팹에 사전 설정된 UI요소들이 있습니다.
public DialogBoxController CreateEmptyDialogBox()
- 단순히 비어있는 다이얼로그박스를 생성합니다.
- 모든 요소를 직접 추가하여 사용해야 합니다.
public DialogBoxController CreateSimpleDialogBox(string title, string context, string buttonText, System.Action<DialogBoxController, string> eventAction = null, string buttonEventID = DialogBoxController.RESERVED_EVENT_CLOSE, int width = 200, int height = 150, int titleHeight = 30, int buttonHeight = 30)
- 단순한 다이얼로그박스를 생성합니다.
- 타이틀, 콘텐츠(텍스트 내용), 버튼 세트로 구성되어 생성됩니다.
- CreateEmptyDialogBox()처럼 다른 요소를 추가할 수 있습니다.
>DialogBoxPrefabDeliver
[SerializeField] private UiElement[] mElements; //런타임 도중 사용할 엘리먼트
- 미리 생성한 UI요소를 해당 컴포넌트에 포함시켜 런타임 도중 스크립트에서 접근할 수 있게 합니다.
- UiElement는 {key(string}, go{GameObject)}로 되어있어 key로 해당 go에 접근할 수 있도록 합니다.
public void Event_Deliver(string eventID)
- 미리 지정한 프리팹에서 이벤트를 호출해야 할 때 다이얼로그에 이벤트를 호출하고 싶다면, UI에서 이 이벤트를 호출하고 eventID를 지정하여 이벤트를 전달할 수 있습니다.
💻 에디터 구현
- 미리 만들어놓은 프리팹에 Dialog Box Controller 컴포넌트를 넣고 포함시킵니다.
- 해당 프리팹 및 전체 스크립트가 포함된 프로젝트는 [깃허브]에 있습니다.
- 해당 프리팹은 캔버스이며, 캔버스 스케일러 설정은 다음과 같습니다.
- 다이얼로그박스의 크기를 설정할 때 가로, 세로길이는 최대 800, 600입니다.
🔎 사용(Sample)
- 깃허브 프로젝트에 제공되는 샘플프로젝트입니다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using DialogBox;
using UnityEngine.UI;
using TMPro;
public class DialogBox_SampleScript : MonoBehaviour
{
[SerializeField] private DialogBoxPrefabDeliver mTogglesSamplePrefab; //토글 샘플 프리팹
private void Start()
{
//간단한 다이얼로그박스 생성
DialogBoxController controller = DialogBoxGenerator.Instance.CreateSimpleDialogBox("알림창", "알림사항", "확인", EVENT_DialogBox, "BTN_Confirm");
//다이얼로그박스에 요소 추가
controller.AddBorder(null, true, 10, true);
controller.AddText(null, true, "확인 버튼을 눌러 \n창을 끌 수 있습니다.", 20, TextAlignmentOptions.CenterGeoAligned);
controller.AddBorder(null, true, 10, true);
controller.AddText(null, true, "단, 토글을 한개 이상 켜세요.", 20, TextAlignmentOptions.CenterGeoAligned);
//프리팹을 추가
controller.AddExistsPrefab("TogglePrefab", true, mTogglesSamplePrefab, true);
//파괴시 이벤트 호출(자기자신)
controller.AddDestroyListener();
}
/// <summary>
/// 다이얼로그박스에서 호출되는 이벤트를 수행하는 대리자
/// </summary>
private void EVENT_DialogBox(DialogBoxController controller, string eventID)
{
switch (eventID)
{
case "BTN_Confirm": //확인 버튼을 누른경우
{
bool isToggle1On = controller.GetInstantiatedObject<DialogBoxPrefabDeliver>("TogglePrefab").GetElement<Toggle>("Toggle1").isOn;
bool isToggle2On = controller.GetInstantiatedObject<DialogBoxPrefabDeliver>("TogglePrefab").GetElement<Toggle>("Toggle2").isOn;
bool isToggle3On = controller.GetInstantiatedObject<DialogBoxPrefabDeliver>("TogglePrefab").GetElement<Toggle>("Toggle3").isOn;
if(isToggle1On || isToggle2On || isToggle3On)
{
controller.DestroyBox();
}
else
{
DialogBoxGenerator.Instance.CreateSimpleDialogBox("", "토글을 최소 한개 이상 켜야합니다.", "확인", null, DialogBoxController.RESERVED_EVENT_CLOSE, 150, 100, 0, 30);
}
break;
}
case "Toggle1":
case "Toggle2":
case "Toggle3":
DialogBoxGenerator.Instance.CreateSimpleDialogBox("", eventID + "눌림", "확인", null, DialogBoxController.RESERVED_EVENT_CLOSE, 150, 100, 0, 30);
controller.GetInstantiatedObject<DialogBoxPrefabDeliver>("TogglePrefab").GetElement<Toggle>("Toggle1").SetIsOnWithoutNotify(Random.value > 0.5f);
controller.GetInstantiatedObject<DialogBoxPrefabDeliver>("TogglePrefab").GetElement<Toggle>("Toggle2").SetIsOnWithoutNotify(Random.value > 0.5f);
controller.GetInstantiatedObject<DialogBoxPrefabDeliver>("TogglePrefab").GetElement<Toggle>("Toggle3").SetIsOnWithoutNotify(Random.value > 0.5f);
break;
case DialogBoxController.RESERVED_EVENT_ON_DESTROY:
{
DialogBoxGenerator.Instance.CreateSimpleDialogBox("", "박스파괴", "종료", null, DialogBoxController.RESERVED_EVENT_CLOSE, 150, 100, 0, 30);
break;
}
}
}
}
- 다이얼로그박스 안에 텍스트를 넣고, 토글이 세 개가 있는 프리팹을 넣어서 테스트합니다.
- 토글이 하나라도 눌리면 토글 모두가 값이 무작위로 바뀝니다.
- 확인 버튼을 누를 때 토글이 하나도 켜져있지 않으면 창을 끌 수 없습니다.
- 확인 버튼을 눌러 창이 종료되거나 종료되지 못하는 경우 다른 창이 떠 현 상황을 알려줍니다.
'unity game modules' 카테고리의 다른 글
[유니티] 구글 스프레드 시트(엑셀) 연동 2 - 유니티 연결 (0) | 2023.03.02 |
---|---|
[유니티] 구글 스프레드 시트(엑셀) 연동 1 - 가입 (1) | 2023.03.02 |
[유니티] 특정 오브젝트 바라보게 하기 - Animation Rigging (0) | 2023.02.07 |
[유니티] 인벤토리 시스템(번외) - 장비 아이템 착용 (1) | 2023.02.06 |
[유니티] 인벤토리 시스템(번외) - 아이템 툴팁 (0) | 2023.02.06 |