본문 바로가기
게임/Unity

[Unity 2D] 조이스틱을 이용한 부드러운 캐릭터 이동

by 신인용 2020. 2. 13.
반응형

조이스틱을 이용한 부드러운 캐릭터 이동

 

 

 오늘은 조이스틱 조종을 통해 캐릭터가 부드럽게 회전하며 이동하는 것을 구현하도록 하겠습니다.

 

 

 오늘 할 것은, 아래의 조이스틱 패드를 조종(터치 및 드래그)하여 캐릭터를 움직이는 것입니다.

제가 선택한 네모난 영역이 사용자가 터치할 영역이고, Inner Pad(안쪽 검은 원)는 사용자가 터치한 곳으로 움직일 것인데, Outer Pad(바깥 원)의 선까지만 움직이게 구현할 것입니다. 이와 동시에 조이스틱 방향으로 캐릭터가 부드럽게 회전하며 이동할 것입니다.

 

 

결과물 미리보기

 

 

 

 

 

 

 

 

 

0. 기본 셋팅

 화면비율은 9:16으로 해주었고,

 Character는 Sprite로, Rigidbody 2D를 추가하고 Gravity Scale을 0을 줍니다. 

 Joystick Touch Area는 Image로, 알파값을 0으로 설정해줍니다.

 

 

 

 

 이제, 스크립트를 작성하도록 하겠습니다. 

 조이스틱은 유니티엔진의 EventSystmes에 구현되어 있는 IDragHandler, IPointerDownHandler, IPointerUpHandler 인터페이스를 이용하여 구현할 것입니다.

 플레이어는 플레이어가 이동할 방향을 조이스틱에서 넘겨받은 벡터 방향으로 이동을 구현할 것입니다.

 

 

1. Joystick

 Joystick 스크립트를 Joystick Touch Area에 추가해주었습니다.

 

 

 

 

 UI를 다루기 위한 UnityEngine.UI를 불러오고

터치이벤트를 다루기 위해 UnityEngine.EventSystems을 불러옵니다.

 

 

 

변수 선언 부분과 Inspector창.

 

 

 

 

 

 그리고 UnityEngine.EventSystems에 있는 IDragHandler 인터페이스, IPointerDownHandler 인터페이스, IPointerUpHandler 인터페이스를 상속받아줍니다.

 

 함수설명을 하자면,

OnDrag는 사용자가 "드래그 할 때" 작동합니다. 터치를 시작하는 순간에는 작동하지 않습니다.

OnPointerDown은 "터치를 시작할 때" 작동합니다.

OnPointerUp은 "터치를 종료할 때" 작동합니다.

 

 매개변수인 PointerEventData에는 터치위치, 드래그 여부, 카메라, 클릭횟수, 클릭시간, 마우스 클릭정보(Left, Right, Middle) 등등의 터치정보가 담겨있습니다.

 

 

 

(Joystick Touch Area의 Image 범위)

 

 이제 Joystick Touch Area에 Image의 터치정보를 처리할 수 있게 되었습니다.

 

 

 

 

 

 OnDrag 함수의 내용 중, RectTransformUtility.ScreenPointToLocalPointRectangle라는 API에 대해 알아보겠습니다.

 

 public static bool ScreenPointToLocalPointInRectangle(RectTransform rect, Vector2 screenPoint, Camera cam, out  Vector2 localPoint);

 

 이 API는 rect안의 영역에서 터치가 발생하면 Camera(Default: Main Camera) Screen 좌표를 rect의 local 좌표로 바꾸어 줍니다.

 반환형은 bool형이며, rect안의 영역에서 터치가 발생한다면 true를 반환하게 됩니다. 

 

 그래서 바꾸어진 Local 터치좌표를 이용하여 조이스틱 벡터를 단위벡터로 계산하고, Character에게 벡터를 넘기고, innerPad의 이미지를 그려주었습니다.

 

 

 

 

 

 터치가 시작되면 OnDrag처리를 해주었고, 터치가 종료되면 innerPad의 위치를 원위치 시켜주었습니다.

 

 

 

 

 

 

 

 

2. Player

 

 TurnAngle함수는 조이스틱의 벡터를 받아와, 캐릭터의 방향과 조이스틱 벡터 사이의 각도를 계산합니다. 그 후 RotateAngle이라는 코루틴을 호출합니다.

 

 

 

 

 

 

 

 

 

 

 

angle : 캐릭터의 방향과 조이스틱 방향 사이의 각도

여기서 Vector3.Angle은 0도~180도만 표현하는데, 저희가 필요한 건 360도입니다. 그래서 sign을 통해 값을 -180 ~ 180로 만들어 주었습니다. (Cross에 대한 설명은 나중에 알아보도록 하겠습니다...)

(-180 ~ 180도로 만드는 것은 RotateAngle 코루틴에서 실행.)

 

 

 

 RotateAngle 코루틴은 angle을 rotateSpeed만큼 0.01초 간격으로 회전시키는 코루틴입니다. 이 코루틴을 적용하면 부드러운 회전이 적용됩니다.

 그리고 for문에서 rotateSpeed만큼 i가 증가하기 때문에, 마지막에 남은 각도를 마저 회전시켜 주었습니다.

ex) angle은 124도인데 rotateSpeed가 5라면, 4도는 회전하지 않게 됩니다. 그래서 angle을 rotateSpeed만큼 나눈 것의 나머지를 구해 마저 회전시켜줘야 합니다.

 

 

 

 Start문에서 180도 회전을 시켜주었습니다.

 

 회전은 조이스틱을 이용했고, 이동하는 것은 Update문에서 계속 앞으로 나아가게 해주었습니다.

 

 

 

 

 

3. 결과

 

 

 이렇게 조이스틱을 이용해 캐릭터의 부드러운 회전과 함께 이동을 구현하였습니다.

 

 

 

 

4. 전체 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
 
public class Joystick : MonoBehaviour, IDragHandler, IPointerDownHandler, IPointerUpHandler
{
    public GameObject character; // 캐릭터 오브젝트.
    public RectTransform touchArea; // Joystick Touch Area 이미지의 RectTransform.
    public Image outerPad; // OuterPad 이미지.
    public Image innerPad; // InnerPad 이미지.
 
    private Vector2 joystickVector; // 조이스틱의 방향벡터이자 플레이어에게 넘길 방향정보.
 
    private float speed = 3f; // 캐릭터 스피드
    private float rotateSpeed = 5f; // 회전 속도
 
    private Coroutine runningCoroutine; // 부드러운 회전 코루틴
 
 
    void Start()
    {
        runningCoroutine = StartCoroutine(RotateAngle(180-1));
        // 시작하면 charactor를 180도 오른쪽으로 회전
    }
 
 
    void Update()
    {
        character.GetComponent<Rigidbody2D>().velocity = character.transform.up * speed;
        // 캐릭터는 3의 속도로 계속 전진
    }
 
 
    public void OnDrag(PointerEventData eventData)
    {
        if(RectTransformUtility.ScreenPointToLocalPointInRectangle(touchArea, 
            eventData.position, eventData.pressEventCamera, out Vector2 localPoint))
        {
            localPoint.x = (localPoint.x / touchArea.sizeDelta.x);
            localPoint.y = (localPoint.y / touchArea.sizeDelta.y);
            // Joystick Touch Area의 비율 구하기 ( -0.5 ~ 0.5 )
 
            joystickVector = new Vector2(localPoint.x * 2.6f, localPoint.y * 2);
            // 조이스틱 벡터 조절 (2.6과 2를 곱해준 것은 TouchArea의 비율 때문임)
 
            TurnAngle(joystickVector); 
            // Character에게 조이스틱 방향 넘기기
 
            joystickVector = (joystickVector.magnitude > 0.35f) ? joystickVector.normalized * 0.35f : joystickVector;
            // innerPad 이미지가 outerPad를 넘어간다면 위치 조절해주기
 
            innerPad.rectTransform.anchoredPosition = new Vector2(joystickVector.x * (outerPad.rectTransform.sizeDelta.x),
                joystickVector.y * (outerPad.rectTransform.sizeDelta.y));
            // innerPad 이미지 터치한 곳으로 옮기기
        }
    }
 
 
    public void OnPointerDown(PointerEventData eventData)
    {
        OnDrag(eventData); // 터치가 시작되면 OnDrag 처리.
    }
 
 
    public void OnPointerUp(PointerEventData eventData)
    {
        innerPad.rectTransform.anchoredPosition = Vector2.zero;
    }
 
 
    private void TurnAngle(Vector3 currentJoystickVec)
    {
        Vector3 originJoystickVec = character.transform.up;
        // character가 바라보고 있는 벡터
 
        float angle = Vector3.Angle(currentJoystickVec, originJoystickVec);
        int sign = (Vector3.Cross(currentJoystickVec, originJoystickVec).z > 0) ? -1 : 1;
        // angle: 현재 바라보고 있는 벡터와, 조이스틱 방향 벡터 사이의 각도
        // sign: character가 바라보는 방향 기준으로, 왼쪽:+ 오른쪽:-
 
        if (runningCoroutine != null)
        {
            StopCoroutine(runningCoroutine);
        }
        runningCoroutine = StartCoroutine(RotateAngle(angle, sign));
        // 코루틴이 실행중이면 실행 중인 코루틴 중단 후 코루틴 실행 
        // 코루틴이 한 개만 존재하도록.
        // => 회전 중에 새로운 회전이 들어왔을 경우, 회전 중이던 것을 멈추고 새로운 회전을 함.
    }
 
 
    IEnumerator RotateAngle(float angle, int sign)
    {
        float mod = angle % rotateSpeed; // 남은 각도 계산
        for (float i = mod; i < angle; i += rotateSpeed)
        {
            character.transform.Rotate(00, sign * rotateSpeed); // 캐릭터 rotateSpeed만큼 회전
            yield return new WaitForSeconds(0.01f); // 0.01초 대기
        }
        character.transform.Rotate(00, sign * mod); // 남은 각도 회전
    }
}
cs

 

 

 

 

 

 

 

 

 

(참고)

https://tenlie10.tistory.com/115  (조이스틱)

https://docs.unity3d.com/ScriptReference/RectTransformUtility.ScreenPointToLocalPointInRectangle.html  (API)

 

반응형

댓글