안녕하세요. 오늘은 키보드를 입력하면 Player가 움직이도록 만들어보겠습니다.
Unity InputSystem Package 설치
Unity에서는 InputSystem는 마우스, 키보드 그리고 터치처럼 사용자의 입력을 처리할 때 사용하는 기술입니다. 그런데 2019년에 새로운 InputSystem이 발표했다고 해요. 처음 프로젝트를 생성했을 때는 기존 InputSystem이 설치되어 있고 추가적으로 PackageManager를 통해 새로운 InputSystem을 설치할 수 있다고 합니다.
새로운 InputSystem은 기존 InputSystem과 다르게 간단한 인터페이스로 모든 플랫폼을 지원할 수 있어 PC 뿐만 아니라 모바일, 조이스틱 용 게임을 모두 한 번에 대응할 수 있다고 합니다.
PackageManager에서 Packages를 Unity Registry로 변경하면 InputSystem Package를 찾을 수 있습니다. 검색하고 Install 버튼을 눌러 Project에 설치해 주시면 됩니다. 설치가 성공하게 된 이후에도 PackageManager에서 Remove 기능을 통해 언제든지 제거할 수도 있다고 합니다!
Player Input 추가
새로운 InputSystem을 Project에 올바르게 적용했다면 이전에 만들어둔 Player Prefabs에 Player Input 컴포넌트를 추가합니다. Player Input은 새로운 InputSystem을 빠르게 설정하는 데 도움이 되는 많은 기능을 담은 상위 수준 래퍼라고 하는데요. 실제로 공식 문서를 읽어도 다양한 지원을 해줘서 신기하더라고요.
그리고 Action을 생성해야 합니다. Player Input에서 Action이란 논리적인 입력(Up)과 사용자가 수행하는 물리적 액션(W)을 정의하는 기능입니다. 전용 에디터 또는 스크립트에서 Action을 정의하고, 이를 기기의 주요 동작 또는 마우스 왼쪽 버튼과 같이 추상적이거나 구체적인 입력에 모두 연동시킬 수 있습니다.
Action을 생성하는 방법은 Player Input 컴포넌트에서 create Actions을 통해 생성할 수 있습니다.
아래 3가지 정보를 가볍게 확인만 하고 넘어가시면 됩니다.
- 기본적으로 정의되어 있는 Move, Look, Fire 3가지의 Action
- 각 Action에 정의된 기기 장치의 물리적인 입력 방법 그리고 연결되는 논리적인 입력
- 각 Action의 Properties
Player Move Script 작성
Script란 Unity 자체적으로 지원해 주는 기능 외에 GameObject를 접근 또는 제어하기 위해 사용되는 기술입니다. Unity에서는 기본적으로 C# 프로그래밍 언어를 통해 지원해주고 있어요
이제 본격적인 코드 작성 시작이네요. 코드 작성에 앞서 미리 말씀드릴 부분은 이 블로그 시리즈에서는 C# 언어에 대한 개념은 다루지 않습니다. 그래서 변수, 함수, 조건문, 반복문 등 이 키워드들을 모르신다면 C# 기본 문법을 배우고 오시는 것을 추천드립니다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerController : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
}
}
위 코드는 Script를 새로 생성하면 기본적으로 작성되어 있는 코드입니다. 생성한 Script의 이름과 동일한 Class가 정의되며 Start 함수와 Update 함수가 생성됩니다. Start 함수와 Update 함수는 시스템으로부터 호출되는 콜백 함수인데요.
Start 함수는 첫 번째 프레임이 생성될 때 호출되는 함수로 오직 1번만 실행됩니다. 그래서 변수 초기화 등을 진행합니다. Update 함수는 프레임이 생성될 때마다 계속 호출됩니다. 그래서 프레임 단위로 주기적으로 실행되어야 하는 UI 로직들을 처리합니다.
OnMove 함수 작성
Unity로 돌아가서 Player Input Behavior 속성을 확인해 보면 Send Messages로 되어 있는 것을 확인할 수 있는데요.
이는 Player Input에서 처리되는 액션을 스크립트의 함수로 전달하는 방식을 사용한다는 것입니다. 하단 Description을 보면 이동키를 눌렀을 때 호출되는 onMove 함수를 확인할 수 있어요.
public class PlayerController : MonoBehaviour
{
Vector2 movementInput;
...
void OnMove(InputValue movementValue)
{
movementInput = movementValue.Get<Vector2>();
}
}
onMove 함수에는 InputValue라는 자료형의 매개변수가 존재합니다. 이 매개변수는 사용자의 논리적인 입력인 Up, Down, Left, Right에 해당하는 Vector2 정보를 담고 있어요. 그래서 Vector2의 x, y를 통해 왼쪽으로 이동하려고 하는 것인지, 위로 이동하려고 하는 것인지 알 수 있습니다.
Rigidbody 2D 추가하기
Rigidbody 2D Component는 Rigidbody 컴포넌트와 유사하게 물리법칙을 적용하지만 2D에 알맞게 XY 평면에서만 움직인다는 것이 특징입니다. Rigidbody 2D 컴포넌트를 추가한 이유는 다음 글에서 알아보게 될 다른 Object와 충돌을 감지하기 위해서도 있지만 움직임에 물리법칙을 적용해 좀 더 자연스러운 움직임을 구현하기 위해서도 사용됩니다.
Body Type을 Kinematic으로 변경
Rigidbody 2D의 body Type은 3가지가 있는데요.
Dynamic - 가장 기본적인 물리 유형이며 GameObject가 중력에 영향을 받는 것이 특징입니다.
Kinematic - 중력처럼 외부 물리적인 영향으로 인해 GameObject가 움직이지 않고 Rigidbody 2D의 MovePosition 등의 함수를 통해서만 물체를 이동시킬 수 있습니다.
Static - 움직이지 않고 위치를 고정시키는 유형입니다.
저희가 만드는 Top-Down 2D 게임에서 Player는 중력에 영향을 받지 않기 때문에 Kinematic을 사용한다고 합니다 cc Tutorial
Rigidbody 2D Script에서 참조
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
public class PlayerController : MonoBehaviour
{
...
Rigidbody2D rb;
void Start()
{
rb = GetComponent<Rigidbody2D>();
}
...
}
Kinematic 유형이기 때문에 직접 GameObject를 Script에서 이동시켜 주기 위해 Rigidbody 2D Component를 가져와야 합니다. Rigidbody 2D 변수를 하나 선언하고 Start 콜백함수에서 GetComponent 함수를 통해 Script 등록된 GameObject에서 제네릭 타입으로 넘겨진 타입의 해당하는 컴포넌트를 가져옵니다.
Box Collider 2D Component 추가
Box Collider 2D는 2D 물리법칙과 상호작용하는 직사각형 모양의 Collider입니다. 직사각형의 크기 및 위치가 GameObject의 충돌 반경을 의미합니다.
2D Top-Down 게임은 위에서 내려다보는 시점의 게임이기 때문에 캐릭터의 머리까지 충돌 반경을 잡게 되면 구조물 아래에 위치하는 것이 불가능해집니다. 그래서 충돌로 인식되는 범위를 변경해야 합니다.
Box Colider 2D의 직사각형 크기 및 위치를 캐릭터의 발 위치로 조정합니다.
이동하는 Script 작성
FixedUpdate 사용
public class PlayerController : MonoBehaviour
{
void FixedUpdate() //Update 대신 FixedUpdate 사용
{
if (movementInput != Vector2.zero)
{
...
rb.MovePosition(...)
}
}
...
}
Rigidbody 2D MovePosition처럼 물리적인 작업을 진행할 때는 Update 대신 FixedUpdate를 사용해야 한다고 합니다.
Update는 Frame 생성마다 호출되는데요. 이전 Frame이 처리되는 시간에 따라서 다음 Fame 생성 시간이 달라지기 때문에 일정한 시간으로 호출되어야 하는 물리 작업에는 Fixed Update가 좀 더 적합하다고 합니다
Fixed Update는 Project Settings의 Fixed TimeStep 만큼 interval을 가진다고 합니다.
RaycastHit 2D를 이용해 충돌 체크
TileMap 중 울타리처럼 장애물이 이동하는 경로에 존재한다면 이동을 막는 로직이 필요합니다. 장애물로 정의된 Tile은 CollisionObjects의 Tile에 정의되어 있으며 Tilemap Collider 2D로 인해 충돌체로 정의되어 있습니다. 그래서 Cast 함수를 이용해 이동하기 전 경로에 존재하는 충돌체를 미리 확인할 수 있습니다.
public class PlayerController : MonoBehaviour
{
public float moveSpeed = 1f;
public float collisitionOffset = 0.05f;
// (1)
public ContactFilter2D movementFilter;
...
// (2)
List<RaycastHit2D> castColisitions = new List<RaycastHit2D>();
...
void FixedUpdate()
{
if (movementInput != Vector2.zero)
{
int count = rb.Cast(
movementInput,
movementFilter,
castColisitions,
moveSpeed * Time.fixedDeltaTime + collisitionOffset // (3)
);
if (count <= 0) // (4)
{
// ... 이동하려는 방향에 충돌체 없음
}
}
}
...
}
(1) Cast에서 필터링할 충돌체
Cast 함수에서 필터링할 충돌체에 대한 정보를 담은 변수입니다. 만약 구조물뿐만 아니라 총알과 몬스터들이 많아져 충돌하는 Object가 많아질 경우 RaycastHit 2D의 디폴트 버퍼를 넘기게 돼서 필터링하는 작업이 필요하다고 합니다.
(2) Cast로 감지된 충돌체
Cast 함수 매개변수로 넘겨지는 변수로 Cast 함수에서 감지된 충돌체의 결과가 저장됩니다. 지금 당장 이 변수의 값을 제어하지는 않지만 추후에 충돌되는 충돌체를 확인하고 제어할 때 이 변수를 활용할 수 있습니다.
(3) Cast 감지 범위
감지하려는 범위는 실제 플레이어가 이동하는 거리 + collisitionOffset입니다.
우선 실제 플레이어가 이동하는 거리는 이동 속도를 의미하는 moveSpeed에서 Time.fixedDeltaTime 곱한 값을 의미합니다. Time.fixedDeltaTime을 기기 성능 차이로 인해 발생하는 오차를 조정해 주기 위함인데요. 자세한 설명은 링크 연결된 블로그를 참고하시면 좋을 것 같습니다.
collisitionsOffset 값을 추가해서 이동거리 그 너머까지 감지를 시도하는 코드입니다. offset을 추가해서 Player 절반이 벽 너머로 그려지는 것을 방지할 수 있다고 합니다.
(4) Cast 감지된 수
Cast는 감지된 충돌체의 수인 정수형을 반환하는 함수입니다. 그래서 반환 값을 통해 충돌체가 없을 때 이동하는 로직을 작성할 수 있습니다.
전체 코드
public class PlayerController : MonoBehaviour
{
public float moveSpeed = 1f;
/*
* 충돌 감지 offset
*/
public float collisitionOffset = 0.05f;
/*
* 충돌 감지에서 제거할 Layer를 지정할 때 사용
*/
public ContactFilter2D movementFilter;
Vector2 movementInput;
Rigidbody2D rb;
/*
* 이동 경로에 존재하는 충돌체가 저장됨
*/
List<RaycastHit2D> castColisitions = new List<RaycastHit2D>();
void Start()
{
rb = GetComponent<Rigidbody2D>();
}
void FixedUpdate()
{
if (movementInput != Vector2.zero)
{
int count = rb.Cast(
movementInput, // 이동하려는 방향
movementFilter, // Filter
castColisitions, // 결과를 저장하려는 배열
moveSpeed * Time.fixedDeltaTime + collisitionOffset // 감지하려는 범위
);
if (count <= 0)
{
rb.MovePosition(rb.position + movementInput * moveSpeed * Time.fixedDeltaTime);
}
}
}
void OnMove(InputValue movementValue)
{
movementInput = movementValue.Get<Vector2>();
}
}
참고
- https://devhuey.tistory.com/11
- https://rito15.github.io/posts/unity-deltatime-and-fixeddeltatime/
'Unity' 카테고리의 다른 글
[Unity] 2D Top Down RPG 게임 만들기 - Camera Move (1) | 2023.11.03 |
---|---|
[Unity] 2D Top Down RPG 게임 만들기 - Player Animation (1) | 2023.10.28 |
[Unity] 2D Top Down RPG 게임 만들기 - TileMap (0) | 2023.10.16 |
[Unity] 2D Top Down RPG 게임 만들기 - Camera 설정하기 (0) | 2023.10.15 |
[Unity] 2D Top Down RPG 게임 만들기 - Player Prefabs 생성 (0) | 2023.10.14 |