티스토리 뷰
0. 코드전문
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using DG.Tweening;
[RequireComponent(typeof(Canvas))]
public class InfoUIController : MonoBehaviour
{
readonly Queue<Image> infoUIs = new Queue<Image>();
public Image prefab;
Image currentInfo = null;
[SerializeField, Header("시작 위치")]
RectTransform startPosition;
[SerializeField, Header("목표 위치")]
RectTransform targetPosition;
[SerializeField, Header("퇴장 위치")]
RectTransform exitPosition;
public float popupDuration = 0.5f;
public float exitDuration = 0.5f;
const Ease ease = Ease.OutCirc;
public void PopUp(Sprite sprt)
{
if (currentInfo != null)
{
Enqueue(currentInfo);
}
if (infoUIs.Count.Equals(0))
{
currentInfo = Instantiate(prefab, this.transform);
}
else
{
currentInfo = infoUIs.Dequeue();
}
currentInfo.sprite = sprt;
currentInfo.color = startColor;
currentInfo.rectTransform.anchoredPosition = startPosition.anchoredPosition;
currentInfo.gameObject.SetActive(true);
currentInfo.rectTransform.DOAnchorPos(targetPosition.anchoredPosition, popupDuration).SetEase(ease);
currentInfo.DOFade(1, popupDuration).SetEase(ease);
}
Color startColor = new Color(1,1,1,0);
public void PopUp()
{
if (currentInfo != null)
{
Enqueue(currentInfo);
}
if (infoUIs.Count.Equals(0))
{
currentInfo = Instantiate(prefab, this.transform);
}
else
{
currentInfo = infoUIs.Dequeue();
}
currentInfo.color = startColor;
currentInfo.rectTransform.anchoredPosition = startPosition.anchoredPosition;
currentInfo.gameObject.SetActive(true);
currentInfo.rectTransform.DOAnchorPos(targetPosition.anchoredPosition, popupDuration).SetEase(ease);
currentInfo.DOFade(1, popupDuration).SetEase(ease);
}
public void Enqueue(Image currentInfo)
{
currentInfo.rectTransform.DOKill();
currentInfo.DOKill();
currentInfo.rectTransform.DOAnchorPos(exitPosition.anchoredPosition, exitDuration).SetEase(ease);
currentInfo.DOFade(0, exitDuration).SetEase(ease).
OnComplete(() =>
{
infoUIs.Enqueue(currentInfo);
currentInfo.gameObject.SetActive(false);
});
}
}
1. 상세 정보 UI
게임에서 캐릭터나 아이템, 혹은 UI 위에 마우스를 올려두면 나타나는 정보 UI가 있습니다.
위의 유희왕의 경우는 화면 왼쪽에 선택한 카드의 확대 이미지라던가 상세 정보를 띄워주고 있습니다.
여기에 DOTween을 사용해서 약간의 모션을 줄 수 있을 것입니다.
작업 중인 카드게임에서는
앞면 상태의 카드 위에 마우스를 올려만 두면
화면 왼쪽에 확대한 카드 이미지를 보여주기로 했습니다.
(간단한 테스트를 위해 아무 이미지나 사용하였습니다...)
2. 포인터 이벤트
기능 테스트를 위해 Event Trigger 컴포넌트로 카드 위에 마우스를 올렸을 때 UI를 팝업 하는 메서드를 실행합니다.
Event Trigger와 Event System에 대해 잠시 정리하려고 합니다.
학원에서 많은 사람들이 자주 까먹거나 실수하는 부분에 대한 것입니다.
화면에서 일어나는 마우스 포인터에 대한 이벤트가 성립하기 위해선 반드시 세 가지가 만족해야합니다.
- 마우스 좌표를 기반으로 화면상의 오브젝트 객체들을 감지하는 Raycaster
- Canvas와 마우스 포인터에 검출된 객체 정보를 담고 있는 EventSystem
- 검출하고자 하는 오브젝트의 Collider
만약 마우스 포인터에 대한 이벤트나 콜백 메서드를 사용하려는데 안 된다면?
먼저 이 세 가지를 꼭 확인합니다.
레이캐스터는 세 종류가 있습니다.
Graphic Raycaster는 생성한 Canvas에 기본적으로 추가되어 있습니다.
이것은 Canvas 하위의 그래픽 영역을 가진 모든 UI 객체를 화면상에서 감지하는 것입니다.
Physics Raycaster은 2D과 3D 두 종류가 있는데,
이름에서 알 수 있듯이 2D Collider와 3D Collider를 감지하는 것입니다.
결국엔 Raycaster가 있어야 Collider를 가진 객체를 마우스로 검출할 수 있습니다!
그다음은 Event System 객체입니다.
캔버스 생성할 때면 자동으로 따라서 생성되는 그 객체입니다.
이 객체가 없으면 포인터에 대한 모든 이벤트들이 실행 안됩니다.
비활성화되어 있어도 안됩니다.
반대로 말하면 비활성화하면 마우스 포인터에 대한 이벤트를 막을 수 있다는 것입니다(?).
그리고 마우스로 검출하려는 오브젝트에 Raycaster와 같은 타입의 콜라이더를 추가합니다.
2D Raycaster는 당연히 Collider 2D 객체를 검출할 것입니다.
주변사람들이 안 된다고 멘탈 터졌을 때 가서 보면 열에 아홉은 이 세 가지 중의 하나였습니다.
물론 좀 더 응용 단계로 가면 Layer나 Raycast Ignore 등의 문제가 있지만,
기본 조건은 갖추고서 작업을 해야 한다고 생각합니다...
어려운 건 절대 아니고, 주변에서 너무 자주 보는 실수들이어서 정리해 보았습니다.
아무튼 마우스를 올려두면
UI가 화면 밖에서 화면 안으로 들어오는 간단한 모션을 만들었지만,
나름 고민한 부분이 바로 오브젝트 풀링이었습니다.
2. 오브젝트 풀링
반복적이고 다발적으로 일어나는 오브젝트의 생성은 게임에서 흔히 일어나는 일 중 하나입니다.
오브젝트의 제거 또한 마찬가지입니다.
그런데 유니티의 생성과 제거는 오버헤드를 야기하는 작업으로 많이들 얘기합니다.
그래서 오브젝트 풀링이라는 개념이 생겨납니다.
오브젝트를 반복적으로 생성과 제거하는 대신,
활성화와 비활성화하자는 것입니다.
Instantiate와 Destroy보다 SetActive(true)와 SetActive(false)가
성능에 더 적은 부하를 주기 때문입니다.
오브젝트 풀링의 요점은 게임을 플레이하는 중에는 Instantiate를 하지 않을 것과,
Destroy를 해야 할 때, 그 오브젝트를 다시 쓸 수 있는 상황이라면 대신에 SetActive(false)를 하는 것입니다.
이렇게 오브젝트 풀링은 게임 최적화의 기본기 중의 기본기가 되었습니다.
유니티에서는 최근 버전부터 아예 오브젝트 풀링을 위한 기능을 지원해 준다고 하는데
차차 알아봐야겠습니다.
여기서 풀(Pool)은 반복적으로 사용하려는 오브젝트를 담아두는 공간을 뜻하는데,
수영장을 이르는 풀과 같은 의미입니다.
그러한 넉넉한 공간에 오브젝트를 잔뜩 담아두고 쓰겠다는 것입니다.
이하는 진정 최적화를 생각하여 작성했다기보단,
순전히 제 취향으로 작성한 코드입니다.
readonly Queue<Image> infoUIs = new Queue<Image>();
먼저 풀을 만듭니다.
UI 오브젝트이므로 Image 타입 Queue를 생성했습니다.
DOTween을 사용할 건데,
Image 타입이므로 사용할 수 있는 건,
Screen 상의 위치에 변화를 주는 것과 Rotation, 그리고 Scale이 있으며
Color 값에도 트윈을 줄 수 있습니다.
트윈이란, 시작 값에서 목푯 값으로 서서히 값을 변화시키는 것입니다.
제가 사용한 트윈은 위치와 투명도입니다.
시작 값과 끝 값을 참조할 변수도 만들었습니다.
오브젝트의 생성이 필요한 경우 사용할 메서드를 만들었습니다.
물론 생성하는 대신 풀에 저장한 오브젝트를 꺼내 사용합니다.
그런데 이 부분은 문제가 있을 수 있습니다.
왜냐하면 아까 제가 적은 말을 위배하기 때문입니다.
일반적으로 게임이 시작한 시점에는
게임 플레이 동안 사용할 오브젝트를 미리, 잔뜩 만들어 둡니다.
하지만 제 코드는 게임 시작 시 풀에 아무것도 추가하지 않고서,
필요한 때가 됐을 때 풀을 확인하여 오브젝트가 없는 경우
그제야 오브젝트를 생성합니다.
개발 초반에는 이 오브젝트가 몇 개나 필요한지 모르는 경우가 있습니다.
때문에 생성할 오브젝트 수가 몇 개인지 파악하기 위해 동적 생성을 할 수도 있다고 합니다.
게임에 필요한 객체 수를 파악하고 나면
게임 초기화 단계에서 해당 수만큼 미리 오브젝트를 생성해 두고
이후의 추가적인 생성은 제한하도록 합니다.
일반적으로는 말입니다.
3. Enqueue
SetActive(false)를 해서 비활성화하는 것을 잊으면 안 됩니다.
그리고 사용을 마친 오브젝트는 다시 풀에 넣어야 합니다.
이때 사용할 수 있는 것이 옵저버 패턴일 수 있습니다.
아니면 더 간단한 방법으로는 콜백 메서드를 활용하는 것입니다.
예로 들기 좋은 것이 파티클입니다.
파티클은 풀링 하기 딱 좋은 오브젝트인데,
유니티의 파티클 시스템은 재생을 마쳤을 때
오브젝트의 처리를 결정할 수 있습니다.
Destroy 해서 파티클을 제거하거나
Disable 하여 비활성화만 할 수 있습니다.
오브젝트 풀링은 객체를 비활성화하여 재사용하는 것이 목적이기에
파티클 시스템의 Stop Action은 Disable로 설정하는 것이 적절합니다.
여기에 MonoBehaviour의 OnDisable 콜백 메서드를 사용할 수 있습니다.
OnDisable은 객체가 비활성화되었을 때 호출되는 메서드입니다.
이 내부에 오브젝트를 풀에 다시 넣은 작업을 하면 적절할 것입니다.
하지만, 이 방법은 오브젝트마다 해당 코드를 가진 스크립트가 추가되어있을 때의 얘기입니다.
제가 사용할 오브젝트는 그저 Image 객체입니다.
중요한 건, 오브젝트 사용을 마쳤을 때 실행되는 메서드에
오브젝트를 풀에 다시 넣는 코드를 추가한다는 것입니다.
그것이 콜백 메서드일 수도 있고 별도의 실행 타이밍을 가진 메서드일 수 있겠습니다.
저는 오브젝트를 풀에서 꺼내는 작업 직전에,
이미 꺼낸 오브젝트가 있다면 그것을 다시 풀에 넣기로 하였습니다.
한 번 확대한 카드 이미지가 나타나면,
다른 카드 위에 마우스를 올리지 않는 이상
카드 이미지는 사라지지 않습니다.
다른 카드 위에 마우스를 올리고 나서야
나타났던 카드 이미지가 화면 밖으로 사라집니다.
이제야 말하지만,
저 currentInfo라는 변수는 현재 화면에 나타난 이미지 객체를 가리키는 것입니다.
아무튼 풀에서 꺼내진 이미지 객체는 몇 개의 DOTween 애니메이션을 달아두고 활성화시켜 사용합니다.
그와 반대로 사용을 마친 오브젝트에는 퇴장에 알맞은 트윈 애니메이션을 실행시켜 주고
DOTween 고유의 OnComplete 콜백 메서드에 오브젝트의 비활성화와 풀에 반환하는 코드를 추가합니다.
하이어라키를 보면 활성화와 비활성화가 활발히 이루어지는 것을 볼 수 있습니다.
풀에 오브젝트가 부족하면 생성도 서슴지 않습니다.
'유니티 > C# Code' 카테고리의 다른 글
[DOTween] Sequence의 상태에 대하여 (0) | 2024.09.14 |
---|---|
Parttern 응용 : Commander (+UniTask) (0) | 2024.09.12 |
Mesh Bake를 배웠습니다. (0) | 2024.08.31 |
RaycastAll과 Array.Sort (0) | 2024.08.31 |
(Player Input) 캐릭터의 이동 구현 코드 (0) | 2024.07.11 |