Unity

[Unity 2D]Cinemachine을 이용하여 화면 드래그 시 카메라 이동시키기

(ꐦ •᷄ࡇ•᷅) 2025. 3. 5. 22:31

카메라 구현에 있어서 Cinemachine을 선택한 이유

  • 기본 카메라를 사용하게 되면 직접 스크립트를 짜서 기능을 구현해야 하는 경우가 많다. 
  • 구현해야 할 기능들이 많아지면 많아질수록 이는 부담으로 다가온다.
  • 하지만 Cinemachine을 사용하면 Cinemachine이 제공하는 강력한 기능들을 손쉽게 프로젝트에 적용할 수 있다.

 

구현해야 하는 것

  • 2D 모바일 환경에서 드래그로 맵을 둘러볼 수 있는 기능.
  • 이때 카메라는 특정 구역 바깥으로 나가지 않아야 한다.

 

구현 방법

1. UI로 DragPanel 만들고, 스크립트 짜기

  • UI로 DragPanel을 만들어 터치할 부분에 오브젝트가 있어도 UI의 특성을 활용해 드래그가 가능하게 했다.
  • 또한 DragPanel 밑 오브젝트를 클릭해야 할 경우도 있으므로 Canvas의 Graphic raycastTarget 속성을 조절하는 방식으로 처리를 해 줬다.
  • (UI가 아닌 오브젝트에 IPointer Handler를 작동시키려면 Physics RayCaster 컴포넌트가 메인 카메라에 붙어 있어야 한다.)
  • 이때 IPointerDownHandler, IPointerUpHandler, IDragHandler 인터페이스를 사용하여 드래그 동작을 구현했다.

UI_DragPanel.cs

더보기
더보기
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

public class UI_DragPanel : UI_Base, IPointerDownHandler, IPointerUpHandler, IDragHandler
{
    // 자연스러운 움직임을 위해 빈 오브젝트를 Cinemachine 카메라가 따라가는 형식으로 구현
    // 드래그인지 아닌지 판단하여, 오브젝트에 이벤트를 전달할 수 있도록 한다

    [Header("Components")]
    public Camera mainCamera;               // mainCam
    public BoxCollider2D confiner;             // 카메라 가두리
    public Transform target;                // 빈 오브젝트

    [Header("Drag Settings")]
    public float dragSpeed = 0.005f;        // 화면 드래그 스피드
    public float dragThreshold = 10f;       // 드래그 판단 임계치 (픽셀)

    private Vector2 lastTouchPos;
    private bool isDragging = false;
    private Graphic graphic;                 // raycast target on/off 용도

    private void Awake()
    {
        mainCamera = GameObject.Find("Cameras").GetComponentInChildren<Camera>();
        target = GameObject.Find("Cameras").transform.Find("Target");
        confiner = GameObject.Find("Map").GetComponent<BoxCollider2D>();

        Managers.Game.Confiner = confiner;

        graphic = GetComponent<Graphic>();
    }

    public override bool Init()
    {
        if (base.Init() == false)
            return false;

        return true;
    }

    public void OnDrag(PointerEventData eventData)
    {
        // 드래그 시작 여부 판단
        if (!isDragging)
        {
            if(Vector2.Distance(lastTouchPos, eventData.position) > dragThreshold)
                isDragging = true;
        }

        // 드래그 중이면 카메라 타겟 이동 처리 
        if (isDragging)
        {
            Vector2 delta = eventData.delta;
            Vector3 worldDelta = mainCamera.ScreenToWorldPoint(new Vector3(0, 0, mainCamera.nearClipPlane))
                                  - mainCamera.ScreenToWorldPoint(new Vector3(delta.x, delta.y, mainCamera.nearClipPlane));
            Vector3 newPos = target.position + worldDelta * dragSpeed;

            // 카메라의 중심이 Confiner를 벗어나지 않게 조정
            if (confiner != null)
            {
                Bounds bounds = confiner.bounds;

                // 화면의 왼쪽 하단, 오른쪽 상단을 월드 좌표로 반환
                Vector3 worldMin = mainCamera.ViewportToWorldPoint(new Vector3(0, 0, mainCamera.nearClipPlane));
                Vector3 worldMax = mainCamera.ViewportToWorldPoint(new Vector3(1, 1, mainCamera.nearClipPlane));

                float halfX = (worldMax.x - worldMin.x) / 2;
                float halfY = (worldMax.y - worldMin.y) / 2;

                newPos.x = Mathf.Clamp(newPos.x, bounds.min.x + halfX, bounds.max.x - halfX);
                newPos.y = Mathf.Clamp(newPos.y, bounds.min.y + halfY, bounds.max.y - halfY);
            }

            target.position = newPos;
        }
    }

    public void OnPointerDown(PointerEventData eventData)
    {
        lastTouchPos = eventData.position;
        isDragging = false;
    }

    public void OnPointerUp(PointerEventData eventData)
    {
        // 드래그가 아니었다면 터치로 간주하여 아래 오브젝트에 이벤트 전달
        if (!isDragging)
        {
            // UI 패널이 터치 이벤트를 가로채지 않도록 잠시 RayCast Target 비활성화
            graphic.raycastTarget = false;

            // 현재 이벤트 정보를 바탕으로 Drag Panel 밑에 있는 오브젝트 찾음
            List<RaycastResult> results = new List<RaycastResult>();
            EventSystem.current.RaycastAll(eventData, results);

            // 자신을 제외한 첫번째 결과에 클릭 이벤트 전달
            foreach (var result in results)
            {
                if (result.gameObject != gameObject)
                {
                    // 클릭 이벤트 실행
                    ExecuteEvents.Execute(result.gameObject, eventData, ExecuteEvents.pointerClickHandler);
                    break;
                }
            }
            
            // Raycast Target 활성화
            graphic.raycastTarget = true;
        }

        isDragging = false;
    }
}

 

2. Confiner 설정

맵 반경을 결정지을 Collider를 만들어 카메라가 비추는 영역이 그 바깥으로 나가지 않게 구역을 정해 준다.

이후 코드에서 Collider 바깥으로 나가지 않게 구현한다.

 

3. Cinemachine이 Follow할 빈 오브젝트 만들기

빈 오브젝트를 만들어 화면을 Drag할 시 빈 오브젝트가 움직이게끔 하고, Cinemachine의 Follow 기능을 이용해 카메라가 빈 오브젝트를 따라 비추게 한다. 

 

결과