본문 바로가기
개발/Unity 내일배움캠프 TIL

Unity2D 플레이어 캐릭터 이동 구현하기

by 석시 2023. 9. 5.



플레이어 캐릭터 이동 구현에 대한 글이다.

다양한 방법으로 구현해볼 수 있겠지만

총 세 가지 방법에 대해 다뤄보도록 하겠다.

1. Input.GetAxis

수평, 수직 키 값을 입력받아 추출해주는 함수이다.

GetAxis

public class TopDownCharacterController : MonoBehaviour
{
    [SerializeField] private float speed = 5f;

    void Update()
    {
        float x = Input.GetAxis("Horizontal");
        float y = Input.GetAxis("Vertical");

        transform.position += new Vector3(x, y) * speed * Time.deltaTime;
    }
}

“Horizental”을 넣어주면 가로 이동 (왼쪽, 오른쪽) 의 값을, ”Vertical”을 넣어주면 세로 이동 (위, 아래) 의 값을 반환한다.

왼쪽은 -1f, 오른쪽은 1f를 반환하는데 살짝 특이한 것이, 반환값이 float이라는 것이다.

GetAxis로 입력을 받아올 때 오른쪽을 입력해주면, 처음부터 1을 받아오는 것이 아니라 어느정도의 시간 간격을 두고 0f에서 1f로 서서히 변하는 것이다.

우리가 평소 캐릭터의 이동을 생각해보면 시작부터 정해진 속도로 가는 것이 아니라, 최대 속도까지 일정하게 가속이 붙는 경우가 많지 않은가.

이러한 부드러운 이동을 위해 서서히 증가하는 float 값을 반환해준다.

시작부터 1f의 속도를 가지는 것이 아니라 이동 시작 시 가속 과정을 거친다. 멈출 때도 브레이크가 걸리듯이 감속을 거친 후 이동을 중지한다.

GetAxisRaw

처음부터 1f의 속도를 가지도록 구현하고 싶을 수도 있겠다.

그럴 때 사용하는 것이 바로 GetAxisRaw이다.

public class TopDownCharacterController : MonoBehaviour
{
    [SerializeField] private float speed = 5f;

    void Update()
    {
        float x = Input.GetAxisRaw("Horizontal");
        float y = Input.GetAxisRaw("Vertical");

        transform.position += new Vector3(x, y) * speed * Time.deltaTime;
    }
}

여전히 float을 반환해주는 건 똑같지만, 부드러운 이동이 아닌 시작부터 바로 1f의 값을 반환해준다.

약간 귀신같이 움직인다. (어떻게 지평좌표계로 고정을 하셨죠?)

Input Manager

Input.GetAxis를 사용할 때 이러한 부드러운 이동의 가속과 감속 정도를 조절하고 싶다거나, WASD or 화살표 조작이 아닌 다른 키를 이동할 때 쓰고 싶은 경우가 있을 것이다.

이때 GetAxis의 프로퍼티를 따로 바꿔줄 수가 있다.

메뉴: Edit → Project Settings → Input Manager

이곳에서 가속 값이나 키 설정을 바꿔줄 수 있다.

심지어 나만의 Axis를 추가하여 새롭게 정의할 수도 있다.


2. Input.GetKey

아예 GetKey로 다이렉트로 키 입력을 받아올 수도 있다.

하지만 추천하지는 않는 방식이다.

public class TopDownCharacterController : MonoBehaviour
{
    private Rigidbody2D playerRigidbody;
    [SerializeField] private float speed = 5f;

    private void Awake()
    {
        playerRigidbody = GetComponent<Rigidbody2D>();
    }

    void Update()
    {
        if (Input.GetKey(KeyCode.UpArrow) == true)
        {
            playerRigidbody.AddForce(new Vector2(0f, speed));
        }

        if (Input.GetKey(KeyCode.DownArrow) == true)
        {
            playerRigidbody.AddForce(new Vector2(0f, -speed));
        }

        if (Input.GetKey(KeyCode.RightArrow) == true)
        {
            playerRigidbody.AddForce(new Vector2(speed, 0f));
        }

        if (Input.GetKey(KeyCode.LeftArrow) == true)
        {
            playerRigidbody.AddForce(new Vector2(-speed, 0f));
        }

    }
}

AddForce로 움직임을 구현했는데, 이 역시 추천하지 않는다.

의도한 대로 동작하지 않을 확률이 높으며, 어떤 식으로 물리가 동작하는 지 추적하기도 힘들다.

얼음판 위를 미끄러지는 느낌. 심지어 키를 떼도 멈추지 않는다.

만약 Transform의 변수들을 직접 조작하는 것이 아닌 메서드를 이용하여 캐릭터의 위치를 변경하고 싶다면 Transform.Translate를 적극 사용하도록 하자.


3. Input System 패키지와 event 활용

마지막 방법이자 오늘의 결론.

앞에서 소개했던 방식들은 특정한 키나 특정한 axis만 처리할 수 있기 때문에 직관성이 떨어진다.

따라서 C#의 event 키워드를 활용하여 콜백 함수를 불러오자.

Input System 패키지 설치

사용하기 전에 먼저 Input System 패키지를 설치해줘야 한다.

메뉴: Window → Package Manager

Input System 패키지가 보이지 않으면 상단의 PackagesIn Project에서 Unity Registry로 바꿔주면 보인다.

Install해주고 재시작 창이 뜨면 유니티를 재시작해주자.

Input Actions 오브젝트 생성

이 후에는 Create 메뉴에 Input Actions라는 새로운 오브젝트를 만들 수 있다.

새로 만든 Input Actions

Input Actions 편집

더블클릭을 하면 애니메이션처럼 새로운 편집창이 뜨게 된다.

이게 초기 상태이다.

Control Scheme이 없기 때문에 새로 추가해주자.

Control Scheme의 이름을 짓고 List에 Keyboard와 Mouse까지 추가해주자.

만약 JoyStick의 Control Scheme이 필요하다면 해당 방식의 Control Scheme을 새로 만들면 되는 것이다.

이제 Action Maps+ 버튼을 눌러 새로운 Action map을 추가해주자.

이제 Actions를 수정해주자.

Action Properties에서 Action TypeValue로 바꾸고 Control TypeVector 2로 해주자.

그러고 Action에 바인딩을 추가할 수 있다.

새로 만든 액션 옆의 + 버튼을 누르면 Add Up\Down\Left\Right Composite이라는 것이 있다.

추가한 바인딩에 키를 매핑해주자.

완성한 Actions.

여기서 WASD말고 화살표로도 조작받고 싶으면 어떻게 하나요? 할 수 있는데 Action을 한 개 더 추가해서 똑같은 방식으로 만들고 키매핑만 화살표로 해주면 된다.

움직임 뿐만 아니라 여러 조작에 대한 기능을 구현할 수 있다.

마우스가 있는 방향을 바라본다던가, 좌클릭을 인식하면 들고 있는 총을 쏜다던가 하는 등의 기능 말이다.

Input Actions를 편집한 뒤에 Save Asset을 눌러 저장해주자.

스크립트 작성

키 입력 받는 스크립트 작성

이제 event와 Action을 활용하는 코드를 작성할 것이다.

eventAction은 C#의 키워드인데, 자세한 내용은 다음의 글을 참고하자.

[내일배움단] 사전캠프 C# TIL 8. 알아두면 좋을 것들
Generic참고) object 타입Generic의 기본 구조InterfaceabstractinterfaceProperty자동구현 Propertydelegate콜백 (Callback)대리자(Delegate)델리게이트 체이닝(Delegate Chaining)EventLambda무명 메서드 (Anonymous Function)람다의 형식람다 일반화 (Action, Func)Exception다양한 Exception 사용 예제ReflectionAttributeNullable C#에서 필요한 잡다한 것들을 정리해놓으려고 한다. Generic 일반화를 위한 기능이다. 특정 자료구조를 내가 직접 한 번 만든다고 해보자.자료구조니까 모든 자료형을 받을 수 있어야 할텐데, 이를 직접 구현한다 생각하면 쉽지 않아 보인다...
https://seoksii.tistory.com/10#eb70570a-a32b-430f-9e5a-25c26309efc8

두 개의 스크립트를 생성하여 다음과 같이 작성해주자.

TopDownCharacterController.cs

public class TopDownCharacterController : MonoBehaviour
{
    // event 외부에서는 호출하지 못하게 막는다
    public event Action<Vector2> OnMoveEvent;

    public void CallMoveEvent(Vector2 direction)
    {
        OnMoveEvent?.Invoke(direction);
    }
}

PlayerInputController.cs

public class PlayerInputController : TopDownCharacterController
{
    public void OnMove(InputValue value)
    {
        Vector2 moveInput = value.Get<Vector2>().normalized;
        CallMoveEvent(moveInput);
    }
}

OnMove라고 함수를 작성하는데, 이는 우리가 Input Actions에서 추가해준 Action의 이름이 Move라서 그렇다.

조금 있다 Input Actions를 컴포넌트로 추가할 때 더 자세히 설명하겠다.

moveInput으로 넣어주는 Vector2normalized 해주는데, 그 이유는 WD를 동시에 입력했을 때 Vector2(1, 1)이다.

이 경우 크기가 1보다 크기 때문에 대각선으로 이동할 때는 속도가 더 빨라지는 증상이 발생한다.

따라서 Vector2의 크기를 1로 만들어주기 위해 normalized라는 키워드를 붙여주게 된다.

코드의 구조를 조금 자세히 설명하자면, WASD의 입력이 있을 때마다 Input Actions에 Action을 추가해놓은대로 OnMove 함수의 호출이 된다.

이 때 OnMove 함수 안에서 OnMoveEvent라는 event Action을 인보크해주는 CallMoveEvent를 호출해주면 이 의미는 잘 생각해보면

WASD의 입력이 감지될 때마다 OnMoveEvent가 발생하는 것이다!

이러면 이제 움직임에 필요한 추가적인 작업이 있다면 모두 OnMoveEvent에다가 +=로 넣어주기만 하면 된다.

참고) event를 사용했을 때의 이점

이러한 방식은 코드의 기능 분리가 매우 깔끔해진다는 점이다.

다른 스크립트에서 캐릭터가 움직일 때만 호출되어야 하는 함수가 있다고 해보자.

event를 사용하지 않았을 때는 캐릭터의 움직임을 알기 위한 정보를 해당 스크립트에 일일히 다 작성해야 한다.

하지만 이런식으로 움직임을 관장하는 스크립트의 event에 내가 호출하고자 하는 다른 스크립트의 함수를 더해줘버리기만 한다면?

아무리 많은 스크립트에서 움직임 감지가 필요하다고 하더라도 그냥 해당 함수를 event에 더해줘 버리면 된다.

마치 유튜브의 구독과도 같다.

그러고 나선 Move가 감지될 때마다 event에 구독된 함수들이 모조리 호출되는 것이다.

이러한 디자인 패턴을 옵저버 패턴이라고 한다.

자세히 다룰 일이 있다면 추가로 게시글을 작성하도록 하겠다.

Entity 이동 스크립트 작성

지금까지 키 입력을 받는 부분을 구현했으니 실제로 오브젝트가 이동하는 스크립트를 작성해야한다.

다음과 같이 스크립트를 생성하여 코드를 작성해주자.

TopDownMovement.cs

public class TopDownMovement : MonoBehaviour
{
    private TopDownCharacterController _controller;

    private Vector2 _movementDirection = Vector2.zero;
    private Rigidbody2D _rigidbody;

    private void Awake()
    {
        _controller = GetComponent<TopDownCharacterController>();
        _rigidbody = GetComponent<Rigidbody2D>();
    }

    private void Start()
    {
        _controller.OnMoveEvent += Move;
    }

    private void FixedUpdate()
    {
        ApplyMovement(_movementDirection);
    }

    private void Move(Vector2 direction)
    {
        _movementDirection = direction;
    }

    private void ApplyMovement(Vector2 direction)
    {
        direction = direction * 5;

        _rigidbody.velocity = direction;
    }
}

오브젝트를 움직이는 함수 Move를 WASD 키가 감지되었을 때 트리거되는 OnMoveEvent에 구독을 해준 것이다.

그림으로 표현하자면 다음과 같다.

컴포넌트 추가

이제 Player 오브젝트에서 지금까지 만든 컴포넌트들을 추가해주자.

움직이려는 오브젝트에 내가 만든 스크립트 PlayerInputController.cs 추가
움직이려는 오브젝트에 Player Input 컴포넌트 추가 → Actions에 내가 만든 Input Actions 드래그 앤 드롭

인스펙터를 자세히 보면 호출 가능한 이벤트 함수 목록이 있는데, 우리가 Move라는 이름의 액션을 추가해줬기 때문에 OnMove가 호출 가능한 모습이다.

앞서 언급한 방법처럼 LookFire를 추가해준다면 마찬가지로 OnLookOnFire 함수 역시 호출 가능하도록 추가되는 것을 확인할 수 있다.

그 다음 실제로 캐릭터를 움직여주는 부분을 만들자.

움직이려는 오브젝트에 TopDownMovement.cs와 Rigidbody2D 추가

참고로 4방향의 움직임을 구현 중이기 때문에 Rigidbody2D에서 Gravity Scale0으로 만들어주자.

값이 있으면 게임 실행시 캐릭터가 아래로 뚝 떨어진다.

이제 완성이다!


지금까지 기존에 주로 유니티에서 이동을 구현하던 방법과 Input System과 event를 활용한 이동 구현 방법이었다.

Input System과 event 모두 유지 보수나 기능 변경이 매우 편리한 구조이기 때문에 앞으로 이동 구현이 필요할 때면 해당 게시글을 적절히 참고하는 것이 좋겠다.


Uploaded by N2T