Carrot
본문 바로가기
Unity/멋쟁이사자처럼 부트캠프

[멋쟁이사자처럼부트캠프] 유니티 게임 개발 5기(27일차) - 2D 플랫포머 게임(2) 조이스틱 & 버튼 UI 구현

by 독기품은토끼 2025. 6. 24.
✅ 오늘의 학습 목표
1. 조이스틱 구현
2. 인터랙션(점프/공격 버튼) 구현

 

1. Joystick

 

시작하기에 앞서 우선 조이스틱을 씬에 배치해 주자

이미지는 어제 유니티 스토어에서 다운로드 받은 Simple Input System 에셋에서 가져와주고,

마우스 상호작용시 필요한 Raycast Target을 체크해 준다.

 

1. Event Trigger

 

어제는 이벤트 트리거라는 컴포넌트를 추가해서 마우스의 동작을 확인했었지만 이걸 코드로도 확인할 수 있다.

 

using UnityEngine;
using UnityEngine.EventSystems;

public class JoystickController : MonoBehaviour, IPointerDownHandler, IPointerUpHandler, IDragHandler
{
    public void OnPointerDown(PointerEventData eventData)
    {
        Debug.Log("Pointer Down");
    }

    public void OnDrag(PointerEventData eventData)
    {
        Debug.Log("Drag");
    }

    public void OnPointerUp(PointerEventData eventData)
    {
        Debug.Log("Pointer Up");
    }
}

 

IPointerDownHandler, IPointerUpHandler, IDragHandler 이 세 가지 인터페이스를 상속받아 가져오면 되는데

실무에서는 컴포넌트 방식보단 이렇게 코드화하는 경우가 많다.

 

2. 조이스틱 포인터 위치

 

 

조이스틱의 움직임을 나타내보자

처음 위치와 움직이고 난 후의 위치를 변수로 가져온 후 활용해 주면 간단히 구현할 수 있다.

 

using UnityEngine;
using UnityEngine.EventSystems;

public class JoystickController : MonoBehaviour, IPointerDownHandler, IPointerUpHandler, IDragHandler
{
    [SerializeField] private GameObject backgroundUI;
    [SerializeField] private GameObject handlerUI;

    private Vector2 startPos, currPos;

    void Start()
    {
        backgroundUI.SetActive(false);
    }

    public void OnPointerDown(PointerEventData eventData)
    {
        backgroundUI.SetActive(true);
        backgroundUI.transform.position = eventData.position;
        startPos = eventData.position;
    }

    public void OnDrag(PointerEventData eventData)
    {
        handlerUI.transform.position = eventData.position;
        currPos = eventData.position;
        Vector2 dragDir = currPos - startPos;
    }

    public void OnPointerUp(PointerEventData eventData)
    {
        handlerUI.transform.localPosition = Vector2.zero; // 핸들러의 위치가 이상한 걸 잡음
        backgroundUI?.SetActive(false);
    }
}

 

이렇게 유니티에서 마우스 입력으로 작동하는 가상 조이스틱을 구현하였다.

사용자가 화면을 누르면 조이스틱 배경이 해당 위치에 나타나고, 드래그하는 동안 조이스틱 핸들러가 따라 움직이며 방향을 계산할 수 있게 된다.

 

그런데 지금 이 코드로는 핸들러가 배경 이미지 바깥까지 이동이 가능한 문제점이 있다.

 

public void OnDrag(PointerEventData eventData)
{
    currPos = eventData.position;
    Vector2 dragDir = currPos - startPos;

    float maxDist = Mathf.Min(dragDir.magnitude, 100f);
    handlerUI.transform.position = startPos + dragDir.normalized * maxDist;
}

 

dragDir.magnitude는 드래그한 방향 벡터의 실제 길이인데, 이 값이 너무 크면 조이스틱 배경 밖으로 벗어나게 된다.
그래서 Mathf.Min을 써서 최대 거리를 100으로 고정해 두고, 방향은 유지하되 거리만 조절해서

'startPos + 방향 * 제한된 거리' 식으로 핸들러 위치를 정해줬다.
이렇게 하면 조이스틱이 자연스럽게 움직이면서도 일정 범위 안에서만 동작하게 된다.

 

 

3. 플레이어 움직임과 조이스틱 연동

이제 조이스틱 움직임에 따라 플레이어 오브젝트도 같이 이동되도록 구현해 보자

// KnightController_Joystick 스크립트
// KnightController_Keyboard 복붙해서 InputKeyboard 구현 부분만 지웠음
public class KnightController_Joystick : MonoBehaviour
{
    public void InputJoystick(float x, float y)
    {
        inputDir = new Vector3(x, y, 0);
    }
}

// JoystickController 스크립트
public class JoystickController : MonoBehaviour, IPointerDownHandler, IPointerUpHandler, IDragHandler
{
    [SerializeField] private KnightController_Joystick knightController;

    public void OnDrag(PointerEventData eventData)
    {
        // 조이스틱 구현
        currPos = eventData.position;
        Vector2 dragDir = currPos - startPos;

        float maxDist = Mathf.Min(dragDir.magnitude, 100f);
        handlerUI.transform.position = startPos + dragDir.normalized * maxDist;

        // 조이스틱 값 플레이어 컨트롤러에 전달
        knightController.InputJoystick(dragDir.x, dragDir.y);
    }
}

 

JoystickController 스크립트에서 드래그 방향을 계산한 값을 KnightController_Joystick 스크립트에 넘겨주어

조이스틱 움직임에 따라 플레이어가 움직이도록 설정해 주었다.

 

4. 애니메이터 구현

 

아직 조이스틱용 애니메이션 변경 로직을 구현해주지 않았기 때문에 걷고 있어도 애니메이션은 여전히 가만히 있다.

 

 

새로운 애니메이터를 만들어준 후 Knight_Joystick 오브젝트에 넣어준다.

 

4.1. Blend 작업

조이스틱같이 방향 값이 민감할 경우 애니메이터의 Base Layer을 사용하기보단 Blend를 사용하는 것이 유리하다. (내 추측임ㅎ;)

음.. 맞말 같긴 한데 위 이유로 이번 실습에서 Blend를 사용해 주기보다는..

 

이 게임씬에 앉는 애니메이션, 앉아서 걷는 애니메이션 등 여러 애니메이션을 추가해 줄 건데

여태 우리는 걷고 뛰고 점프하는 2~3개 정도의 애니메이션만 사용했었기 때문에 애니메이터로 작업해도 큰 문제가 없었다.

그런데 이제 애니메이션이 많아지면 관리가 어렵고 충돌이 발생할 수도 있기 때문에 Blend를 사용해 주기로 했다.

 

 

Blend Tree 안에서는 float 값 여러 개를 기준으로 애니메이션을 섞을 수 있다.

Knight_Joystick 스크립트에 x와 y 값에 따른 움직임이 구현되어 있으니 float로 두 개의 파라미터를 생성해 주었다.

 

 

이제 파라미터 값에 따라서 다른 애니메이션이 적용되도록 설정해 줄 건데

2개의 파라미터를 선언하였으니 Blend Type을 2D Blend Tree로 변경해 주고,

Add Motion Field로 모션을 추가한 후 값에 따른 애니메이션을 설정해 주면 된다.

 

 

최종적으로 적용한 애니메이션이다.

부가적인 설명을 덧붙여보자면

Motion Pos X Pos Y 동작
Knight_Run 1 0 오른쪽으로 달리기
Knight_Idle 0 0 정지 상태
Knight_Run -1 0 왼쪽으로 달리기
Knight_Idle 0 1 위쪽 정지 상태
Knight_Idle 0 -1 아래쪽 정지 상태
(나중에 여기에 앉는 애니메이션 넣어줄 거임)

 

PosX는 캐릭터가 왼쪽(-1), 정지(0), 오른쪽(1) 중 어떤 방향으로 이동 중인지 표현하는 데 사용되고

PosY는 Blend Tree가 입력 벡터 (JoystickX, JoystickY)를 기반으로 2D 공간에서 가장 가까운 Motion을 찾는 과정에서 반드시 필요하기 때문에 설정해 주었다.

→ PosY가 실제로 위아래 이동이 없더라도 Motion들을 추가하지 않으면 Blend Tree 내부 계산에서 그 방향으로 가까운 애니메이션이 없다고 판단해서 해당 애니메이션들을 Blend에 사용하지 않게 되어 애니메이션이 작동되질 않는다.

= 블렌드 타입을 2D로 만들어주었기 때문에 필요하다

 

public void InputJoystick(float x, float y)
{
    inputDir = new Vector3(x, y, 0).normalized;

    // 애니메이터 파라미터에 값 전달 => 애니메이션 동작
    animator.SetFloat("JoystickX", inputDir.x);
    animator.SetFloat("JoystickY", inputDir.y);

    // Flip
    if (inputDir.x != 0)
    {
        var scaleX = inputDir.x > 0 ? 1 : -1;
        transform.localScale = new Vector3(scaleX, 1, 1);
    }
}

 

애니메이터에 파라미터 값을 전달해 주어서 Blend가 이 값을 토대로 애니메이션이 동작되도록 구현해 주었다.

 

2. Interactions

조이스틱으로 움직임을 구현했다면 이번에는 점프 같은 부가 기능을 구현해 보자

 

키보드에서는 스페이스바를 누르면 점프되도록 했는데, 조이스틱에는 점프가 없으니 버튼 UI를 통해 점프 기능이 작동되도록 구현해 줄 것이다.

 

using UnityEngine;
using UnityEngine.UI;

public class KnightController_Joystick : MonoBehaviour
{
    private Animator animator;
    private Rigidbody2D knightRb;

    [SerializeField] private Button jumpButton;

    private Vector3 inputDir;
    [SerializeField] private float moveSpeed = 3f;
    [SerializeField] private float jumpPower = 13f;

    private bool isGround;

    void Start()
    {
        animator = GetComponent<Animator>();
        knightRb = GetComponent<Rigidbody2D>();

        jumpButton.onClick.AddListener(Jump);
    }

    void FixedUpdate()
    {
        Move();
    }

    void OnCollisionEnter2D(Collision2D other)
    {
        if (other.gameObject.CompareTag("Ground"))
        {
            animator.SetBool("[Bool] IsGround", true);
            isGround = true;
        }
    }

    private void OnCollisionExit2D(Collision2D other)
    {
        if (other.gameObject.CompareTag("Ground"))
        {
            animator.SetBool("[Bool] IsGround", false);
            isGround = false;
        }
    }

    public void InputJoystick(float x, float y)
    {
        inputDir = new Vector3(x, y, 0).normalized;

        // 애니메이터 파라미터에 값 전달 => 애니메이션 동작
        animator.SetFloat("JoystickX", inputDir.x);
        animator.SetFloat("JoystickY", inputDir.y);

        // Flip
        if (inputDir.x != 0)
        {
            var scaleX = inputDir.x > 0 ? 1 : -1;
            transform.localScale = new Vector3(scaleX, 1, 1);
        }
    }

    void Move()
    {
        if (inputDir.x != 0)
            knightRb.linearVelocityX = inputDir.x * moveSpeed;
    }

    void Jump()
    {
        if (isGround)
        {
            animator.SetTrigger("[Trigger] Jump");
            knightRb.AddForceY(jumpPower, ForceMode2D.Impulse);
        }
    }
}

 

중복된 부분은 수정해 주고 Button UI를 갖고 와서 점프 기능이 동작되도록 구현해 주었다.

 

 

 

3. 신규 애니메이션

1. 앉기

1.1. 애니메이션

 

우선 앉기 애니메이션과 앉아서 걷는 애니메이션 2가지를 추가해 준다.

 

1.2. Blend 작업

 

유니티는 진짜 천재다 이런 생각을 어캐했지... 난 창의력 바닥!!ㅠㅠ

 

지금 보면 캐릭터가 움직이고 있는데 애니메이션은 Idle 상태가 유지돼서 Blend에 모션을 좀 더 추가해 줬다 (이하 생략^^!)

 

2. 공격

2.1. 이미지 슬라이스

 

공격 이미지의 경우 셀마다 이미지의 크기가 다르기 때문에 이 상태로 애니메이션을 실행하면 뒤로 밀리는 듯한 느낌을 받는다.

이럴 때에는 이미지를 오토매틱으로 자르지 말고 셀 카운트로 잘라주면 된다.

 

2.2. 애니메이터

 

2.3. 버튼 UI 추가

public class KnightController_Joystick : MonoBehaviour
{
    [SerializeField] private Button attackButton;

    void Start()
    {
        animator = GetComponent<Animator>();
        knightRb = GetComponent<Rigidbody2D>();

        jumpButton.onClick.AddListener(Jump);
        attackButton.onClick.AddListener(Attack);
    }

    void Attack()
    {
        animator.SetTrigger("[Trigger] Attack");
    }
}

 

3. 공격 콤보

 

공격 버튼을 0.5초 내에 한번 더 클릭하면 Combo 공격을 수행하도록 애니메이션을 추가해 줄 것이다.

 

3.1. 애니메이션 이벤트

 

우선 콤보 애니메이션 생성해 주고 로직 연결해 주고..

 

 

애니메이션이 실행하는 동안 버튼을 한번 더 클릭하면 콤보 애니메이션이 실행되도록 구현해 줄 것이기 때문에 애니메이션 이벤트를 추가해 주었다. (이미지에는 0:05에 이벤트를 추가해 주었는데 0:03으로 옮겨줌)

 

public class KnightController_Joystick : MonoBehaviour
{
    // bool 기본값 = false
    private bool isAttack;
    private bool isCombo;

    void Attack()
    {
        if (!isAttack)
        {
            // 중복 공격 막기 & 콤보 체크용
            isAttack = true;
            animator.SetTrigger("[Trigger] Attack");
        }
        else
        {
            Debug.Log("콤보 호출");
            isCombo = true;            
        }
    }

    public void CheckCombo()
    {
        if (isCombo)
        {
            Debug.Log("콤보 실행");
            animator.SetBool("[Bool] IsCombo", true);
        }
        else
        {
            animator.SetBool("[Bool] IsCombo", false);
            isAttack = false;
        }
    }
}

 

파리미터를 통해서 공격과 콤보가 수행되도록 코드를 구현해 주었는데

첫 공격은 isAttack이 false라 수행되지만 그 상태에서 콤보 공격 수행을 위해 한번 더 클릭하면 isAttack이 true라 isCombo = true만 수행된다.

그래서 CheckCombo() 메서드의 isCombo가 true인 경우인 코드만 실행되기 때문에 isAttack = true 상태가 유지된다.

그래서 Attack()에서 if (!isAttack) 조건을 통과하지 못해서 두 번째부터 공격이 발동되지 않는다

 

public void EndCombo()
{
    // Debug.Log("콤보 종료");
    isAttack = false;
    isCombo = false;
}

 

isAttack이 false가 되도록 메서드를 하나 더 생성해 주고 해당 메서드를 애니메이션 이벤트로 추가해 주면 해결된다.

 

 

근데 이 방법으로 해주면 빠르게 타닥 누르면 콤보가 발생하긴 하는뎁..  중복 공격이 방지가 안되기 때문에.. 조금만 천천히 눌러도 1단 공격이 빠르게 2번 실행된다..

그래서 애니메이션 이벤트 위치를 조절해줘 봤는데 크게 달라지는 건 없어서 조건문을 달아줘야 하나 싶다

아마 내일 보완하실 것 같아서 예습은 하지 않겠다..

실은 EndCombo도 쉬는시간에 혼자 구현했는데 갑자기 회고시간에 수업 나가셔서 앗 걍 존버할걸ㅋ 이랬음ㅋㅋㅋㅠ 에라이

 

 


 

 

 

쿠쿠.. 오늘 조이스틱 구현해 보니까 넘 잼따

처음 해보는 Blend도 넘 신박하고 역시 이래서 천재들이 Unity같은 프로그램을 만드는 건가 싶었음..

이런 기능 잘 활용해 보면 VR 게임도 만들 수 있겠구.. 메타퀘스트 참 잼게했었는데 그립다

 

그리고 오늘부터 회고팀 정해진다 했는데 흠 머지 5시인데 아직도 회고팀 안 알려주심 ㅇㅅㅇ 내가 잘 못 들었나

암튼 오늘 기록 끝!