[Unity] 인풋시스템으로 모바일 줌 인/아웃 만들기

요구르트·2025년 3월 6일

Unity

목록 보기
1/7

이번에 졸업작품을 모바일로 제작하게 되면서, 모바일 터치 및 조작에 대해 공부하였습니다.
제가 만들 기능은 핀치 줌(Pinch Zoom In/Out)을 만들어볼 예정입니다. 아래는 완성된 모습입니다.

이 영상을 참고하였습니다.

핀치 줌(Pinch Zoom In/Out)

핀치 줌은 모바일 게임에서 확대 축소할 때 많이 사용됩니다.
모바일 특징이 터치를 여러번 동시에 할 수 있기 때문에, 많은 모바일 게임에서 핀치 줌을 사용합니다.
핀치 줌의 구현은 대충 아래와 같습니다.

좌표축이 카메라이고, 바라볼 물체는 앞에 있습니다.
여기서 저 파란색 화살표(z축)를 앞으로 가져가면 어떻게 될까요?

바라볼 물체가 더 크게 보일 겁니다. 이런 방법을 통해 줌을 구현할 겁니다.
하지만 그 전에 Input System으로 먼저 휴대폰 터치에 대한 입력을 받아볼 겁니다.

1. Input System

인풋 시스템은 Unity 6가 되면서 기본적으로 탑재된 기능입니다. 기존의 레거시 기능은 멀티플랫폼에도 불편했고, 가독성도 그리 좋지 않았거든요.
Unity 6 버전으로 프로젝트를 파면

이런 친구가 보일 겁니다. 이 친구를 이용하여 저희는 휴대폰 터치를 구현해볼 겁니다.

제가 이 블로그를 쓰는 시점이 이미 다 만들고 쓰는 방식이라 약간 차이점이 있을 수 있습니다. 처음에는 Action Maps에 Player와 UI밖에 없을 겁니다.
저희는 딱히 플레이어도 없고, UI도 아니여서 Action Maps 옆에 있는 화살표를 눌러 ScreenTouch라고 액션 맵을 하나 만들어 줍시다.
Touch라고 하니까 이름 충돌이 되는 것 같아서 ScreenTouch라고 정의하였습니다.

액션 맵을 생성하면 Actions에 이렇게 달랑 하나가 있을 겁니다.

펼치면 이런 친구도 있을 거구요.
일단 저희는 입력을 두 개 받을 거기 때문에, Actions 옆에 있는 +를 눌러 액션을 하나 만들어 줍시다.

첫번째 터치 입력과 두번째 터치 입력을 받을 거니, 각 액션당 이름을 이런식으로 지어줬습니다.
유튜브에서도 이렇게 지어서 찾아보니 Contact는 접촉을 의미하는 것이더라구요. 아마 클릭했는 지를 알아보기 위해 이렇게 이름을 지은 것 같습니다.

각 액션을 클릭하면 Action Type(입력 종류)과 Interaction(상호작용)이 있습니다.
Action Type은 Value, Button, PassThrough 3개로, InputSystem은 너무 깊게 다루지 않을 예정이기에 간단하게 설명만 적어두겠습니다.

Value : 값을 지속적으로 받을 수 있음. 조이스틱이나 캐릭터 움직임에 자주 사용함.
Button : 누를 때 / 뗄 때마다 트리거처럼 작동함.
PassThrough : 바인딩된 컨트롤을 변경하면 해당 컨트롤 값으로 콜백이 트리거됨.

PassThrough는 써본 적이 없어서 이 분의 블로그를 참고하였습니다.
저의 경우에는 화면 터치를 하고 떼는 걸 알아야 하기 때문에 Button으로 설정하였습니다.

Interation은 상호작용을 뜻합니다. 종류는 Hold, Press, SlowTap, Tap이 있습니다.

Hold : Hold Time이라는 시간을 조정하여 일정 시간 이상 누르고 있어야 입력이 감지됨.
Press : 버튼을 눌렀을 때 감지함. PressPoint 이상이 되면 Performed상태로 변경됨.

SlowTap과 Tap은 아직 써본 적이 없어서 나중에 시간이 되면 업로드하도록 하겠습니다.
Interaction은 테스트를 다시 해봤는데 굳이 필요하지 않은 것 같아서 추가하지 않으셔도 되고, Action Type만 Button으로 설정해 주세요.

PrimaryTouchContact와 SecondaryTouchContact가 완성되었으면 PrimaryTouch, SecondaryTouch를 만들어 Action Type을 value로 설정해 주세요.

Control Type의 경우 얻는 값을 뜻합니다. PrimaryTouch는 Position을 얻을 거라 Any로 설정해 주시고, SecondaryTouch는 Vector2로 설정해 주세요.

모두 설정하였으면 Save Asset을 누르고 C# 파일을 생성해 줍니다.

2. 뉴인풋 SO

위에서 만들었던 Input System을 이용하여 좀 더 쉽게 사용하기 위해 SO화를 할 겁니다.
CameraInputSO라는 이름으로 ScriptableObject 스크립트 파일을 만들어주세요.

using System;
using UnityEngine;
using UnityEngine.InputSystem;

namespace JMT.InputSystem
{
    [CreateAssetMenu(menuName = "SO/Input/InputSO")]
    public class CameraInputSO : ScriptableObject, Controls.IScreenTouchActions
    {
        public event Action OnZoomStartEvent;
        public event Action OnZoomEndEvent;

        private Controls controls;

        private void OnEnable()
        {
            if (controls == null)
            {
                controls = new Controls();
                controls.ScreenTouch.SetCallbacks(this);
                controls.ScreenTouch.Enable();
            }
        }

        private void OnDisable()
        {
            controls.ScreenTouch.Disable();
        }

        public Vector2 GetPrimaryPosition() => controls.ScreenTouch.PrimaryTouch.ReadValue<Vector2>();
        public Vector2 GetSecondaryPosition() => controls.ScreenTouch.SecondaryTouch.ReadValue<Vector2>();

        public void OnPrimaryTouch(InputAction.CallbackContext context)
        {
        }

        public void OnSecondaryTouch(InputAction.CallbackContext context)
        {
        }

        public void OnSecondaryTouchContact(InputAction.CallbackContext context)
        {
            switch(context.phase)
            {
                case InputActionPhase.Started:
                    OnZoomStartEvent?.Invoke();
                    break;
                case InputActionPhase.Canceled:
                    OnZoomEndEvent?.Invoke();
                    break;
            }
        }

        public void OnPrimaryTouchContact(InputAction.CallbackContext context)
        {
        	// 현재는 필요하지 않습니다.
        }
    }
}

Action은 다른 코드에서 줌인/아웃을 하기 위해 만들었습니다. Action 앞에 Event를 붙이면 이 코드에서만 Invoke를 할 수 있어서 헷갈리지 않게 해줍니다.

OnEnable에서 new Controls(); 까지는 알겠는데, SetCallbacks와 Enable은 무엇일까 검색해 보았습니다.
SetCallbacks는 this(이 객체)에게 ScreenTouch라는 액션맵의 입력을 받게 해주는 것이고,
Enable은 this에게 정의된 맵을 활성화 해주는 친구입니다.
즉, ScreenTouch를 this에게 정의하고, 그걸 실행해 주는 과정이라고 보시면 될 것 같습니다.
Disable는 하지 않을 경우 에러가 발생하더라구요. 딱히 작동에 문제는 없는데, 빨간 에러는 보고싶지 않으니 해주시길 바랍니다.

OnSecondaryTouchContact()함수는 아까 Input System에서 해준 OnSecondaryTouchContact 친구가 작동할 때 실행되는 함수입니다.
context라는 친구가 상태를 나타냅니다. started, canceled, performed 가 있으며, 각각 시작할 때(GetKeyDown) / 계속 누르고 있을 때(GetKey) / 뗐을 때(GetKeyUp)를 나타냅니다.
누르기 시작했으면 OnZoomStartEvent를 호출하고, 화면에서 뗐으면 OnZoomEndEvent를 호출해줍니다.

이제 저 SO를 생성하고, 실질적으로 화면 줌/아웃을 만들어 보겠습니다.

SO를 생성한 후 CameraInput 스크립트도 생성합시다.

using JMT.InputSystem;
using System;
using System.Collections;
using UnityEngine;
using DG.Tweening;

namespace JMT.CameraSystem
{
    public class CameraInput : MonoBehaviour
    {
        [SerializeField] private float camSpeed = 4f;
        [SerializeField] private CameraInputSO inputSO;
        [SerializeField] private Transform camParentTrm;
        private Transform camTransform;
        private Coroutine zoomCoroutine;

        private void Awake()
        {
            camTransform = camParentTrm.GetChild(0);
            inputSO.OnZoomStartEvent += HandleZoomStartEvent;
            inputSO.OnZoomEndEvent += HandleZoomEndEvent;
        }

        private void HandleZoomStartEvent()
        {
            zoomCoroutine = StartCoroutine(ZoomDetection());
        }

        private void HandleZoomEndEvent()
        {
            StopCoroutine(zoomCoroutine);
        }

        private IEnumerator ZoomDetection()
        {
            float prevDistance = Vector2.Distance(inputSO.GetPrimaryPosition(), inputSO.GetSecondaryPosition());
            while (true)
            {
                float distance = Vector2.Distance(inputSO.GetPrimaryPosition(), inputSO.GetSecondaryPosition());
                float deltaDistance = distance - prevDistance;

                Vector3 targetPos = camTransform.localPosition;
                targetPos.z -= deltaDistance * camSpeed;
                targetPos.z = Mathf.Clamp(targetPos.z, 0.3f, 3f);

                camTransform.localPosition = new Vector3(camTransform.localPosition.x, camTransform.localPosition.y,
                                            Mathf.Lerp(camTransform.localPosition.z, targetPos.z, Time.deltaTime * camSpeed));

                prevDistance = distance;
                yield return null;
            }
        }
    }
}

camParentTrm은 제멋대로 한 거라 어떻게 바꿔도 상관이 없습니다. 중요한 건 줌인/아웃 코드라 ZoomDetection 함수만 참고하시면 될 것 같습니다.

prevDistance는 처음 위치입니다. SO에서 PrimaryPosition(첫번째 터치한 포지션)과 SecondaryPosition(두번째 터치한 포지션)을 가져와서 그 길이를 구합니다.
그리고 터치를 뗄 때까지 distance에 위와 같은 길이를 구하여 실시간으로 커지고 있는지 작아지고 있는지 구합니다.
작아지고 있으면 deltaDistance가 - 값이 나올 것이고, 아니라면 + 값이 나옵니다.
이 값을 뺌으로써 확대 축소를 하고, camSpeed를 곱해서 그만큼 속도를 냅니다.
이제 이 값을 Lerp로 부드럽게 처리해 줍니다. localPosition 대신 position을 쓰게 될 경우 나중에 회전을 할 때 문제가 발생하여 localPosition을 사용하였습니다.
그리고 그 distance를 prevDistance에 대입하여 그 다음에 확대가 잘 작동하도록 하였습니다.

코드를 작성하였으면 오브젝트를 파고 스크립트를 넣어줍니다.

캠 스피드는 5정도가 적당했고, 아까 만들었던 SO를 InputSO에 넣어주시면 잘 작동될 겁니다.

profile
경기게임마이스터고 4기

0개의 댓글