유니티 프로젝트 (3) - isOnGround (2D)
점프를 구현함에 있어 첫 번째로 봉착하게 되는 문제는 땅에 닿아있는 상태를 확인하는 겁니다. 저번 시간에 구현한 점프 기능은, 공중에서도 몇 번이고 점프가 가능하다는 문제가 있었죠. 이래서는 제대로 된 게임이 되기는 글러먹었습니다. 땅에 닿아있을 때만 점프가 가능하게 만들면 좋겠습니다. 물론 로직 자체는 어려운 게 아니죠.
문제는 이걸 어떻게 확인하느냐는 겁니다. 다행스럽게도 여기에는 적지 않은 수의 해결방법이 존재합니다. 몇 가지 잘 알려진 방법을 골라 소개해드리겠습니다. 이 글에서는 편의상 다음 방식들로 구분하겠습니다.
- cast
- Threshold
- IsTouching
일단 결론부터 말씀드리자면 IsTouching
이 적어도 2D게임을 만든다면 최선의 방식이라는 겁니다. 이제부터 각 방법들의 원리를 소개하고, 어떤 장단이 있는지를 알아보죠.
cast
유니티가 제공하는 cast
methods는 마치 레이저를 쏘듯 지정된 방향으로 계속 충돌 검사를 하는 기능입니다. 레이저를 얼마나 길게 쏠지는 설정이 가능하죠. 이를 이용하면 플레이어 바로 밑에 바닥이 있는지 알 수 있습니다. 이 cast
는 어떤 모양의 레이저를 쏠 것인지에 따라 Raycast
, Boxcast
, Spherecast
등 다양한 바리에이션을 제공하지만 우리가 원하는 목적을 위해서는 Raycast
(또는 귀퉁이에 올라서는 경우를 고려한다면 Boxcast
까지) 정도만 써도 됩니다.
코드는 다음과 같이 작성하면 됩니다.
이렇게 작성된 프로퍼티 IsOnGroundUsingCast
는 플레이어의 한 가운데로부터 아래 방향으로 최대 2f 길이의 레이저를 쏴서 (Layer
가 Ground로 설정된) 바닥이 있는지 검사합니다.
이제 바닥에 닿아있을 때에만 이 값은 true를 반환합니다. 그럼 된 걸까요?
아쉽게도 이 방식에는 몇 가지 고려해야 할 점들이 있습니다.
일단 레이저를 너무 길게 쏴서는 안 된다는 겁니다. 플레이어가 점프한 지 얼마되지 않는 순간에도 레이저는 바닥을 감지하고 있을 가능성이 있습니다. 이때 빠르게 점프 버튼을 연타해버리면 어떻게 될까요?
또다른 한 가지 문제는 Unity가 제공하는 Effector2D를 함께 사용할 때 발생합니다.
Effector2D
플랫포머 게임을 할 때 빠지지 않는 게 점프해서 올라갈 수 있는 여러 층의 바닥들입니다. 이 바닥은 밑에서 점프해서 올라갈 때는 충돌판정이 되지 않지만 위에서 내려올 때는 제대로 착지할 수 있어야겠죠. 이 기능을 쉽게 구현할 수 있도록 도와주는 컴포넌트가 바로 Effector2D
입니다.
플레이어가 플랫폼을 통과하는 것을 제대로 보기 위해 색깔을 좀 바꿨습니다. Platform Effector 2D
속성을 추가해주면 우리가 흔히 생각하는 플랫포머 게임에서의 그 점프해서 올라갈 수 있는 플랫폼이 되죠.
잘 작동하는군요!
문제는 이게, cast
와는 함께 쓰기에는 까다로워진다는 겁니다. cast
는 실제 플레이어가 바닥과 충돌하는지와는 관계없이 그냥 있는지만을 검사하기 때문에, 플레이어가 플랫폼 사이를 지나가고 있을 때조차 cast
는 true를 반환합니다. 그리고 이는 전혀 예상하지 못한 상황에서 Double Jump 문제를 만들어버리죠.
Threshold
이 방식은 충돌판정을 활용하지는 않습니다. 대신 착지한 상태의 대상은 속도가 0이라는 단순한 특성에 초점을 맞춰 플레이어의 상태를 확인합니다.
활용하는 특성이 단순하므로 이를 구현하는 것도 꽤 심플합니다.
일단 역치를 설정해줍니다. 게임 개발에 관심을 가질 정도로 프로그래밍을 하신 분들이라면 다들 아실테지만, Unity
에서 모든 물리와 관련된 변수들은 float
자료형을 쓰고, 이러한 부동소수점에의 동등비교는 아무 의미가 없죠. 대신 충분히 작은 역치를 잡고, 이 작은 범위 안에 있을 경우 0으로 치는 겁니다.
이제 플레이어의 위아래 방향 움직임이 없을 때에만 true를 반환하는군요. 그럼 된 걸까요?
안타깝게도, 이 방식에는 한 가지 치명적인 문제가 있습니다.
플레이어의 수직 방향 속도가 0이 되는 순간은 착지해있을 때 뿐만이 아닙니다.
점프의 최정점에 도달하는 순간, 이 방식은 true를 반환할 수도 있습니다. 물론 역치가 충분히 작다면 이 순간은 무척이나 짧기 때문에 어지간해서는 별 문제가 되지 않을 수도 있습니다만, 운이 없다면 의도치 않게 Double Jump가 발생할 수 있다는 점은 고려해야겠죠.
IsTouching
이 방법은 cast
방식처럼 별도의 충돌체를 쏘거나, threshold
방식처럼 본질을 벗어나지도 않습니다. 플레이어 자체의 충돌판정을 이용하죠. 이 방식을 활용하기 위해서는 ContanctFilter2D 변수를 만들어둘 필요가 있습니다.
코드 자체는 굉장히 짧고 단순합니다. 대신 ContactFilter를 설정해 주어야합니다.
여기까지 해두면 IsTouching
을 활용할 준비는 다 된 겁니다.
이제 바닥과 충돌한 순간에만 true를 반환합니다.
IsTouching을 활용할 때의 가장 큰 장점은, 위 두 방식을 이용했을 때의 문제점들이 해결된다는 겁니다. effector2d
로 인해 충돌이 무시된 경우 false를 반환하기 때문에 플랫폼 사이에서 의도치 않은 점프가 발생하지 않으며, 바닥에 거의 닿은 상태에서 cast 길이 때문에 true를 반환하지 않습니다. 또한 본질적인 충돌 자체를 판단하기 때문에 정점에서 true를 반환할 우려도 없습니다. Unity에서도 이 방식을 쓰라고 추천하니까, 플랫포머 게임을 만든다면 이 방식을 쓰는 것을 고려해 봅시다.
그럼 문제는 없는 걸까요? 물론 없는 건 아닙니다.
IsTouching
은 Rigidbody2D
의 내장 메소드입니다. 3D게임을 만든다면, 안타깝게도 이 방법을 쓸 수가 없습니다! 애초에 Unity
의 충돌 판정 메소드들은 Unity에서 자체제작했다기보다는 NVIDIA
가 만들어놓은 물리 기능들을 활용한 것에 가깝고, 3D를 위한 IsTouching 메소드는 포함되지 않았죠.
그도 그럴만한게, 충돌판정 자체가 상당히 비싼 메소드라서, 3D에서 이 기능을 매 프레임마다 함부로 돌렸다가는 실행 성능을 보장할 수가 없을 겁니다. 2D에서는 이 비용이 큰 문제가 되지 않지만 3D에서는 그렇지 않습니다. 하지만 3D에서도 IsTouching과 비슷하게 착지를 판단할 수 있는 방법은 있습니다. 이건 나중에 다뤄보죠.
(다음은 코드 전문입니다. 시험해보고 싶다면 써보도록 합시다.)