[Charon] #1. New Input System을 적용하여 플레이어 이동 구현하기

2022. 8. 17. 17:25Projects/Charon

 

 

 

리팩토링(Refactoring) 결정

 

오랜만에 다시 Unity 프로젝트 글을 올리게 되었네요. 4학년 1학기가 끝나 방학이 왔고, 잠시 휴식 기간을 가졌었습니다. 잠깐 쉬고 난 뒤에는 C++ 문법 공부, Python으로 백준 풀기, Unreal Engine5 배우는 등으로 시간을 보냈습니다. 그러다가 미루고 미루던 기존 카론(Charon) Unity 프로젝트를 슬슬 손 봐야겠다는 생각이 들더라구요.

 

기존에 작성했던 소스코드 구조는 너무 맘에 안 들었고, 이대로 가다가는 스파게티 코드가 되는 건 시간 문제라는 게 보였습니다. 그래서 어떻게 하지 하다가 결국 리팩토링을 하기로 결정했어요.

 

제일 먼저 작업을 시작하기로 한 건 입력 시스템이었습니다. 이전에 컴포넌트별로 클래스를 나눈 것까지는 좋았으나, 갈수록 왜 나누었는지 모르겠었던 구조라서 제일 먼저 바꾸기로 결심했습니다.

 

이번에는 Unity Engine에 새로 도입되었던 New Input System을 써보기 위해, 이틀 정도 공부를 했습니다.

솔직히 말해 내용이 좀 어렵더군요. 아직까지 영어로 된 레퍼런스를 읽는 것은 익숙하지 않기에 한글 번역도 같이 보며 공부하긴 했지만, 한글 번역은 그 한계 때문에 매끄러운 해석이 힘들었습니다.

(개발자에게 영어가 중요한 이유인 것 같네요.)

 

직접 써보며 실제로 적용해보는 게 더 빨리 실력이 늘겠다 싶어서, 그냥 바로 진행했습니다.ㅎㅎㅎ... 

 

 

 

1. New Input System 도입하기

 

Unity에서 기존에 노후화된 입력 시스템을 개선하기 위해, Unity 2019.1 버전부터 출시한 기능입니다. 기존 Old Input System은 플랫폼을 변경하여 게임을 출시하려면, 입력과 관련하여 작성했던 소스코드들을 다시 고쳐야 하는 번거로움이 있었지요. 또한 입력 키들이 늘어날수록 Update() 함수 크기가 방대해져 소스코드가 지저분해지는 문제점도 존재했습니다.

 

이런 문제점들에 대해 플랫폼 간 키 바인딩이 자유롭고, 실시간 입력 체크가 아닌 이벤트 기반 입력 체크 방식으로 개선한 것이 바로 New Input System입니다. 또한, 호환성을 위해 기존 레거시 입력 방식과 New Input System 방식을 둘 다 사용이 가능한 것도 장점입니다. 🔗New Input System에 대해 다루는 글은 나중에 따로 올리도록 할게요.

 

 

입력 시스템 기반 만들기

 

Input Actions는 프로젝트의 실제 에셋으로, 입력과 관련된 정보들을 하나로 담아 관리하는 파일입니다. 저는 "PlayerInputActions"라는 이름으로 하나 생성해주었습니다.

 

 

플레이어가 사용할 입력 키들을 정의해야 하므로 Actions Maps에 "Player"라는 이름으로 하나 생성해 주었습니다.

 

 

우리 게임은 WASD키로 캐릭터를 움직이므로, "Move"라는 Action을 생성하여 Action TypeValue로 지정해주었습니다. 캐릭터가 위로 움직이는 기능은 없기에 2차원 방향 정보만 알면 평면 위에서 캐릭터를 움직일 수 있습니다.

Control TypeVector2로 설정해줍니다.

 

 

바인딩(Binding)에 대해서는 다음과 같이 설정해주었습니다. 캐릭터가 8방향으로만 움직이면 되고, 대각선으로 이동할 때 더 빠르게 이동하면 안 되므로 ModeDigital Normalized로 설정해주었습니다. 그리고 각각 키들을 연결해주면 되겠지요.

 

 

우선 이동과 관련해서는 이 정도 세팅만 있으면 됩니다. 그리고 이 키 세팅들은 데스크탑 PC 플랫폼에서 사용할 것이므로 따로 스키마를 저장해주었습니다.

.

스키마

 

나중에 플랫폼이 점차 늘어나면 이런 식으로 하나씩 스키마를 늘려주고, 빌드할 때 원하는 플랫폼 스키마를 골라 빌드만 해주면 소스코드를 따로 건드릴 필요가 없습니다. New Input System에 대한 내용 설명을 생략해서 이해가 잘 안 가실 수도 있겠네요. 나중에 글을 따로 다루도록 하겠습니다. 궁금하신 분들은 🔗여기에 레퍼런스를 남겨 놓도록 할게요.

 

Auto-Save를 체크한 뒤, 창을 닫아줍니다.

 

 

그리고 플레이어 캐릭터 오브젝트에다가 Player Input 컴포넌트를 부착해줍니다. 그리고 Actions에는 위에서 만든 PlayerInputActions를 넣어주고, 나머지는 다음과 같이 세팅해줬습니다.

 

저는 Unity Event을 이벤트 기반으로 사용했습니다.

 

이제 소스코드를 작성할 차례네요!

 

 

 

2. 캐릭터 이동 구현하기

 

데이터와 관련된 내용들은 Player 스크립트에, 입력 및 컨트롤에 관한 내용들은 PlayerController 스크립트에 각각 분리하여 작성하였습니다.

 

Player 스크립트

 

플레이어는 최대 체력, 현재 체력, 방어력, 이동 속도, 연속 대시 가능 횟수 데이터들을 가집니다.

함부로 변경되면 안 되는 데이터들이기에 외부에서는 읽기만 가능하도록 읽기 전용 프로퍼티로 구성했습니다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Player : MonoBehaviour
{
    public float MaxHP     { get { return maxHP; } }
    public float CurrentHP { get { return currentHP; } }
    public float Armor     { get { return armor; } }
    public float MoveSpeed { get { return moveSpeed; } }
    public float DashCount { get { return dashCount; } }

    [SerializeField] protected float maxHP;
    [SerializeField] protected float currentHP;
    [SerializeField] protected float armor;
    [SerializeField] protected float moveSpeed;
    [SerializeField] protected float dashCount;

    public void OnUpdateStat(float maxHP, float currentHP, float armor, float moveSpeed, float dashCount)
    {
        this.maxHP = maxHP;
        this.currentHP = currentHP;
        this.armor = armor;
        this.moveSpeed = moveSpeed;
        this.dashCount = dashCount;
    }
}

 

원래라면 데이터베이스에서 사용자 플레이어 캐릭터 정보를 읽어 와서 로드를 해주어야 하지만, 지금은 그 기능이 없으므로 Inspector 창에서 편하게 테스트를 하기 위해 [SerializeField]를 사용했습니다. 그리고 나중에 해당 기능이 추가되었을 때 수정해주면 되겠지요.

 

 

PlayerController 스크립트와 이벤트 함수 등록

 

PlayerController 스크립트에서 이제 캐릭터 이동 기능을 구현해볼 겁니다. 그럴려면 우선 캐릭터의 이동 속도 데이터가 필요한데, 이것은 Player 스크립트에 존재합니다. 그래서 PlayerController 스크립트를 컴포넌트로 부착하면, 자동으로 Player 스크립트 또한 컴포넌트로 부착되도록 [RequireComonponent()]를 사용했습니다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;  // New Input System을 사용하기 위해 필요


[RequireComponent(typeof(Player))]
public class PlayerController : MonoBehaviour
{
    protected Player player;
    
    void Start()
    {
        player = GetComponent<Player>();
    }
   
   ...
}

 

이제 캐릭터 이동 입력과 관련하여, 입력 시스템에 등록할 함수를 하나 만들어야 합니다.

[RequireComponent(typeof(Player))]
public class PlayerController : MonoBehaviour
{
    public Vector3 direction { get; private set; }
    ...
    
    public void OnMoveInput(InputAction.CallbackContext context)
    {
        Vector2 input = context.ReadValue<Vector2>();
        direction = new Vector3(input.x, 0f, input.y);
    }
}

 

이제 이 OnMoveInput() 함수를 Player Input 컴포넌트에 이벤트 함수로 등록해야 합니다. Events 토글을 열어보면, 우리가 작성했던 Action Maps"Player"가 존재합니다. Player 토글을 열어보면, 우리가 작성했던 "Move" Action 이벤트가 존재하는 것을 볼 수 있습니다.

 

여기에 위에서 만들었던 함수를 등록해줍니다.

(Unity Event 방식은 등록할 함수의 접근한정자가 public이어야 Inspector 창에서 등록 가능)

 

 

이제 Move Action에 바인딩했던 입력 키들(WASD)를 누르면, 여기에 이벤트로 등록했던 OnMoveInput() 함수가 호출됩니다. 하나 아셔야 할 것은 눌렀을 때 한 번, 뗐을 때 한 번 이렇게 두 번 호출된다는 점입니다.

 

/*

예를 들어 왼쪽으로 가려고 A키를 눌렀다 떼면, (-1, 0), (0, 0) 이렇게 두 번 호출되는 것입니다.

이것은 바인딩 관련 이벤트(Started, Performed, Canceled 등)과 관련 있는데, 이것도 나중에 다루도록 하겠습니다.

*/

 

이제 입력 값들은 받을 수 있게 되었으니, 이 값을 토대로 캐릭터를 움직여 보았습니다. 물리 기반으로 움직일 것이므로 RigidBody 컴포넌트도 추가해 주었습니다.

protected const float CONVERT_UNIT_VALUE = 0.01f;

protected void Move()
{
    float currentMoveSpeed = player.MoveSpeed * CONVERT_UNIT_VALUE;
    LookAt();
    rigidBody.velocity = direction * currentMoveSpeed + Vector3.up * rigidBody.velocity.y;
}

protected void LookAt()   // 캐릭터가 이동하는 방향을 바라봄
{
    if(direction != Vector3.zero)
    {
        Quaternion targetAngle = Quaternion.LookRotation(direction);
        rigidBody.rotation = targetAngle;
    }
}

 

다만 이렇게 작성하고 실행하여 테스트 해보면, 키를 연타하여 눌러야 캐릭터가 조금씩 움직일 겁니다. 한 번 키를 누르고 있는다고 계속 호출되는 게 아니기 때문이지요. 그래서 FixedUpdate()Move() 함수를 넣어 해결해 주었습니다. 

void FixedUpdate()
{
    Move();
}

 

그리고 이동 속도와 관련된 코드들이 조금 이해가 안 되실 수도 있는데, 실제 기획서 상에 이동 속도 표가 다음과 같이 작성되어 있습니다.

 

 

이 수치가 어느 정도의 속도인지를 가늠할 수가 없어, 기획자와 얘기하며 실시간으로 값을 조정하며 맞추다 보니

기획서 이동 속도 300  →  Unity Rigidbody 속도 3 정도로 된다는 걸 알았습니다. 다만, 실제 UI 표시와 데이터베이스에 저장할 이동 속도는 기획서 이동 속도 규칙을 따라야 헷갈리지 않을 것이니, 그냥 코드 내부에서 변환 작업을 해준 것일 뿐입니다.

// 기획서 이동속도 300 -> 게임 월드 내 이동속도 3
protected const float CONVERT_UNIT_VALUE = 0.01f;

protected void Move()
{
    float currentMoveSpeed = player.MoveSpeed * CONVERT_UNIT_VALUE;
    ...
}

 

그리고 공중에 뜬 상태에서 플레이어 캐릭터가 천천히 내려오던 버그가 있었는데, RigidBody 속도의 y축 방향에도 힘을 가해주어야 올바르게 중력을 받았습니다.

rigidBody.velocity = direction * currentMoveSpeed + Vector3.up * rigidBody.velocity.y;

/* 2022-08-18 수정)
   이 코드를 그대로 사용하면 경사로 지형에서 문제가 발생하네요.
   추후 게시글에서 해결한 코드를 작성하였습니다.
*/

 

문제 해결 전

 

문제 해결 후

 

이제 실행 해보면 잘 움직이는 것을 볼 수 있습니다.

 

 

그리고 문제를 하나 해결해줘야 하는데, 벽에 붙어 이동하면 벽면에 달라붙어 움직이지 않습니다.

 

벽에 달라 붙어서 움직이지 못하는 상태

 

이것은 벽의 콜라이더와 캐릭터의 콜라이더 간 마찰력이 너무 세기 때문에 발생하는 문제입니다.

PhysicsMaterial을 하나 만들고, 다음과 같이 설정한 후 캐릭터 콜라이더에 넣어주었습니다.

 

 

 

그러면 이제 벽에 붙어서 이동해도 매끄럽게 잘 이동하는 것을 볼 수 있습니다.

 

마찰력 문제 해결

 

 

마무리

 

생각보다 글이 굉장히 길어졌네요. 이동 애니메이션까지 다루는 걸 작성하려고 했는데, 다음 글에서 작성해야 할 것 같습니다. 읽어주셔서 감사합니다!

 

 

728x90
반응형