캐릭터의 Rigidbody 2D와 타일의 Collider가 충돌이 일어나면 실제 물리 충돌이 일어나는데 그로 인해 캐릭터가 빙글빙글 도는 현상이 일어난다. 그것을 방지하기 위해 Rigidbody 2D - Constraints - Freeze Rotation의 Z을 체크해준다.
타일맵을 찍을 때 Edit - Preference - Tile Palette에서 기본툴 외의 다른 툴들을 추가해서 사용할 수 있다.
캐릭터 애니메이션 추가
애니메이터 창을 열어 기본 상태 애니메이션에 걷는 애니메이션을 추가해준 뒤, 서로 연결해준다.
Stand에서 Walk로 가는 Transition을 클릭하여 Has Exit Time 체크를 해제하고, Conditions를 추가해서 Walk를 true로 바꿔준다.
Walk에서 Stand로 가는 Transition 마찬가지로 Has Exit Time 체크를 해제하고, 이번엔 Walk를 false로 바꿔준다.
Has Exit Time는 종료 시점을 활성화하는 옵션으로 종료 시점의 비율만큼 애니메이션이 재생된 후에 다음 상태로 전이된다. 즉시 재생시키고 싶다면 비활성화 해야한다.
이제 위에서 만든 애니메이션을 관리하기 위해 스크립트를 만든다.
움직일 때 Walk로 전환 즉, 키를 입력받았을 때 전환시키려면 InputController가 필요하기 때문에 컨트롤러를 불러온다.
유니티에서 이미지를 import하면 기본적으로 PPU(Pixels Per Unit)값이 100이다.
PPU값이 100이라는 의미는 Unity의 단위인 1유닛 안에 100 픽셀이 들어간다는 뜻이다. 즉, PPU값이 작을수록 1유닛 안에 차지하는 픽셀수가 줄어들기 때문에 이미지가 픽셀화 되며 커진다. PPU값은 이미지들끼리 통일해주는 것이 좋으며 높아질수록 고해상도가 되어 성능에도 영향을 미치기 때문에 값을 고려해야한다.
GameObject는 앞서 말한 것처럼 부모-자식 관계를 가지며, Transform 또한 마찬가지다.
부모인 Transform의 Position값을 바꾸면 자식인 MainSprite의 값도 따라온다. 부모의 절대 위치(World Position)가 기준이 되어 자식의 상대 위치(Local Position)가 절대 위치와 달라질 수 있는 것이다.
예를 들어, 자동차를 만든다면 핸들, 바퀴 등의 전부 다른 오브젝트들을 똑같이 이동해야하는 경로를 계산하여 만들어주기 번거롭기 때문에 자동차라는 큰 틀 아래에 핸들은 핸들 위치에, 바퀴는 바퀴 위치에 부품들을 자식으로 넣어주는 것이다.
포지션을 얻을 때 .position을 해주면 절대 위치를 가져오고, .localpostion을 해야 상대 위치를 가져온다.
유니티에서 오브젝트에 연결해준 스크립트를 열면 MonoBehaviour를 상속 받은 것을 볼 수 있다.
Start는 가장 첫 프레임에 동작하며 Update는 매 프레임마다 동작한다.
입력과 캐릭터 이동
캐릭터를 이동시키는 방법에는 여러가지가 있다.
Transform을 직접 좌표로 움직이는 방법이 있고 Rigidbody를 사용해서 물리적인 이동 처리를 하는 방식이 있다.
먼저 Edit - Project Settings - Player - Other Settings에서 Configuration의 Active Input Handling을 Both로 바꿔주자.
Transform을 직접 좌표로 움직이는 방법(Legacy - 절차지향)
Update()에 해당 코드를 입력한다.
float x = Input.GetAxis("Horizontal");
float y = Input.GetAxis("Vertical");
transform.position += new Vector3(x, y);
GetAxis가 가로 "Horizontal"와 세로 "Verticla"을 지칭하며 가져오고, 절대 위치인 transform.position에 새로운 Vecter3()를 만들며 x, y 좌표를 더해준다.
Edit - Project Settings - Input Manager에서 보면 "Horizontal"은 Left와 Right를, "Verticla"은 Down와 Up 방향을 자동으로 처리해준다. 즉, Axis(축) 값을 가지고 왼쪽, 오른쪽, 위, 아래 라는 값을 반환 받고 있는 것이다.
필요하다면 이곳에서 엑세스를 더 추가하거나 Input 클래스에 있는 GetKeyDown으로 각각의 개별적인 키 입력을 받을 수 있다.
그런데 이대로 실행하면 캐릭터를 이동했을 때, Update에 입력해주었기 때문에 매 프레임마다 호출하여 1초에 200~300만큼 이동하게 되어 너무 빠르다.
성능이 다른 컴퓨터마다 프레임이 다르므로 속도 또한 제각각일 것이다. 그렇기 때문에 deltaTime을 곱해준다.
float x = Input.GetAxis("Horizontal");
float y = Input.GetAxis("Vertical");
transform.position += new Vector3(x, y) * Time.deltaTime;
deltaTime은 이전 프레임과 현재 프레임 사이의 시간이다. 게임의 프레임 속도에 상관없이 일정한 시간 간격으로 동작하게 해준다.
10프레임과 60프레임이 있다고 가정했을 때, 프레임과 프레임 사이가 10프레임은 10/1, 60프레임은 60/1일 것이다. 그런데 1초가 지나면 10/1 * 10, 60/1 * 60이 되어 결과적으로 1이 되기 때문에 같아진다. 10프레임은 1초 동안 10번 쪼개서 움직이고 60프레임은 1초 동안 60번 쪼개서 움직인 것이다. 그렇기 때문에 deltaTime을 곱하는 것이다.
deltaTime은 실제 누적 시간 또한 처리할 수 있다.
GetAxis는 0부터 1까지 도달하는 가속도가 붙는 느낌을 주며, GetAxisRaw는 0, 1, -1 딱 떨어지는 값으로 주기 때문에 스무싱 값이 없다.
float x = Input.GetAxisRaw("Horizontal");
float y = Input.GetAxisRaw("Vertical");
transform.position += new Vector3(x, y) * Time.deltaTime;
speed를 public값으로 주면 동기화 되어 유니티 Inspector에서 마음대로 조절할 수 있다.
public class TopDownCharacterController : MonoBehaviour
{
public float speed = 5f;
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
float x = Input.GetAxisRaw("Horizontal");
float y = Input.GetAxisRaw("Vertical");
transform.position += new Vector3(x, y) * speed * Time.deltaTime;
}
}
만약 중요한 값이기 때문에 public으로 하고 싶지 않다면 [SerializeField]로 강제적으로 동기화 시켜도 똑같이 동작한다.
public class TopDownCharacterController : MonoBehaviour
{
[SerializeField] private float speed = 5f;
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
float x = Input.GetAxisRaw("Horizontal");
float y = Input.GetAxisRaw("Vertical");
transform.position += new Vector3(x, y) * speed * Time.deltaTime;
}
}
Rigidbody를 사용한 물리적인 이동 처리 방법(Input System - 객체지향)
이벤트 Action을 이용해보자.
public class TopDownCharacterController : MonoBehaviour
{
// event : 외부에서 호출하지 못하게 막아주는 기능
public event Action<Vector2> OnMoveEvent;
public event Action<Vector2> OnLookEvent;
public void CallMoveEvent(Vector2 direction)
{
OnMoveEvent?.Invoke(direction); //?. : OnLookEvent이 Null이 아닐 때만 동작
}
public void CallLookEvent(Vector2 direction)
{
OnLookEvent?.Invoke(direction);
}
}
?.은 Move 이벤트나 Look 이벤트에 액션이 잘 걸려있을 때만 동작하도록 한다. event는 이벤트들을 외부에서 호출(Invoke)하지 못하게 막아주는 기능이다. 위의 코드에서 보다시피 본인만 호출하고 있다.
이제 플레이어의 Input을 처리할 수 있도록 만들어주기 위해 유니티에 Input System을 추가적으로 설치 해준다. Input System은 조이스틱이나 다른 컨트롤러 등까지 호환되기 때문에 자주 사용된다.
Window - Package Manager에서 Uniy Registry에 보이는 Input System을 Install 한다.
추가한 컨트롤러를 열고 Add Control Scheme에서 +를 눌러 키보드와 마우스를 생성한다.
Action Type을 Value로, Control Type을 Vector2로 바꿔준다. 이 액션을 취했을 때, 돌려받는 값이 Vector2인 것이다.
New action에서 아까 추가한 Up&Down&Right&Left를 누르고 키를 설정해준다.
여기까지 Action을 연결할 준비가 되었다.
입력 처리를 하기 위해 Player와 연결 시켜줄 스크립트를 생성하여 Input Action 스크립트를 상속시킨 뒤, 유니티 자체 엔진 클래스인 Camera를 객체로 불러온다.
public class PlayerInputController : TopDownCharacterController
{
private Camera _camera;
private void Awake()
{
_camera = Camera.main; // Camera 씬에서 태그가 main인 코드를 찾아오겠다 = 마우스와 캐릭터 포지션 위치 확인
}
private void OnMove(InputValue value)
{
Debug.Log("OnMove" + value.ToString());
}
private void OnLook(InputValue value)
{
Debug.Log("OnLook" + value.ToString());
}
private void OnFire(InputValue value)
{
Debug.Log("OnFire" + value.ToString());
}
}
Send 메세지 방식은 만들어진 액션에 On을 붙이면 실행됐을 때, 돌려받는 형식이다. 확인하기 위해 Debug 해준다.
Player 오브젝트에 방금 작성한 스크립트와 Player Input을 컴포넌트 한 뒤, Actions에 설정해뒀던 방향 컨트롤러를 끌어다 놓고 플레이하여 방금 설정한 값들(wasd, 방향키, 마우스 등)을 입력하면 Console창에 뜬다.
값이 입력되는 것을 확인하고 다시 돌아와서 코드를 완성시킨다.
normalized를 하는 이유는 방향키를 두 개 동시에 입력했을 때, x와 y축 값이 합쳐져 대각선 방향의 속도가 빨라지기 때문에 그것을 방지하기 위해 단위 벡터로 만들어 주는 것이다.
private void OnMove(InputValue value)
{
// Debug.Log("OnMove" + value.ToString());
Vector2 moveinput = value.Get<Vector2>().normalized;
CallMoveEvent(moveinput); // 움직일 때 벡터 값을 불러온다
}
private void OnLook(InputValue value)
{
// Debug.Log("OnLook" + value.ToString());
Vector2 newAim = value.Get<Vector2>(); // 마우스 포지션을 받아온다
Vector2 worldPos = _camera.ScreenToWorldPoint(newAim); // 마우스 위치를 절대 위치로 바꾼다
newAim = (worldPos - (Vector2)transform.position).normalized; // 캐릭터와 마우스 커서까지의 거리와 방향
//if (newAim.magnitude >= .9f) // magnitude : 크기(normalized 했기 때문에 1) 혹시 모를 오류(0, 0처럼 0으로 나누는 경우) 방지를 위해 제한
//{
// CallLookEvent(newAim);
//} // 이미 normalized 해주었기 때문에 안해도 됨. 강의에서 실수한 듯!
}
private void OnFire(InputValue value)
{
Debug.Log("OnFire" + value.ToString());
}
Move, Look, Fire 이벤트들을 어떤 행동할 때 알려주는 것을 걸어놓는 것을 '구독한다(Subscribe)'라고 표현한다. CallMoveEvent에서 이벤트 액션들을 Invoke하면 구독한 것들에 신호를 준다. 이것을 Observer 패턴이라고 한다.
이제 이동을 처리하는 스크립트를 만들어준다.
GetComponent는 Inspector 안에서 Component끼리 서로 인지할 수 있는 방법으로 상위의 Compenent인TopDownCharacterController를 가져온다. 같은 방식으로 Rigidbody를 가져온다. 키보드 값이 입력되면 ApplyMevment에서 direction을 받아오고 속도를 곱해서 가속도를 저장해준다. 그리고 rigidbody가 그 가속도만큼 움직인다.
FixedUpdate()는 Update()보다 호출이 느리기 때문에 물리처리가 끝난 이후에 호출이 되도록 한다.