미니맵 줌 시스템 설계 (슬라이더 & 스크롤 통합)
1. 시스템 요구 사항
미니맵 카메라가 플레이어를 정확히 추적하고 회전까지 동기화되더라도, 항상 같은 시야 범위만 제공한다면 실제 플레이에서는 한계가 드러난다.
탐색 상황에서는 넓은 범위를 한눈에 보고 싶고, 전투나 정밀 이동 상황에서는 주변만 집중해서 보고 싶은 순간이 반복적으로 발생한다.
즉, 미니맵은 고정된 시야가 아니라 상황에 따라 유연하게 조절 가능한 시야를 제공해야 했다.
이때 단순히 확대·축소 기능을 추가하는 것보다 더 중요한 문제는 어떤 입력 방식으로 줌을 조절할 것인가였다.
마우스 스크롤은 빠르고 직관적이지만 정밀한 조절에는 부적합하고, 슬라이더는 정확하지만 조작 속도가 느리다.
두 입력 방식을 동시에 제공하려면, 각 입력이 서로 다른 값을 건드리는 구조가 아니라 하나의 줌 상태를 공유하며 동기화되는 구조가 필요했다.
그렇지 않으면 스크롤로 조정한 직후 슬라이더가 엉뚱한 위치를 가리키거나, 슬라이더 조작 후 스크롤이 갑자기 튀는 문제가 발생한다.
따라서 이 시스템의 요구사항은 스크롤과 슬라이더가 서로 다른 입력이지만, 항상 같은 줌 상태를 가리키도록 만드는 것이었다.
2. 설계 목표
- 미니맵의 확대·축소를 하나의 연속적인 값으로 관리할 것
- 마우스 스크롤과 슬라이더 입력이 항상 동일한 줌 상태를 공유하도록 할 것
- 미니맵 영역 위에서만 스크롤 입력이 반응하도록 제한할 것
- 카메라 방식(직교/원근)과 무관하게 동일한 줌 로직을 유지할 것
3. 흐름도

이 흐름의 핵심은 스크롤이든 슬라이더든 항상 SetZoom 하나로 수렴한다는 점이다.
줌 상태를 변경하는 진입점이 하나뿐이기 때문에, 입력 방식이 달라도 결과는 항상 일관된다.
4. 구현
4.1. 슬라이더 기반 줌 초기화 및 입력 처리
void Start()
{
if (zoomSlider != null)
{
zoomSlider.minValue = 0f;
zoomSlider.maxValue = 1f;
float t = Mathf.InverseLerp(minZoom, maxZoom, defaultZoom);
zoomSlider.value = t;
zoomSlider.onValueChanged.AddListener(OnZoomSliderChanged);
}
SetZoom(defaultZoom);
}
미니맵 줌 슬라이더는 실제 줌 값(minZoom ~ maxZoom)을 직접 다루지 않고, 0~1 사이의 정규화된 비율 값만 다루도록 설계했다.
이 방식의 장점은, 슬라이더 UI가 줌의 실제 단위나 카메라 구현 방식에 의존하지 않는다는 점이다.
슬라이더는 단순히 '가장 가까움 ↔ 가장 멀리' 라는 의미만 표현하고, 실제 값 변환은 코드에서 담당한다.
Mathf.InverseLerp를 사용해 기본 줌 값을 0~1 범위로 변환함으로써, 미니맵이 처음 표시되는 순간부터 카메라 상태와 슬라이더 상태가 정확히 일치하도록 만들었다.
void OnZoomSliderChanged(float t)
{
float value = Mathf.Lerp(minZoom, maxZoom, t);
SetZoom(value);
}
슬라이더가 움직이면 전달되는 값은 항상 0~1 사이의 비율이다.
이를 Mathf.Lerp로 실제 줌 값으로 환산한 뒤, 최종 적용은 반드시 SetZoom을 통해서만 이루어지도록 했다.
이 구조 덕분에 슬라이더는 줌 상태를 표현할 뿐이고, 줌 로직의 실제 책임은 한 곳으로 모이게 된다.
4.2. 마우스 스크롤 기반 줌 처리 (미니맵 영역 제한)
void HandleScrollZoom()
{
if (zoomSlider == null || miniMapArea == null)
return;
bool overMiniMap = RectTransformUtility.RectangleContainsScreenPoint(
miniMapArea,
Input.mousePosition,
null
);
if (!overMiniMap) return;
float scroll = Input.GetAxis("Mouse ScrollWheel");
if (Mathf.Abs(scroll) < 0.0001f) return;
float current = GetCurrentZoomValue();
current -= scroll * scrollSensitivity;
current = Mathf.Clamp(current, minZoom, maxZoom);
SetZoom(current);
float t = Mathf.InverseLerp(minZoom, maxZoom, current);
zoomSlider.SetValueWithoutNotify(t);
}
스크롤 줌에서 가장 먼저 처리한 것은 마우스가 실제로 미니맵 위에 있는지 여부였다.
RectTransformUtility.RectangleContainsScreenPoint를 사용해 마우스 위치가 미니맵 영역 안에 있을 때만 스크롤 입력을 허용했다.
이를 통해 화면의 다른 UI를 스크롤할 때 미니맵이 의도치 않게 확대·축소되는 문제를 구조적으로 차단했다.
스크롤 입력은 현재 줌 값에서 증감하는 방식으로 처리했다.
이때도 직접 카메라를 조작하지 않고, 항상 현재 줌 값을 가져와 계산한 뒤 SetZoom으로 전달한다.
중요한 점은, 스크롤로 변경된 줌 값이 즉시 슬라이더 값에도 반영된다는 점이다.
SetValueWithoutNotify를 사용해 슬라이더 이벤트를 다시 발생시키지 않으면서 UI 상태만 동기화했다.
이로 인해 스크롤과 슬라이더는 항상 같은 줌 상태를 바라보게 된다.
4.3. 카메라 방식과 무관한 줌 적용
float GetCurrentZoomValue()
{
if (miniMapCam.orthographic)
return miniMapCam.orthographicSize;
else
return transform.position.y;
}
미니맵 카메라는 프로젝트 설정에 따라 직교(Orthographic) 또는 원근(Perspective) 방식이 될 수 있다.
확대 · 축소의 기준 역시 이 두 방식에서 서로 다르다.
직교 카메라는 orthographicSize가 시야 크기를 결정하고, 원근 카메라는 카메라의 높이(Y 좌표)가 시야 범위를 결정한다.
이 차이를 외부 로직에서 신경 쓰지 않도록, 현재 줌 값을 가져오는 책임을 하나의 함수로 묶었다.
void SetZoom(float value)
{
value = Mathf.Clamp(value, minZoom, maxZoom);
if (miniMapCam.orthographic)
{
miniMapCam.orthographicSize = value;
}
else
{
Vector3 p = transform.position;
p.y = value;
transform.position = p;
}
}
최종적으로 줌 값을 적용하는 단계 역시 카메라 방식에 따라 분기되지만, 외부에서는 항상 SetZoom(value)만 호출하도록 설계했다.
이 구조 덕분에 줌 로직은 카메라 구현 방식과 분리되었고, 미니맵 카메라 설정이 바뀌더라도 입력·UI 코드는 수정할 필요가 없게 된다.
5. 개발 의도
이 게시글에서 보여주고자 한 핵심은 입력 방식이 여러 개여도, 상태는 하나여야 한다는 설계 원칙이다.
스크롤과 슬라이더는 서로 다른 UX를 제공하지만, 결과적으로 조작하는 대상은 동일한 ‘미니맵 시야 범위’다.
그래서 입력을 늘리는 대신, 상태를 하나로 통합하고 모든 입력을 그 상태로 수렴시키는 구조를 선택했다.
이 방식은 이후 줌 애니메이션, 자동 줌 조절, 퀘스트 시점 확대 같은 기능이 추가되더라도 기존 입력 구조를 건드리지 않고 자연스럽게 확장할 수 있는 기반이 된다.
다음 게시글에서는 미니맵이 단순한 위치 표시를 넘어 월드 오브젝트(NPC) 정보를 어떻게 UI와 연결했는지를 다룬다.
특히 RawImage 기반 미니맵에서 발생한 좌표 문제와 그 해결 과정을 중심으로 설명할 예정이다.
