멈추지 않고 끈질기게
[Unity] 롤링 배너 구현 본문
※ 해당 포스팅은 개인의 공부 정리용 글입니다. 틀린 내용이 있다면 추후 수정될 수 있습니다.
※ 해당 포스팅은 Unity 2022.3.10f1 버전을 기준으로 작성되었습니다.
이번 포스팅에서는 유니티에서 롤링 배너를 구현하는 법에 대해 알아보겠습니다.
1. UI 설정
우선 캔버스에 UI > Scroll View를 선택하여 추가하고, 스크롤바를 모두 삭제합니다. 스크롤뷰 선택 후 컴포넌트를 확인해보면 Scroll Rect라는 컴포넌트가 있고, 여기서 스크롤 방향, 무브먼트 타입(부드럽게 or 고정되게), 스크롤 민감도 등을 조정할 수 있습니다. 스크롤 방향은 디폴트로 모두 체크되어 있지만, 수평 방향으로만 움직일 예정이므로 Vertical은 체크를 해제해줍니다.
스크롤뷰를 보면 하위에 View Port가 있고, 그 하위에 Content가 있습니다. 이 Content가 스크롤하며 보여줄 내용물이 들어갈 자리이므로, 크기를 내용물에 맞게 조정해주어야 합니다. 배너를 스크롤뷰에 꽉 차게 보여줄 예정이라면 높이는 스크롤 뷰와 동일하게, 너비는 (스크롤뷰의 너비) * (배너 갯수)로 설정해줍니다. 또한 배너는 수평으로 동일한 크기로 정렬할 예정이므로 Content에 Horizontal Layout Group을 추가하고, Control Child Size 및 Child ForceExpand 를 수평, 수직 모두 체크해줍니다.
이제 Content 하위에 배너로 사용할 이미지를 예정된 갯수만큼 추가해주면, 자동으로 스크롤뷰 크기에 딱 맞도록 조정됩니다. 저는 Content의 너비를 스크롤뷰의 4배로 설정하고, 배너 4개를 추가하여 세팅했습니다.
2. 스크립트 제어
다음은 롤링배너를 구현한 스크립트 예시입니다.
public class RollingBanner : MonoBehaviour, IPointerDownHandler, IPointerUpHandler
{
Dictionary<string, Image> dict = new Dictionary<string, Image>();
enum Images
{
White,
Red,
Blue,
Green
}
Image[] _banners;
RectTransform _contents;
float _width = 0;
float[] xPos;
int _curIdx = 0;
Coroutine _rollingRoutine;
string[] _urlArr;
#region Init
void Awake()
{
Application.targetFrameRate = 60;
}
void Start()
{
BindImage();
InitBanners();
InitUrls();
_rollingRoutine = StartCoroutine(AutoRolling());
}
void BindImage()
{
string[] names = Enum.GetNames(typeof(Images));
for (int i = 0; i < names.Length; i++)
{
foreach (Image img in GetComponentsInChildren<Image>())
{
if (names[i] == img.name)
{
dict.Add(names[i], img);
break;
}
}
}
}
void InitBanners()
{
string[] names = Enum.GetNames(typeof(Images));
_banners = new Image[names.Length];
for (int i = 0; i < _banners.Length; i++)
_banners[i] = dict[names[i]];
foreach (RectTransform rect in GetComponentsInChildren<RectTransform>())
{
if (rect.name == "Content")
{
_contents = rect;
break;
}
}
_width = GetComponent<RectTransform>().sizeDelta.x;
xPos = new float[_banners.Length];
for (int i = 0; i < xPos.Length; i++)
xPos[i] = -i * _width;
}
void InitUrls()
{
List<string> urls = new List<string>(_banners.Length)
{
"https://www.youtube.com/",
"https://github.com/pkh0308",
"https://sam0308.tistory.com/",
"https://www.naver.com/"
};
_urlArr = new string[urls.Count];
for (int i = 0; i < _urlArr.Length;i++)
{
int idx = UnityEngine.Random.Range(0, urls.Count);
string targetURL = urls[idx];
_urlArr[i] = targetURL;
urls.RemoveAt(idx);
}
}
#endregion
우선 롤링배너의 드래그 후 위치 원복 or 다음 배너로 이동하는 기능을 구현하기 위해 IPointerDownHandler , IPointerUpHandler 인터페이스를 상속받습니다(구현부는 후술). 스크립트 상에서 UI를 찾고 저장해두기 위한 Dictionary와 배열 등을 선언하였고, 고정된 성능을 확인하기 위해 타겟프레임은 60으로 제한했습니다. Start()에서는 초기화용으로 선언한 함수들을 실행하고, 오토 스크롤을 구현한 코루틴을 실행합니다.
BindImage()는 배너 이름들을 그대로 선언한 열거형과 GetComponentsInChildren()을 통해 배너들을 검색한 후, Dictionary에 저장합니다. InitBanners()에서는 이를 이용해 이미지 타입 배열을 초기화하고, Content의 RectTransform을 별도의 변수로 참조하게 합니다. (스크롤뷰의 너비) == (각 배너의 너비) 이므로 스크롤뷰의 너비를 따로 저장해두고, 이를 통해 각 배너가 정확하게 보이는 x 좌표를 배열에 저장합니다.
배너 클릭 시 웹페이지를 오픈하도록 구현할 예정이고, InitUrls()는 url을 랜덤하게 배정하는 함수입니다. 원소의 갯수가 많지 않으므로 리스트에서 랜덤하게 뽑아서 배열에 저장하고, 리스트의 원본은 삭제하는 식으로 구현했습니다.
#region interface
public void OnPointerDown(PointerEventData eventData)
{
StopCoroutine(_rollingRoutine);
}
public void OnPointerUp(PointerEventData eventData)
{
float gap = _contents.anchoredPosition.x - xPos[_curIdx];
if(Mathf.Abs(gap) < 5.0f)
{
Application.OpenURL(_urlArr[_curIdx]);
_rollingRoutine = StartCoroutine(AutoRolling());
return;
}
if (gap < _width * -0.5f && _curIdx < _banners.Length - 1) // 오른쪽
StartCoroutine(MoveRight());
else if (gap > _width * 0.5f && _curIdx > 0) // 왼쪽
StartCoroutine(MoveLeft());
else // 제자리
MoveReset();
_rollingRoutine = StartCoroutine(AutoRolling());
}
#endregion
IPointerDownHandler 및 IPointerUpHandler의 구현부입니다. OnPointerDown()에서는 드래그 중에 오토 스크롤되는 상황을 막기 위해 오토 스크롤 코루틴을 중지하도록 했습니다. OnPointerUp()에서는 Content의 현재 x좌표와 현재 배너의(온전히 보이는) x좌표의 차를 계산하고, 값이 아주 작다면 클릭으로 간주하여 해당 인덱스의 url을 오픈하도록 했습니다.
그 외의 경우 스크롤뷰 너비의 절반을 기준으로 기준보다 오른쪽으로 이동했다면 우측 이동 코루틴을, 좌측으로 이동했다면 좌측 이동 코루틴을 실행하도록 했습니다. 기준보다 적게 움직인 경우에는 배너를 제자리로 돌리는 MoveReset()을 실행하고, 드래그가 끝난 후에는 다시 오토 스크롤이 동작하도록 오토 스크롤 코루틴을 실행하도록 했습니다.
#region Controll
const float MOVE_OFFSET = 3.0f;
const float MOVE_OFFSET_TOSTART = 7.0f;
IEnumerator MoveRight()
{
_curIdx++;
float targetPos = xPos[_curIdx];
int count = 1;
while (_contents.anchoredPosition.x != targetPos)
{
_contents.anchoredPosition += Vector2.left * Mathf.Log10(count) * MOVE_OFFSET;
if (_contents.anchoredPosition.x < targetPos)
_contents.anchoredPosition = new Vector2(targetPos, _contents.anchoredPosition.y);
count++;
yield return null;
}
}
IEnumerator MoveLeft(bool toStart = false)
{
_curIdx = toStart ? 0 : _curIdx - 1;
float targetPos = xPos[_curIdx];
float offset = toStart ? MOVE_OFFSET_TOSTART : MOVE_OFFSET;
int count = 1;
while (_contents.anchoredPosition.x != targetPos)
{
_contents.anchoredPosition += Vector2.right * Mathf.Log10(count) * offset;
if (_contents.anchoredPosition.x > targetPos)
_contents.anchoredPosition = new Vector2(targetPos, _contents.anchoredPosition.y);
count++;
yield return null;
}
}
void MoveReset()
{
float gap = _contents.anchoredPosition.x - xPos[_curIdx];
if(gap > 0)
{
_curIdx--;
StartCoroutine(MoveRight());
}
else
{
_curIdx++;
StartCoroutine(MoveLeft());
}
}
IEnumerator AutoRolling()
{
while(true)
{
yield return new WaitForSeconds(2.0f);
int nextIdx = (_curIdx + 1) % _banners.Length;
if(nextIdx > _curIdx) // 오른쪽 이동
StartCoroutine(MoveRight());
else // 처음 위치로 이동
StartCoroutine(MoveLeft(true));
}
}
#endregion
}
MoveRight()는 현재 인덱스를 1 증가시키고 다음 배너의 x좌표를 타겟으로 정한 후, Content를 왼쪽으로 밀다가(왼쪽으로 밀어야 오른쪽 배너가 보임) 타겟 좌표를 넘어간 경우 타겟 좌표로 설정하여 while()문을 벗어나도록 했습니다. 속도 계수는 상수값으로 선언해두고, 부드러운 움직임을 위해 로그스케일로 계산한 값을 사용했습니다. MoveLeft()의 경우에도 방향을 제외하고 동일하며, 단 마지막 배너에서 처음 배너로 돌아가는 경우가 있기에 bool 매개변수를 통해 구분하도록 했습니다.
MoveReset()은 배너 크기의 절반보다 조금 드래그했을 때 호출되는 함수로, MoveRight()나 MoveLeft()를 실행하기 전에 인덱스값을 증감하여 같은 값으로 유지되도록 했습니다. AutoRolling()은 오토 스크롤을 구현한 코루틴으로, 2초마다 다음 배너로 이동(우측)하거나 맨 처음 배너로 이동(좌측)합니다.
다음은 해당 스크립트로 구현된 롤링 배너 예시입니다(클릭 확인을 위해 로그 출력을 추가하였습니다).
3. 결론
롤링 배너는 모바일 게임에서 자주 사용되는 UI라 생각난 김에 한번 구현해보았습니다. 각 배너의 ID를 데이터베이스 등 외부에서 불러오고, 그에 맞는 리소스를 로드해오는 형태로 하면 좀 더 그럴듯한 모습이 될 것 같습니다. 다만 Content 영역의 너비 등 좀 더 많은 영역을 스크립트로 제어하도록 추가 개선해보면 좋을 듯 합니다.
'Unity' 카테고리의 다른 글
[Unity][Android] Google AdMob 관련 이슈(can only be called from the main thread) (0) | 2023.10.03 |
---|---|
[Unity][기타] 데이터 경로 관련 (0) | 2023.10.03 |
[Unity] 클릭/터치 인터페이스(IPointerClickHandler, IPointerDownHandler, IPointerUpHandler) (1) | 2023.09.06 |
[Unity] Addressable 기능 (0) | 2023.07.27 |
[Unity][포트폴리오] Rigidbody.AddForce()가 제대로 동작하지 않는 이슈 (0) | 2023.07.03 |