[20260119] - 물리엔진

SmartBear·2026년 1월 19일

물리 충돌 처리

물리와 충돌은 거의 모든 게임에서 다뤄지는 오브젝트간의 상호작용 중 하나.
물리 연산은 생각보다 무거운 편이다.
하지만 많이 쓸 수 밖에는 없다.

기본 주요 용어

  • Rigidbody : 오브젝트에 물리적 특성을 부여하는 컴포넌트로, 물리 엔진의 영향을 받아 동작하게됨. 힘을 가하거나 중력의 영향을 받도록 설정 가능.
  • Collider: 오브젝트의 물리적 형태를 정의하는 컴포넌트. Rigidbody가 없어도 충돌 감지만 가능하지만, 일반적으로 Rigidbody와 함께 사용되어 충돌 시 물리적 상호작용을 일으킨다.
  • Mass (질량): 오브젝트의 무게를 결정하며, 충돌 시 힘의 전달에 영향을 준다. 상대적인 값으로, 당연하지만 값이 클수록 더 무겁다.
  • Drag (공기 저항) / Angular Drag (회전 공기 저항): 물체의 움직임이나 회전을 방해하는 저항 값.
  • Use Gravity (중력 사용): 체크 시 오브젝트가 중력의 영향을 받는다. 동작 중 체크 해제해도 이미 이 적용되더 있어 계속 떨어진다.
  • Physics Material (물리 머티리얼): 콜라이더에 적용하여 충돌 시 마찰(Friction)과 반발력(Bounciness)을 조절. 'Friction'은 멈춰있을 때(Static)와 움직일 때(Dynamic) 설정 가능하며, 'Bounciness'는 튕겨 오르는 정도를 설정.
  • AddForce() / AddTorque(): 스크립트에서 리지드바디에 힘을 가하거나 회전력을 주어 움직이는 함수.
  • Is Kinematic: Rigidbody 가 외부의 물리적 힘(충격)을 받지 않지만, 다른 Rigidbody에는 영향을 줄 수 있다. 이 경우 Transform을 직접 제어할 수 있다. 또한 충돌 감지도 가능하다.
  • velocity / angularVelocity: 오브젝트에 가해지고 있는 물리/회전력.
  • FixedUpdate(): 물리 연산에 사용되는 업데이트 함수로, Update()와 달리 물리 갱신 주기에 맞춰 일정한 간격(20ms)으로 호출. 물리 관련 로직은 이 함수 안에서 작성하는 것을 권장.

충돌 검사 (Collision Detection)

중력이 적용되어 있는 상태에서 오브젝트를 아주 높이 놓은 상태에서 떨어뜨리면 땅을 뚫고 가버린다.
물리 엔진은 꽤 무거운 연산이기 때문에 매 Frame 별로 연산되는 것이 아니라 특정 시점에서만 연산을 하게 된다.
그 연산 사이에 충돌이 발생되면 충돌감지를 하지 못 하게 된다.(터널, 터널링) 이에, 연산을 어떻게 할 것인지에 대한 옵션을 줄 수 있다.

  • Discrete (이산); 보통 많이 쓰는 충돌 검사. 오브젝트가 느리게 움직일 경우 사용.
  • Continous (연속); 오브젝트가 빠르게 움직일 때 사용. 이산보다는 자주 연산하기 때문에 CPU 부담이 높다.
  • Continous Dynamic (연속 동작); 오브젝트가 빠른 속도로 복잡한 움직임이 있을 경우 사용. CPU 부담이 높다.
  • Continous Speculative; 미리 충돌 여부를 감지해야 하는 경우 사용. CPU 부담이 높다.

대략 cpu 부담은 Continous Speculative > Continous Dynamic > Continous >> Discrete

특정 시간마다 연산하는 이유?

유니티는 기본적으로 프레임 단위의 라이프사이클을 기준으로 동작한다.
우리가 가장 자주 사용하는 Update는 매 프레임마다 호출되며, 이 프레임 수는 PC 성능에 따라 초당 호출 횟수가 달라진다.

프레임마다 실행되는 일반 게임 로직은 호출 주기가 조금 변하더라도 큰 문제가 되지 않지만,
물리 연산이 프레임 변화의 영향을 직접 받게 되면 게임 속도가 느려지거나, 움직임이 불안정해지는 등 플레이에 큰 영향을 끼치게 된다.

이러한 문제를 방지하기 위해 유니티는 물리 엔진 연산을 일정한 시간 간격으로 실행하도록 분리해 두었다.
즉, 게임 로직과는 독립적으로 물리 연산이 항상 일정한 타이밍에 처리되도록 함으로써,
프레임 변동과 관계없이 물리적 동작이 안정적이고 일관되게 동작하도록 설계된 것이다.

전반적으로는 “프레임 의존 로직”과 “시간 의존 물리 연산”을 분리하기 위한 구조라고 보면 된다.

AddForce & Force Mode

AddForce함수는 물리 작용에 많이 사용하는 함수로, 간단히 작용하는 힘에 대한 스칼라와 벡터, 그리고 힘의 종류를 RigidBody에 추가하는 것이다.
당연하지만, RigidBody 가 Enable 상태에서만 동작한다.

해당 함수는 FixedUpdate()에서 진행 하는 것이 일반 적이다.

Force Mode 는 말 그대로 힘의 종류로, 아래와 같이 제공되고 있다.

  • Force: 연속적인 힘. Mass 값에 따라 적용되는 것이 다르다. 현실적인 물리 현상에 자주 쓴다.
  • Acceleration: 오브젝트의 질량과는 관계없이 가속도를 준다.
  • Impulse: 연속적이지 않은 힘. Mass 값에 따라 적용되는 것이 다르다. 짧은 순간의 힘(폭발 같은?)에 자주 쓴다.
  • VelocityChange: 질량에 관계없이 속도를 변경한다. 질량이 다른 오브젝트 여러개를 모두 같은 속도로 이동시킬때 좋다.

Collision

⬆️ 이것이 충돌이다! 라기 보다는 이해를 돕기 위한 그림 입니다. ⬆️

  • OnCollisionEnter; 충돌이 막 발생되었을 때
  • OnCollisionStay; 충돌 진행시? 상대적 사용 빈도 적음.
  • OnCollisionExit; 충돌에서 나왔을 때

Trigger

⬆️ 이것이 트리거다! 라기 보다는 이해를 돕기 위한 그림 입니다. ⬆️

※ 동작해야 하는 Object 의 한쪽에라도 Rigidbody 가 필요하다.

  • OnTriggerEnter; Trigger 를 설정한 영억 내 진입시
  • OnTriggerStay; 영역내 있을 때. ?상대적 사용 빈도 적음?
  • OnTriggerExit; 영역을 나갔을 때

⚠️ 본 링크를 타고 들어가 문서의 아래쪽 충돌 액션 메트릭스를 꼭 읽어봅시다 ⚠️

⬇️아래는 강사님이 정리해 주신 메트릭스.

Static ColliderRigidbody ColliderKinematic Rigidbody ColliderStatic Trigger ColliderRigidbody Trigger ColliderKinematic Rigidbody Trigger Collider
Static Collidercollisiontriggertrigger
Rigidbody Collidercollisioncollisioncollisiontriggertriggertrigger
Kinematic Rigidbody Collidercollisiontriggertriggertrigger
Static Trigger Collidertriggertriggertriggertrigger
Rigidbody Trigger Collidertriggertriggertriggertriggertriggertrigger
Kinematic Rigidbody Trigger Collidertriggertriggertriggertriggertriggertrigger

각 Stay 관련

Collision, Trigger 둘다 Stay 에서 무언가 하는 경우가 많지 않다.
왜냐면 두 함수 모두 충돌/트리거가 계속 진행되고 있을 경우 특정 물리적 현상에 대한 무언가를 하는 경우는 많지 않기 때문이다.
대부분이 게임 로직에 대한 것은 update에서 진행된다.

물론, 물리적인 내용에 대한 구현이 필요하면 OnCollisionStay, OnTriggerStay 에서 하는 것이 맞다.

Physics in C# Script

간단히 물리 엔진 라이브러리이다. C# 코드에서는 물리엔진 관련 함수등을 호출 할 수 있도록 도와주는 라이브러리라 봐도 무방하다.

  • OverlapXXX ; 범위 내 존재하는 Collider 를 반환해주는 메소드. Collider 배열을 반환한다.
    • XXX -> Sphere/Box/Capsule
  • OverlapXXXNonAlloc ; 기본 동작은 OverlapXXX 와 같되 반환 값이 integer로 되어 있다. (참조된 GameObject 의 참조값)
    • XXX -> Sphere/Box/Capsule

NonAlloc 이 있고 없고의 차이.

Physics.OverlapSphere()Physics.OverlapSphereNonAlloc()
메모리 사용량새로운 배열을 할당하여 결과를 반환기존 배열을 사용하여 결과를 반환
성능메모리 할당 및 해제 과정이 추가되어 성능이 저하될 수 있음메모리 할당 및 해제 과정이 없기 때문에 성능이 향상될 수 있음
사용법새로운 배열을 생성하여 결과를 저장해야 함기존 배열을 사용하여 결과를 저장할 수 있음
// Physics.OverlapSphere() 메서드 사용 예
int numColliders = Physics.OverlapSphere(position, radius);
Collider[] colliders = new Collider[numColliders];
Physics.OverlapSphere(position, radius, colliders);

// Physics.OverlapSphereNonAlloc() 메서드 사용 예
Collider[] colliders = new Collider[10];
int numColliders = Physics.OverlapSphereNonAlloc(position, radius, colliders);

출처: 끄적끄적 코딩 공방

다른 CS Script 불러오기

개발 중, Object 에 물려있는 다른 Script 를 불러오거나, 자신의 자식 혹은 부모 Object 에 있는 Script 를 불러오는 경우도 있다.
이럴때 유용하게 사용할 수 있는 내용에 대해 간단히 정리한다.

// 해당 Script Class 위에 사용한다.
[RequireComponent(typeof(CLASSNAME001))] // 필요한 Script 가 인스팩트에 자동으로 추가되며, 인스팩트에서 삭제할 수 없다. (강제한다.)
public class CLASSNAME000 : MonoBehaviour
{
    [SerializeField] private CLASSNAME001 _className001;
}

GetComponent<T>(); // 제네럴T 는 찾으려고 하는 스크립트를 현재 gameObject 내에서만 찾는다. 즉, 유니티 에디터 의 GameObject 에 인스팩터에 등록은 해놔야한다.
GetComponent<interface>(); // Interface 형도 사용 가능하다.
GetComponentInChildren<T>();  // 자식 gameObject 에 대해 모두 찾는다.
GetComponentInParent<T>(); // 최상위 부모 gameObject 까지 모두 찾는다.

// 아래 함수들은 모두 배열로 받는다.
GetComponents<T>(); 
GetComponentsInChildren<T>();
GetComponentsInParent<T>();

// Element 로부터 gameObject 를 가져올 수도 있다 (물론 다른 방법으로 Object 를 불러 올 수도 있다.)
_className001.gameObject; 

GetComponentUpdate에서 사용하는 것은 좋지 않다.
기본적으로 GetComponent는 선형탐색을 통하여 게임오브젝트내 모든 컴포넌트를 확인하고 지정된 타입과 일치할 경우 반환하는 형태로 되어 있다.
즉, CPU 에 영향이 큰 동작을 한다. 또한, 반환시 매번 새로운 참조를 생성하기 때문에 캐싱하지 않거나 로딩한 참조를 반환 및 정리하지 않으면 GC 성능 문제로 이어질 수도 있다.

따라서, GetComponentAwakeStart등에서 한번만 검색하고 캐싱해 둔 상태로 사용하는 것을 권장하는 편이다.

Layer

물리 작용에 대한 대상에 대한 그룹 관리 로 볼 수 있다.

묶인 Layer 를 통해 Layer 간 충돌에 대한 Logic 을 구현하면 비교적 쉽게 구현이 가능하(다고한)다.
가령, Monster 와 Player, Ground, 그외 맵 내 Object 들에 대해 Layer 를 각각 주고
각 Layer 간 충돌에 대한 규칙을 상세히 설계하면 tag 나 name 을 이용한 추가적인 작업 없이
일괄적으로 적용하는 것이 가능하다는 것이다.

과거 GPT 를 이용한 게임 만들기에서도
적끼리는 겹치지 못하게, Player 의 모함과도 겹치지 못하게,
단, 모함이 생성한 탐재기는 충돌은 없도록 할때 Layer 를 활용(하라고 GPT가 말)하였다.

Layer Bitmask 계산하기

다음 교안에도 배우지만 먼저 이야기 하자면..
Bit 연산을 이용하여 Layer Matching 을 찾는 것을 할 수 있다.

"IP Network에서 netmask 를 이용하여 network id 와 host id 를 뽑는 것처럼"
4byte 로 이루어진 layer 값을 32개의 bit 를 이용하여 and연산을 통해 matching 되는 bit 를 찾는 것이다.
그래서 실제 Unity Editor 내에서도 Layer 는 최대 32개(이미 given 으로 주어진 것들이 4~5개 정도 있다)까지 생성할 수 있다.

// 간단한 예시.
// bit shift 를 꼭 해줘야 한다.
if (((1 << other.gameObject.layer) & _layerMask.value) != 0)
{
    _helicopterController = other.gameObject.GetComponent<HelicopterController>();
}

Gizmo

❓이친구는 아니다.⬆️

오브젝트를 만들었을 때 보이던 일종의 뼈대 같이 보이던 그것.

오브젝트를 선택 하면 보이긴 하다만, 매번 찾아서 클릭해 보는 것은 귀찮은 짓이다.

기즈모는 해당 오브젝트의 위치나 기울어진 정도등을 잘 확인할 수 있도록 도와주는 녀석이다. 하지만 빈 오브젝트를 생성하거나 오브젝트가 매우 작을 경우 이를 확인하는 것은 매우 어렵다. 때문에 아래와 같이 임의적으로 오브젝트를 확인하기 위해 기즈모의 형태 및 색상등을 변경하면 조금 더 보기 편해진다

디버깅 용도로 사용하면 굉장히 편리하다!

profile
Python Dev with Infra -> Game Programmer

0개의 댓글