게임 물리 시뮬레이션의 계산 비용과 프레임 간 상관관계
물리 연산은 매 프레임 완전히 새로 계산되지 않는다. 충돌 후보와 접촉 상태가 이어진다는 점을 활용해 비용을 줄이고 그 전제가 깨질 때 생기는 성능 문제를 살펴본다.

물리 비용은 오브젝트 수만으로 결정되지 않는다
게임에서 물리 시뮬레이션 비용을 이야기할 때 흔히 “물체가 많으면 느려진다”고 말한다. 방향은 맞지만 실제 병목을 설명하기에는 부족하다. 같은 수의 물체라도 서로 멀리 떨어져 있으면 비용이 작고 한 지점에 몰려 접촉과 제약 조건을 많이 만들면 급격히 비싸진다.
프레임당 물리 비용은 대체로 다음 단계의 합으로 볼 수 있다.
- Broad phase: 충돌할 가능성이 있는 쌍을 넓게 거른다.
- Narrow phase: 실제 형상끼리의 교차 여부와 접촉점, 법선을 계산한다.
- Solver: 관통을 해소하고 마찰·반발·관절 같은 제약을 반복 계산한다.
- Sync: 물리 결과를 렌더링용 트랜스폼과 맞춘다.
이 중 broad phase는 공간 분할 구조를 잘 유지하면 비교적 예측 가능하다. 반면 narrow phase와 solver는 충돌 군집의 크기, 복잡한 콜라이더, 반복 횟수에 영향을 크게 받는다.
연속된 프레임은 대개 비슷하다
대부분의 게임에서 한 프레임과 다음 프레임의 물리 상태는 급격히 달라지지 않는다. 바닥 위 상자가 다음 프레임에 전혀 다른 물체가 되는 일은 드물다. 이 성질을 시간적 일관성(temporal coherence) 이라고 부른다.
물리 엔진은 이를 이용해 이전 프레임의 정보를 재사용한다. 이미 겹치지 않았던 콜라이더 쌍은 다음 프레임에도 대체로 겹치지 않는다. 반대로 바닥과 상자처럼 계속 접촉하는 쌍은 접촉점과 충격량을 다시 활용할 수 있다.

이 재사용은 단순한 미세 최적화가 아니다. 프레임 간 상관관계가 높은 평상시 장면에서는 매번 처음부터 풀지 않아도 되므로 안정적인 접촉과 낮은 계산 비용을 함께 얻을 수 있다.
웜 스타팅이 솔버 비용을 줄이는 방식
반복형 제약 솔버는 접촉 제약을 여러 차례 갱신하며 해를 근사한다. 이전 프레임에서 계산한 충격량을 초기값으로 넣는 방식을 웜 스타팅이라고 한다. 정지해 있거나 천천히 움직이는 물체에서는 이전 해가 다음 해와 가깝기 때문에 적은 반복으로도 충분한 결과를 얻기 쉽다.
개념적으로는 이전 프레임의 충격량을 초기값으로 사용한다.
여기서 는 접촉 또는 관절 제약에 적용한 충격량이다. 실제 구현에서는 콜라이더 쌍뿐 아니라 접촉점의 특징값도 함께 비교해야 한다. 접촉점이 달라졌는데 오래된 충격량을 그대로 적용하면 튀거나 떨리는 현상이 생길 수 있다.
for (Contact& contact : contacts) {
CacheKey key = MakeCacheKey(contact);
if (auto cached = impulseCache.find(key); cached != impulseCache.end()) {
contact.normalImpulse = cached->second.normalImpulse;
contact.tangentImpulse = cached->second.tangentImpulse;
ApplyCachedImpulse(contact);
}
}
for (int iteration = 0; iteration < solverIterations; ++iteration) {
SolveContactConstraints(contacts);
}
StoreImpulses(contacts, impulseCache);
이 예제에서 중요한 점은 캐시가 반복 횟수를 무조건 줄여 주는 장치가 아니라는 것이다. 빠르게 움직이는 물체, 순간이동, 파괴 가능한 지형처럼 상태가 크게 바뀌는 경우에는 캐시 적중률이 떨어지고 검증 비용도 늘어난다.
상관관계가 깨지는 순간이 성능 위험 구간이다
프레임 간 상관관계가 낮아지는 대표적인 상황은 다음과 같다.
- 폭발이나 대규모 낙하처럼 많은 물체가 동시에 깨어날 때
- 순간이동 또는 큰 위치 보정을 적용할 때
- 많은 동적 오브젝트가 좁은 공간으로 몰릴 때
- 메시 콜라이더나 복잡한 복합 형상을 대량으로 사용할 때
- 프레임 지연으로 인해 물리 스텝 하나가 긴 시간을 처리해야 할 때
특히 프레임이 느려져 물리 스텝 시간이 커지면 물체가 더 멀리 이동하고 새로운 충돌 후보가 늘어난다. 그 결과 물리 비용이 다시 증가해 프레임이 더 느려지는 악순환이 생길 수 있다.
고정 시간 스텝을 쓴다면 누적 시간을 제한하는 방어가 필요하다.
accumulator += frameDelta;
accumulator = std::min(accumulator, 0.25f);
while (accumulator >= fixedDelta) {
physicsWorld.Step(fixedDelta);
accumulator -= fixedDelta;
}
이 코드는 긴 일시 정지 뒤에 수십 개의 물리 스텝을 한 프레임에 몰아서 실행하는 상황을 완화한다. 다만 누적 시간을 자르면 시뮬레이션 시간이 실제 시간보다 뒤처질 수 있으므로 대전 게임이나 정밀한 리플레이처럼 결정성이 중요한 장르에서는 별도의 동기화 정책이 필요하다.
측정할 때는 평균보다 분포를 본다
물리 성능을 평균 프레임 시간 하나로 판단하면 문제를 놓치기 쉽다. 평소에는 1ms 안에 끝나더라도 상자가 무너지는 장면에서 12ms가 걸린다면 플레이어는 그 순간을 끊김으로 느낀다.
다음 지표를 프레임별로 기록하면 원인을 좁히기 좋다.
- broad phase 후보 쌍 수
- 실제 접촉 쌍과 접촉점 수
- 깨어 있는 동적 바디 수
- 솔버 반복 횟수와 제약 수
- 물리 스텝 시간의 평균, 상위 백분위, 최댓값
- 웜 스타트 캐시 적중률
캐시 적중률이 낮고 후보 쌍이 급증한다면 장면 배치나 공간 분할 갱신을 의심할 수 있다. 접촉 수는 비슷한데 솔버 시간이 늘어난다면 관절 체인, 마찰 제약, 반복 횟수를 먼저 살펴보는 편이 낫다.
실무에서 적용할 우선순위
먼저 게임에서 허용 가능한 최악의 물리 장면을 정한다. 예를 들어 적 수, 파편 수, 동시에 굴러갈 수 있는 오브젝트 수를 예산으로 둔다. 그 장면을 실제 기기에서 반복 실행하며 비용을 측정해야 한다.
그다음에는 다음 순서로 개선하는 것이 안전하다.
- 필요 없는 충돌 조합을 레이어와 마스크로 제외한다.
- 정적인 배경은 정적 충돌 구조로 묶고 움직이지 않는 동적 바디는 수면 상태로 전환한다.
- 복잡한 메시 대신 단순한 볼록 형상이나 여러 개의 기본 콜라이더를 검토한다.
- 폭발·파편·군집 상황에 오브젝트 수와 활성 시간을 제한한다.
- 정확도 문제가 확인된 경우에만 솔버 반복 횟수나 연속 충돌 검사를 늘린다.
물리 시뮬레이션 최적화의 핵심은 모든 프레임을 같은 비용으로 만드는 데 있지 않다. 연속된 프레임이 비슷하다는 사실을 최대한 활용하되 그 가정이 깨지는 장면을 미리 설계하고 측정하는 데 있다. 인디 게임에서도 이 경계를 분명히 잡아 두면 더 많은 상호작용을 넣으면서 프레임 타임을 관리하기 쉬워진다.



