클릭은 콜라이더가 받고 UI 는 월드에 놓여 있다
Esc

    클릭은 콜라이더가 받고 UI 는 월드에 놓여 있다

    –UnityUINGUI라이브러리

    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 를 그리는 카메라마다 하나씩 붙입니다. 그 카메라가 화면 좌표에서 레이를 만들어 쏩니다.

    클릭 한 번이 지나가는 길 화면을 누릅니다 마우스나 터치 UICamera 가 그 점으로 레이를 만듭니다 카메라마다 하나씩 붙습니다 레이어로 거르고 콜라이더를 맞힙니다 cullingMask & eventReceiverMask 맞은 것들을 raycastDepth 로 정렬합니다 거리가 아닙니다 맨 앞 오브젝트에 SendMessage OnClick() 이 있으면 불립니다 받는 쪽이 등록하는 게 아니라, 보내는 쪽이 이름으로 부릅니다
    물리 레이캐스트로 시작해서 함수 이름으로 끝납니다.

    쏘는 방식이 네 가지입니다. 주석이 각각 한 줄로 적혀 있습니다.

    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).”

    UI 좌표가 월드 좌표가 되는 방법 UI 좌표 버튼 높이 720 칸 localScale 2 / 720 월드 좌표 같은 버튼 높이 2 유닛 정사영 카메라 size = 1 이 딱 덮습니다 루트가 미리 줄여 놔서 위젯의 localPosition 100 이 화면에서 100픽셀쯤 됩니다 같은 공간이라 3D 오브젝트를 UI 사이에 그대로 끼울 수 있습니다
    UIRoot 가 Update 에서 매 프레임 이 스케일을 맞춥니다.

    실제로 하는 일도 그 한 줄입니다.

    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 같은 연속 값은 없습니다