유니티에서의 물리, 충돌 처리를 할 수 있는 컴포넌트는 크게 Rigidbody, 충돌체, Physics, Joint(연결 부위) 등이 있다.
이들 중에서 특히 중요한 Rigidbody, 충돌체에 대해 다뤄보려고 한다.
Rigidbody 컴포넌트는 게임 오브젝트에 물리엔진을 적용하는 컴포넌트이다. 물체의 질량, 공기저항, 중력에 대한 연산과 기본지원 함수를 사용해 보다 현실적인 물리처리가 가능하다.
Rigidbody 컴포넌트를 추가하면 위와 같이 많은 기능들이 나온다. 이 내용에 대해 간단하게 알아보자.
- Mass : 질량
- Drag : 이동 운동에 대한 저항력
- Angular Drag : 회전 운동에 대한 저항력
- Use Grabity : 중력 사용 여부
tip) 중력가속도는 -9.81이나 게임사에서 게임을 제작할 때는 -30 ~ -25 를 사용할 수 있다.- Is Kinematic : 물리력 사용 여부
해당 기능이 활성화되어 있을 시 오브젝트는 외부의 물리력의 영향을 받지 않고 Transform을 통해서만 이동
다른 충돌체는 밀 수 있지만 자기 자신은 밀리지 않음- Interpolate : 프레임 단위의 오브젝트 보간 여부
tip) 너무 빠른 속도로 물체가 이동할 시에 충돌체를 뚫고 지나가는 현상이 생길 수 있다.
(물리 엔진은 0.02초마다 갱신하는 FixedUpdate 특성 때문)
이런 경우에는 보간이 필요하나, 보간을 사용하는 물체가 많을 경우 프레임 드랍 발생 가능- Collision Detection : 충돌 감지 방식 설정
위 Interpolate와 같이 필요할 시에만 사용을 권장(연속 충돌성검사-CCD)- Constraints : 위치 혹은 회전 제어
- Info : 각 물리력이 적용되고 있는 수치를 출력
AddForce(x, y, z)
매개변수로 입력되는 x, y, z축 방향으로 물리적인 힘을 가한다. 입력이 반복될수록 적용되는 힘이 누적된다.
AddTorque()
매개변수로 입력되는 축의 물리적인 회전력을 더한다.
velocity
게임 오브젝트에 가해지고 있는 물리력.
angularVelocity
게임 오브젝트에 가해지고 있는 회전력.
using UnityEngine;
public class RigidBodyTester : MonoBehaviour
{
[SerializeField] Rigidbody rigid;
[SerializeField] float power;
private float xInput;
void Update()
{
xInput = Input.GetAxis("Horizontal");
if(Input.GetButtonDown("Jump"))
{
rigid.AddForce(Vector3.up * power, ForceMode.Impulse);
}
}
// 물리력 계산과 같은 부분은 FixedUpdate에서 진행하도록 권장한다.
private void FixedUpdate()
{
// 1. 힘 가해주기 : 가속시키기
// rigid.AddForce(Vector3.right * xInput * power);
// 2. 속도를 설정하기 : 속도를 원하는 대로 설정
// rigid.velocity = Vector3.right * xInput * power;
// 3. 회전력 가해주기
// rigid.AddTorque(Vector3.up * xInput * power);
// 4. 회전속도 설정하기
//rigid.angularVelocity = Vector3.up * xInput * power;
}
}
ForceMode.(모드 종류)로 사용한다.
Force = 질량의 영향을 받음, 지속적으로 미는 힘을 줌 (기본)
Impulse = 질량의 영향을 받음, 순간적으로 강한 힘을 줌
Acceleration = 질량을 무시하고 지속적으로 미는 힘을 줌
VelocityChange = 질량을 무시하고 순간적으로 강한 힘을 줌
// 총알에 컴포넌트를 달자
using UnityEngine;
public class Bullet : MonoBehaviour
{
[SerializeField] Rigidbody rigid;
// 총알이 떨어질 때 자연스럽게 떨어지게 하기
private void Update()
{
// 속도가 0이 될때까지 회전하면 이상하게 출력되므로, 테스트하면서 수치 조정
if (rigid.velocity.magnitude > 2)
{
transform.forward = rigid.velocity;
}
}
}
// 탱크 터렛에 컴포넌트를 달자
public class Shooter : MonoBehaviour
{
[SerializeField] GameObject bulletPrefeb;
[SerializeField] Transform muzzlePoint;
[SerializeField] ObjectPool bulletPool;
[Range(10, 30)]
[SerializeField] float bulletSpeed;
public void Fire()
{
GameObject instance = Instantiate(bulletPrefeb, muzzlePoint.position, muzzlePoint.rotation);
Rigidbody bulletRigidbody = instance.GetComponent<Rigidbody>();
bulletRigidbody.velocity = muzzlePoint.forward * bulletSpeed;
}
}
충돌체 : 게임오브젝트의 물리적 충돌을 목적으로 모양을 정의하는 것을 말한다.
게임 오브젝트 간의 충돌체로 부딪힘과 반발력을 처리한다.
충돌체는 충돌상황에 있을 경우 유니티 충돌메시지를 받아 상황을 확인한다.
* 충돌 이벤트를 사용하기 위해서는 Is Trigger가 해제되어 있어야 한다.
(Is Trigger가 체크되어 있으면 충돌이 발생하지 않음)
Collision 이벤트는 아래와 같이 3가지 이벤트 메시지가 있다.
// 유니티 충돌 메시지
private void OnCollisionEnter(Collision collision)
{
// 충돌 진입시 호출
Debug.Log("Collision Enter");
}
private void OnCollisionStay(Collision collision)
{
// 충돌 중 호출
Debug.Log("Collision Stay");
}
private void OnCollisionExit(Collision collision)
{
// 충돌 해제 시 호출
Debug.Log("Collision Exit");
}
using UnityEngine;
public class Bullet : MonoBehaviour
{
[Header("Components")]
[SerializeField] Rigidbody rigid;
[Header("Propertis")]
[SerializeField] GameObject explosionEffetPrefeb;
private void Update()
{
if (rigid.velocity.magnitude > 2)
{
transform.forward = rigid.velocity;
}
}
// 총알이 충돌체와 만나면 제거되면서 이펙트 발생
private void OnCollisionEnter(Collision collision)
{
Destroy(gameObject);
Instantiate(explosionEffetPrefeb, transform.position, transform.rotation);
}
}
하나의 충돌체가 충돌을 일으키지 않고 다른 충돌체의 공간에 들어가는 것을 감지하는 것을 말한다.
트리거는 겹침상황에 있을 경우 유니티 트리거 메시지를 받아 상황을 확인한다.
* Trigger 를 사용하기 위해선 Is Trigger가 체크되어 있어야 함.
Trigger 이벤트는 아래와 같이 3가지 이벤트 메시지가 있다.
private void OnTriggerEnter(Collider other)
{
Debug.Log("Trigger Enter!");
}
private void OnTriggerStay(Collider other)
{
Debug.Log("Trigger Stay!");
}
private void OnTriggerExit(Collider other)
{
Debug.Log("Trigger Exit!");
}
언뜻 보면 충돌 이벤트와 트리거 이벤트와 비슷해 보일 수도 있지만, 충돌 이벤트만으로 구현할 수 없는 상황이 존재하므로 트리거 이벤트가 따로 존재한다.
트리거 이벤트 예시
총알이 포물선을 그리며 날아가고, 충돌체에 부딪치면 폭발하는 기능까지 잘 구현하였다. 하지만 이런 문제점을생각할 수 있다.
총알을 연속해서 발사하니, 총알끼리 부딪혀서 폭발하며 파괴되는 현상
플레이어가 발사한 총알인데 플레이어한테 맞아서 폭발하며 파괴되는 현상
이와 같은 문제를 해결하기 위해 Layer를 사용해야 한다.
인스펙터 우측 상단에 보면 Layer라는 영역이 있다. 유니티는 기본적으로 제공하는 Layer가 있으며, 필요시에 Layer를 만들 수도 있다.
이와 같이 레이어를 만든 후 해당 레이어를 오브젝트에 적절하게 적용한다.
이 다음으로 레이어 별 상호작용 조건을 Edit > Project Setting에서 변경해야 한다.
Physics 영역에서 하단으로 내려보면 Layer Collision Matrix 라는 영역이 있다. 여기에서 Bullet끼리 충돌하지 않고, 플레이어와도 충돌하지 않도록 해당 체크박스를 해제해야 한다.
이와 같은 내용을 반영하면 총알끼리 부딪혀 파괴되거나 플레이어와 부딪혀 파괴되는 현상을 막을 수 있다.
충돌체와 트리거의 사용에 있어서 유의해야 할 부분이 있다. 충돌체가 어떤 종류이냐에 따라 상호작용이 달라질 수 있기 때문이다.
두 게임 오브젝트가 충돌하면 리지드바디의 환경설정에 따라 다른 반응을 할 수 있으며, 일부 조합의 경우 두 게임 오브젝트 중에서 하나만 충돌의 영향을 받을 수도 있다.
일반적으로 Rigidbody 컴포넌트가 있는 게임 오브젝트의 물리가 적용되는 것이 원칙이다.
정적 충돌체(Static Collider)
Rigidbody가 없는 충돌체로, 외부의 힘에 움직이지 않는다.
ex) 절대로 움직이지 않는 지형, 구성요소에 주로 사용한다.
리지드바드 충돌체(Rigidbody Collider)
Rigidbody가 있는 충돌체로, 외부에 힘을 받아 움직인다.
ex) 충돌하여 힘을 받아 움직이는 물체에 사용한다. 모든 움직이는 물체는 왠만하면 리지드바디 충돌체로 만드는 것이 좋다. 아니면 물체를 통과하는 등의 비정상적인 결과가 나올 수도 있다.
키네마틱 충돌체(Kinematic Collider)
Kinematic Rigidbody가 있는 충돌체로, 외부의 힘에 반응하지 않는다.
움직이지만 외부 충격에는 밀리지 않는 물체에 사용한다.
Kinematic 상태를 활성화/ 비활성화하여 움직임 여부를 설정하는 경우에도 사용한다.
* Kinematic이 있는 물체끼리도 서로 반응하지 않는다.
충돌체와 트리거의 종류에 따라 메시지가 전송되는 경우가 다르며,
유니티 메뉴얼에 해당 내용이 자세히 나와있다. (콜라이더)
레이캐스트는 시작 지점부터 일직선상으로 진행되는 레이저를 발사하고, 레이저에 닿는 오브젝트를 검출하는 기능이다. 이 기능을 사용해 구현할 수 있는 예시는 아래와 같은 것이 있다.
- FPS
FPS 게임의 에임에 사용된다. 실제로 FPS 게임을 설계할 때, 총알을 실제로 발사하는 방식 대신 레이캐스트를 사용하는 경우가 많다. (탄환이 눈에 보이면 피할 수도 있고, 그렇다고 탄환 속도를 너무 빠르게 하면 탄환이 충돌하지 않고 통과하는 경우가 발생)
탄두 발사 경로부터 에임 범위에 들어오는 사용자의 닉네임이 표시되는 기능 또한 레이캐스트를 사용한다.
- 게임 내 오브젝트 선택과 상호작용
게임 내에서 마우스로 오브젝트를 선택할 수 있는 기능도 레이캐스트를 사용한다.
카메라로부터 시작된 레이캐스트에 닿는 오브젝트는 선택 된 것으로 구현한다.
위와 같은 예시만으로도 거의 모든 게임에서 필요한 기능이니, 사용 방법을 숙지하는 것이 중요하다.
동시에 레이캐스트는 많은 연산량이 요구되므로 사용 환경과 오브젝트의 거리 등 많은 상황을 고려해 적절히 사용해야 한다.
레이캐스트를 사용해보자.
using UnityEngine;
public class RaicastTestor : MonoBehaviour
{
private void Update()
{
// 레이캐스트 : 시작위치에서 방향으로 레이저를 발사하여 부딪히는 충돌체를 감지(거리 10m로 설정)
// 오버로딩 함수(16가지)
if (Physics.Raycast(transform.position, transform.forward, out RaycastHit hitInfo, 10f))
{
// 레이저에 닿은 충돌체가 있다.
Debug.Log(hitInfo.collider.gameObject.name);
}
else
{
// 레이저에 닿은 충돌체가 없다
Debug.Log("None");
}
}
}
이와 같이 작성하고 테스트를 해 보자.
위와 같이 전방 10m에 아무것도 없을 때는 충돌체가 없으므로 None을 출력한다.
해당 물체를 전방으로 이동했을 경우 Building을 인식하여 반환하는 것을 확인할 수 있다.
테스트하면서 레이캐스트에 관한 걸 알아보았지만, 조금 불편하다고 생각할 수 있다.
레이캐스트가 실질적으로 눈에 잘 보이지 않으니 제대로 작동하고 있는지 확인하기 어렵기 때문이다.
이러한 문제를 해결하기 위해 기즈모라는 기능을 활용할 수 있다.
using UnityEngine;
public class RaicastTestor : MonoBehaviour
{
private void Update()
{
// 레이캐스트 : 시작위치에서 방향으로 레이저를 발사하여 부딪히는 충돌체를 감지
if (Physics.Raycast(transform.position, transform.forward, out RaycastHit hitInfo, 10f))
{
// 레이저에 닿은 충돌체가 있다.
Debug.Log(hitInfo.collider.gameObject.name);
Debug.DrawLine(transform.position, hitInfo.point, Color.green);
}
else
{
// 레이저에 닿은 충돌체가 없다
Debug.Log("None");
Debug.DrawLine(transform.position, transform.position + transform.forward * 10f, Color.red);
}
}
// 구 형태의 영역으로 표시
private void OnDrawGizmos()
{
Gizmos.color = Color.red;
Gizmos.DrawWireSphere(transform.position, 3f);
}
// 지정한 좌표로의 선으로 표시
private void OnDrawGizmosSelected()
{
Gizmos.color= Color.green;
Gizmos.DrawLine(transform.position, transform.forward * 10);
}
}
이와 같이 작성하고서 씬 뷰와 게임 뷰를 살펴보자.
OnDrawGizmos() 와 OnDrawGizmosSelected()의 경우에는 게임을 실행하지 않아도 씬 뷰에서 바로 표시된다. OnDrawGizmosSelected()의 경우, 해당 물체를 선택하면 출력된다.
이와 같이 확인한 다음 Update 문 안에 있는 Debug.DrawLine을 살펴보자. 해당 기능을 게임을 테스트할 때만 출력이 된다.
전방으로 뻗어나가는 선이 추가되었다. 이 선이 충돌체에 닿았을 때 어떻게 되는지 살펴보자.
충돌체를 인식하여 기즈모의 선 색깔이 바뀌었고 해당 충돌체의 이름을 출력하고 있다.
아래와 같은 조건으로 몬스터와 탱크를 구현해 보기로 했다.
- 맵에 몬스터를 구현한다
- 몬스터는 3의 체력을 갖는다
- 탱크의 탄환은 몬스터를 맞추면 체력을 1 감소시킨다
- 몬스터는 모든 체력을 소진했을 때 사라진다
- 몬스터는 탱크 방향으로 지속적으로 추적하여 움직인다
- 몬스터는 탱크를 추적하지만 중간에 장애물이 있는 경우 멈춘다
- 탱크는 몬스터와 부딪힐 때 사라진다
테스트용으로 만드는 것이라 간단하게만 구현했지만 보통 인간형 캐릭터를 구현할 때 캡슐형에 시야 부분을 표시한 형태로 구현한다고 배웠다.
(의외로 프리팹 만드는 게 쉽지 않았다.)
위의 내용을 구현하기 위해 탱크 컨트롤러에 내용을 추가하고, 몬스터 컨트롤러를 구현한다.
using UnityEngine;
public class TankMovementUpgrade : MonoBehaviour
{
...
[Header("Properties")]
[SerializeField] GameObject tankExplosionEffetPrefeb;
// 앞의 글에서 사용한 걸 이어서 쓰는 거라 내용 생략
private void OnCollisionEnter(Collision other)
{
if(other.gameObject.tag == "Monster")
{
Destroy(gameObject);
Instantiate(tankExplosionEffetPrefeb, transform.position, transform.rotation);
}
}
}
using UnityEngine;
public class MonsterMover : MonoBehaviour
{
[Header("Components")]
[SerializeField] Transform eyeTransform;
[SerializeField] float sightRange;
[SerializeField] float moveSpeed;
[SerializeField] GameObject target;
[SerializeField] GameObject monsterExplosionEffetPrefeb;
private int monsterMaxHp = 3;
private int monsterHp;
private void Awake()
{
// 몬스터 체력 초기 설정
monsterHp = monsterMaxHp;
}
private void Update()
{
// 타겟 추적
DetectTarget();
// 플레이어 추적
if (target != null)
{
Trace();
}
// 몬스터 체력 판정
MonsterIsDead();
}
private void OnCollisionEnter(Collision other)
{
if (other.gameObject.tag == "PlayerBullet")
{
TakeDamage();
}
}
private void TakeDamage()
{
monsterHp--;
}
private void MonsterIsDead()
{
if (monsterHp == 0)
{
Destroy(gameObject);
Instantiate(monsterExplosionEffetPrefeb, transform.position, transform.rotation);
}
}
private void DetectTarget()
{
if(Physics.Raycast(eyeTransform.position, eyeTransform.forward, out RaycastHit hitInfo, sightRange))
{
Debug.DrawLine(eyeTransform.position, hitInfo.point, Color.green);
if(hitInfo.collider.gameObject.tag == "Player")
{
target = hitInfo.collider.gameObject;
}
else
{
target = null;
}
}
else
{
Debug.DrawLine(eyeTransform.position, eyeTransform.position + eyeTransform.forward * sightRange, Color.red);
target = null;
}
}
private void Trace()
{
transform.position = Vector3.MoveTowards(transform.position, target.transform.position, moveSpeed * Time.deltaTime);
transform.LookAt(target.transform.position);
}
}
이와 같이 작성하고서 몬스터 프리팹 설정을 한다.
이제 테스트를 진행해보자.
사소한 것부터 시작해 시행착오가 많았던 터라 따로 정리해보기로 했다.
처음에 탱크에다가 Rigidbody를 부여했을 때 아래와 같은 현상이 발생했다.
탱크가 바닥에 안착해야 하는데 바닥을 뚫고 떨어지는 것이다. 처음에는 원인을 찾지 못해서 Plane 설정이 잘못되어 있나 이것저것 건들여봤는데 안 고쳐지는 것이다.
그런데 다른 공이나 이런 걸 생성하면 바닥에 잘 안착이 되었다.
알고 보니 이 탱크는 모델만 있는 것으로, Mesh도 따로 없고 충돌체가 구현되어 있지 않았다. 이럴 경우에는 프로그래머가 따로 충돌체를 만들어줘야 한다.
세세하게 만들 순 없었고 우선은 박스 충돌체로 만들어 주었다. 이렇게 해주니 바닥에 잘 안착하는 모습을 볼 수 있었다.
지금 보면 사소한 실수일 수 있지만, 까딱하면 놓치기 쉬운 내용이라서 기록해 놓는 것이 좋겠단 생각이 들었다.
특히나 왜 이런 건지 제일 고민했던 내용이 아닐까 싶다.
몬스터 기능을 구현하는 건 문제가 없었는데, 몬스터가 이상하게 출력되는 현상이 발생한 것이다.
원인은 Rigidbody를 추가하지 않은 것에 있었다. Rigidbody가 없는 충돌체는 물체를 그대로 통과해버리기 때문에, 바닥을 통과한다는 사실을 몸소 체험한 셈이다.
결국 빠뜨린 Rigidbody를 추가하고, Rotation을 전부 제한을 건 후 다시 실행시켜 보았다.
Rotation 제약을 건 후에 움직임을 살펴보면, 똑바로 선 채로 쫓아오길 바라는데 자꾸 고개를 숙이는(?) 이상한 현상이 발생한다.
처음엔 Rotation만 제약을 걸어서 문제가 발생하는 건가 싶어서 Position 쪽에 제약을 걸었더니, 또다시 땅으로 몬스터가 빠지는 모습을을 볼 수 있었다.
다시 몬스터 프리팹을 살펴보자.
나는 처음에 Empty Space가 몬스터의 중심부에 있어야 한다고 생각을 했었다. 하지만 가만 생각해보니, 자꾸만 몬스터가 가라앉는 위치가 몬스터 중심부까지 가라앉는 거라는 사실을 알게 되었다.
실제로 몬스터 코드를 살펴 보면, eyeTransform이라는 위치까지 따로 받으며 시야를 설정했지만, 만약 Monster Empty Space 자체를 중심부로 잡게 되면 eyeTransform의 위치는 저 중심부로부터 빨간 박스 사이의 거리밖에 되지 않게 된다.
즉, Empty Space 아래의 부분이 존재하지 않는 것처럼 취급된다는 소리다.
그래서 문제가 있는 몬스터 프리팹을 고쳐야 한다는 걸 알게 되었다.
위와 같이 최종적으로 프리팹을 수정했고, Rotation만 제한시키면 최종적으로 원하는 의도대로의 몬스터 움직임이 구사된다.