클릭은 콜라이더가 받고 UI 는 월드에 놓여 있다
uGUI 는 NGUI 에서 시작했다에서 갈리는 지점 셋을 짚었고, 그리는 순서는 하이어라키가 아니라 숫자가 정한다에서 첫째를 봤습니다. 남은 둘이 이번입니다.
둘을 따로 쓸 생각이었는데 소스를 열어 보니 한 이야기였습니다. UI 가 월드에 놓인 오브젝트라서 입력도 물리로 받습니다. 순서가 반대가 아니라 이쪽입니다.
버튼에 콜라이더가 붙어 있습니다
NGUI 로 버튼을 만들면 BoxCollider 가 같이 붙습니다. NGUITools.AddWidgetCollider 가 하는 일이 그것입니다. 붙일 때 두 가지를 같이 설정합니다.
box = go.AddComponent<BoxCollider>();
box.isTrigger = true;
var widget = go.FastGetComponent<UIWidget>();
if (widget != null) widget.autoResizeBoxCollider = true;
isTrigger 라서 물리 충돌은 안 일으킵니다. 레이캐스트에만 걸리라고 붙은 것입니다. autoResizeBoxCollider 가 켜져 있으니 위젯 크기를 바꾸면 콜라이더도 따라 바뀝니다.
씬에 UICamera 의 eventType 이 2D 계열로 되어 있으면 BoxCollider2D 를 대신 붙입니다. 어느 쪽이든 버튼에 콜라이더가 있는 것이 정상 입니다.
이어받은 프로젝트에서 “쓸데없는 콜라이더가 붙어 있네” 하고 지우면 그 버튼은 안 눌립니다. uGUI 의 Raycast Target 체크를 끄는 것과 같은 결과인데, 여기서는 지웠다는 자각조차 없이 일어납니다.
UICamera 가 레이를 쏩니다
받는 쪽은 카메라입니다. UICamera 는 [RequireComponent(typeof(Camera))] 이고 컴포넌트 메뉴 이름이 “NGUI/UI/Event System (UICamera)” 입니다. 소스 주석이 이렇게 시작합니다.
“This script should be attached to each camera that’s used to draw the objects with UI components on them.”
UI 를 그리는 카메라마다 하나씩 붙입니다. 그 카메라가 화면 좌표에서 레이를 만들어 쏩니다.
쏘는 방식이 네 가지입니다. 주석이 각각 한 줄로 적혀 있습니다.
eventType |
소스 주석 |
|---|---|
World_3D |
“Perform a Physics.Raycast and sort by distance to the point that was hit” |
UI_3D |
“Perform a Physics.Raycast and sort by widget depth” |
World_2D |
“Perform a Physics2D.OverlapPoint” |
UI_2D |
“Physics2D.OverlapPoint then sort by widget depth” |
기본값이 UI_3D 입니다. 무엇을 맞힐지는 레이어로 거릅니다. 코드에서 currentCamera.cullingMask & (int)cam.eventReceiverMask 로 둘을 겹쳐 쓰니, 카메라가 안 그리는 레이어는 클릭도 안 받습니다.
이 구조 때문에 따라오는 것이 있습니다. UI 를 여러 카메라로 나눠 그리면 UICamera 도 카메라 수만큼 붙습니다. 3D 월드를 누르는 것과 UI 를 누르는 것이 서로 다른 카메라의 일이 되고, 레이어로 갈라 둡니다.
가까운 것이 아니라 앞에 있는 것이 이깁니다
UI_3D 가 하는 일을 한 번 더 봅니다. 레이를 쏘는 것까지는 물리인데, 고르는 기준이 거리가 아닙니다.
맞은 것들을 전부 모은 다음 NGUITools.CalculateRaycastDepth 로 각각의 깊이를 구하고, 큰 것부터 내림차순으로 정렬합니다. 그 깊이가 앞 편에서 본 raycastDepth 입니다.
return (panel != null) ? mDepth + panel.depth * 1000 : mDepth;
그리는 순서와 클릭 받는 순서가 같은 숫자에서 나옵니다. 앞에 그려진 것이 클릭도 먼저 받습니다. 거리를 안 보니까 UI 를 카메라 앞 어디에 놓든 상관없습니다.
CalculateRaycastDepth 에는 한 가지가 더 들어 있습니다. 맞은 오브젝트에 위젯이 없으면 자식들의 위젯을 모아서 그중 제일 작은 깊이 를 씁니다. 콜라이더를 부모에 하나만 붙이고 속에 위젯 여러 개를 넣는 구조가 이래서 동작합니다.
uGUI 와 UI Toolkit 을 나란히 놓으면 이렇습니다.
| 체계 | 클릭을 받는 조건 | 겹쳤을 때 이기는 것 |
|---|---|---|
| uGUI | Graphic 의 Raycast Target | 하이어라키상 앞에 있는 것 |
| NGUI | 콜라이더 | raycastDepth 가 큰 것 |
| UI Toolkit | pickingMode |
앞에 그려진 것 (그리는 순서의 반대로 검사) |
셋 다 “앞에 있는 것이 이긴다” 는 같은데, 앞을 정하는 방법이 각각 다릅니다. 그게 2편에서 본 순서 이야기와 같은 갈림길입니다.
알림은 함수 이름으로 옵니다
맞힌 다음에는 알려 줘야 합니다. UICamera.Notify 가 그 일을 합니다.
go.SendMessage(funcName, obj, SendMessageOptions.DontRequireReceiver);
SendMessage 입니다. 이름이 맞는 함수가 그 오브젝트에 있으면 불리고, 없으면 조용히 넘어갑니다. 그래서 NGUI 프로젝트의 스크립트에는 이런 함수들이 붙어 있습니다.
void OnClick () { }
void OnPress (bool isPressed) { }
void OnDrag (Vector2 delta) { }
void OnHover (bool isOver) { }
인터페이스를 구현하는 것도 아니고 등록하는 것도 아닙니다. 이름만 맞으면 됩니다. 오타가 나면 컴파일은 통과하고 그냥 안 불립니다. 이어받은 프로젝트에서 “이 함수를 누가 부르지” 하고 찾아봐도 호출부가 안 나오는 이유가 이것입니다.
UICamera 가 보내는 이름은 열댓 개입니다. OnHover OnPress OnSelect OnClick OnDoubleClick OnDragStart OnDrag OnDragOver OnDragOut OnDragEnd OnTooltip OnScroll OnNavigate OnPan OnKey.
인스펙터로 거는 방법도 따로 있습니다. UIButton 에는 List<EventDelegate> onClick 이 있어서 uGUI 의 On Click 칸처럼 끌어다 붙입니다. 이벤트는 루트에서 내려갔다가 다시 올라온다에서 본 UI Toolkit 과 비교하면, 인스펙터 연결이 없는 쪽은 셋 중 UI Toolkit 하나뿐 입니다.
UI 는 월드에 놓여 있습니다
여기까지 오면 왜 물리로 받는지가 보입니다. NGUI 의 UI 는 화면에 얹는 무엇이 아니라 씬 안의 진짜 오브젝트 입니다. 메시가 있고 콜라이더가 있고 좌표가 있습니다.
그 좌표를 맞춰 주는 것이 UIRoot 입니다. 주석이 한 줄입니다.
“This is a script used to keep the game object scaled to 2/(Screen.height).”
실제로 하는 일도 그 한 줄입니다.
float size = 2f / calcActiveHeight;
mTrans.localScale = new Vector3(size, size, size);
Update 에서 매 프레임 돌면서 루트의 스케일을 이 값으로 맞춥니다. 화면 높이가 720 이면 스케일이 2/720 이고, 그러면 UI 720칸이 월드 2유닛 이 됩니다. 정사영 카메라의 orthographicSize 가 1 일 때 보이는 높이가 딱 2유닛이니, 화면에 정확히 맞습니다.
따라오는 것이 둘입니다.
- 좌표가 픽셀처럼 보입니다. 위젯의
localPosition을 100 으로 두면 화면에서 대략 100픽셀만큼 옮겨집니다. 루트가 그만큼 줄여 놨기 때문입니다 - 3D 오브젝트를 UI 사이에 끼울 수 있습니다. 캐릭터 모델을 UI 카메라가 비추는 자리에 놓으면 UI 와 같은 공간에 섞입니다. uGUI 의 Screen Space Overlay 에서는 이게 안 되고, World Space 캔버스로 바꿔야 했습니다
해상도 대응은 UIRoot 의 Scaling 입니다
UIRoot.Scaling 이 셋입니다. Flexible, Constrained, ConstrainedOnMobiles.
Flexible 은 위에서 본 계산을 화면 높이에 그대로 씁니다. 1 UI 칸이 1 픽셀이 되는 셈이라 화면이 커지면 UI 가 작아 보입니다. 대신 minimumHeight 320 과 maximumHeight 1536 으로 위아래를 죕니다. 너무 작거나 너무 큰 화면에서는 고정 크기처럼 동작합니다.
Constrained 는 manualWidth 1280 과 manualHeight 720 을 기준으로 잡습니다. Canvas Scaler 는 가로와 세로 중 한쪽만 지킨다에서 본 것과 같은 자리이고, fitWidth 와 fitHeight 체크 둘로 어느 쪽을 지킬지 고릅니다. 둘 다 켜면 Fit, 둘 다 끄면 Fill 입니다.
ConstrainedOnMobiles 는 모바일에서만 Constrained 로 동작합니다. 에디터와 PC 에서는 Flexible 입니다.
Canvas Scaler 의 Match 슬라이더 같은 연속 값이 없습니다. 체크 두 개로 네 조합입니다. 대응을 찾는다면 fitHeight 만 켠 것이 Match 1 에, fitWidth 만 켠 것이 Match 0 에 가깝습니다.
정리
- NGUI 의 버튼에는
BoxCollider가isTrigger로 붙어 있고, 위젯 크기를 따라갑니다. 지우면 안 눌립니다 UICamera가 카메라마다 붙어서 레이를 쏩니다.cullingMask & eventReceiverMask라 카메라가 안 그리는 레이어는 클릭도 안 받습니다- 기본
UI_3D는 거리가 아니라raycastDepth로 고릅니다. 그리는 순서와 클릭 순서가 같은 숫자에서 나옵니다 - 알림은
SendMessage로 옵니다.void OnClick ()처럼 이름만 맞으면 불립니다. 호출부를 찾아도 안 나옵니다 - 인스펙터 연결도 있습니다(
List<EventDelegate> onClick). 셋 중 그게 없는 쪽은 UI Toolkit 뿐입니다 UIRoot가 루트를2/화면높이로 축소합니다. 그래서 좌표가 픽셀처럼 보이고, 3D 오브젝트를 UI 사이에 끼울 수 있습니다- 해상도 대응은
Flexible/Constrained/ConstrainedOnMobiles셋에 체크 두 개입니다. Match 같은 연속 값은 없습니다