최종 프로젝트에 들어오며 덱 빌딩 로그라이크 게임을 하기로 하였고, 기존까지는 협업 시 하지 않았던 UI 쪽을 깊게 해 보는 게 좋을 것 같아서, 카드의 UI 적인 기능과 상호작용을 담당하게 되었다.

기본적인 카드위에 마우스를 올릴 시 움직이는 기능은 IPointerEnterHandler, IPointerExitHandler 인터페이스를 사용해서 구현 하였다. 이는 마우스 관련 인터페이스들에 대한 정보를 이전에 배운 적이 있기에 어렵지 않게 구현할 수 있었다.

문제는 단순히 마우스를 위로 올리는것의 구현이 아니라 많은 카드 게임에서 보여주는, 카드를 특정 위치로 드래그 시의, 화살표가 오버레이처럼 표시되는것이다. 생각보다 구현이 어려웠고, 언젠가 또 응용할 수도 있기 때문에 기록해 두고자 한다.
먼저 점선 처럼 따라오는 선은 일반적인 월드 스페이스에서 구현하게 된다면 LineRenderer 컴포넌트를 사용하면 되지만, UI의 경우 다른 능력자 분이 만들어준 UI 익스텐션을 받아야 UI에서도 동일한 방식으로 LineRenderer를 쓸 수 있다.
https://github.com/Unity-UI-Extensions/com.unity.uiextensions
GitHub - Unity-UI-Extensions/com.unity.uiextensions
Contribute to Unity-UI-Extensions/com.unity.uiextensions development by creating an account on GitHub.
github.com
현업에서는 어떤 방식으로 구현하는지 모르겠지만 아직 배우는 입장에서는 남이 만든 익스텐션의 메서드를 활용하면서 배우는 것만으로도 충분히 도움이 된다고 생각하기에 바로 가져다 사용해 보았다.

깃을 통해 가져온 UILineRenderer 컴포넌트는 Bezier mode를 통해 간단하게 베지어 곡선을 구현할 수 있고. Points의 여러 개의 점선을 통해 베지어 곡선 형태의 자연스러운 곡선을 구현할 수도 있다.
베지어 곡선 이란?
베지어 곡선(Bezier Curve)은 컴퓨터 그래픽스에서 사용되는 특별한 형태의 곡선으로, CSS 애니메이션 등에서 도형을 그릴 때 사용합니다.
저렇게 가져온 곡선의 Points를 아래와 같이 마우스의 위치를 기반으로 세팅해주면, 자연스러운 곡선을 렌더링 할 수 있다.
void UpdateLineRenderer()
{
RectTransform cardRect = currentCard.GetComponent<RectTransform>();
Vector2 localMousePos;
// 마우스 위치를 UI 캔버스의 로컬 좌표로 변환.
RectTransformUtility.ScreenPointToLocalPointInRectangle(canvasRect, Input.mousePosition, null, out localMousePos);// 추후 카메라 분리시 null에 UI용 카메라 넣어주기
// lineRenderer를 곡선의 형태로 표시.
lineRenderer.Points[0] = cardRect.anchoredPosition; // 카드의 현재 위치
lineRenderer.Points[1] = new Vector2(localMousePos.x * 0.33f, localMousePos.y * 0.85f);
lineRenderer.Points[2] = new Vector3(localMousePos.x * 0.66f, localMousePos.y * 0.95f);
lineRenderer.Points[3] = localMousePos;
lineRenderer.SetAllDirty();// 라인 렌더러 업데이트.
}
이때 마지막에 SetAllDirty() 를 해주는 것이 매우 중요한데, 처음에 저 부분을 빼고 실행하자, 곡선이 드래그 한 딱 1 프레임만 보이고 고정되는 모습이어서 고생했다...
참고로 SetAllDirty() 의 기능은 Graphic 클래스를 상속받은 컴포넌트들에서 자주 사용된다.
위에 작성된 코드처럼 Points 로만 통해서 일부 수치가 변경되는 등의 경우, 유니티는 이를 그래픽 변화로 감지하지 않고 무시하기 때문에, 수동으로 해당 그래픽 변화를 업데이트 해라는 명령을 해주는 것이라고 보면 된다.
아무튼 위와 같은 작업을 거친 뒤 마지막으로 곡선의 끝부분에 표시할 화살표 부분의 위치와 각도를 업데이트 해 주었다.
void UpdateArrowHead()
{
if(lineRenderer.Points.Length < 2) return;
// 화살표의 위치와 각도를 업데이트
Vector2 p1 = CalculateBezierPoint(0.98f, lineRenderer.Points[0], lineRenderer.Points[1], lineRenderer.Points[2], lineRenderer.Points[3]);// 마지막 포인트에 근접한 점을 구하기 위해 t를 0.98f로 설정.
Vector2 p2 = lineRenderer.Points[lineRenderer.Points.Length - 1];// 마지막 포인트
Vector2 direction = (p2 - p1).normalized;
// 화살표의 위치 설정
arrowImage.anchoredPosition = p2;
// 화살표의 각도 설정
float angle = Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg;// UI에서 사용하기 위해 방향 벡터를 회전 각도로 변환.
arrowImage.localRotation = Quaternion.Euler(0, 0, angle);
}
// 3차 베지어 곡선 수식
Vector2 CalculateBezierPoint(float t, Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3)
{
float u = 1 - t;
float tt = t * t;
float uu = u * u;
float uuu = uu * u;
float ttt = tt * t;
Vector2 point = uuu * p0;
point += 3 * uu * t * p1;
point += 3 * u * tt * p2;
point += ttt * p3;
return point;
}
코드 하단에 있는 3차 베지어 곡선 수식은 화살표의 회전과 관련해서 유니티 포럼을 찾아보다 발견한 것으로 베지어 곡선에 대한 설명을 읽어보았지만 완벽히 이해는 하지 못하였다.. 하지만 공식 그 자체는 일단 맞아떨어진다고 판단하였고 코드를 가져와 LineRenderer의 마지막 Point의 직전 위치와 마지막 Point의 각도를 통해 화살표의 Euler 각도를 설정해 주니 아래와 같이 잘 작동하는 모습을 볼 수 있었다.

간단한 화살표를 곡선 형식으로 그려내면 금방 끝나겠지~ 라는 생각으로 시작했는데, 이때까지 UI 부분에 대한 경험이 부족했다는 것이 느껴졌다. 단순히 normalize를 통해 방향벡터를 구하면 이를 UI에서 사용하려면 라디안으로 변환하여 사용해야 한다는 부분부터, 마우스와 화면 간의 상호작용시에 계속 UI 상의 Rect와 월드 좌표 간의 연동을 신경 써줘야 하는 부분등...
어려운 만큼 스스로 모르는 부분을 배워갈 수 있었기에 내심 즐거운 기분도 들어 오늘은 정말 하루종일 식사도 거르고 집중해서 학습을 한 것 같다.
화이팅.
'C# > 게임 제작 + TIL' 카테고리의 다른 글
| UI - 팝업 시스템 (1) | 2025.04.18 |
|---|---|
| Dotween OnComplete() - 람다에서 루프 변수 참조시 변수 값 캐싱 (0) | 2025.04.08 |
| Physics2D.OverlapPoint - 2D 상의 겹침의 정도 (0) | 2025.04.02 |
| 삼각 함수 활용 - 원 형태로 투사체 발사시 (0) | 2025.04.01 |
| Particle System - 잔상 효과 (0) | 2025.03.31 |