물리·판정
총알·주먹이 맞았는지 가리는 판정과, 쓰러지고 부서지고 달리는 것을 흉내 내는 물리다.
목차 (14)
- 히트박스와 활성 프레임, 스윕 판정
- 래그돌: 몸을 도형과 관절로 나눈다
- 보로노이 파괴: 무늬를 맞은 자리에 대고 자른다
- 모래가 흐르는 세계: 셀룰러 오토마타와 더티 렉트
- 물속 총알: 이차 항력과 프레임 적분
- 움직이는 발판: 캐릭터 컨트롤러와 속도 상속
- 궤적 예측: 같은 물리를 미래로 미리 돌린다
- 레이캐스트 서스펜션: 자동차는 바퀴 위에 서 있지 않다
- 레이캐스트 바퀴로 달리는 탱크
- 거미줄 스윙: 진자를 속인다
- 투사체 판정: 화면의 총알은 총알이 아니다
- 적분: 같은 두 줄도 순서에 따라 에너지가 샌다
- 충돌 검사의 두 단계: 넓게 거르고 좁게 잰다
- 고전 게임이 벽을 뚫는 이유: 마리오 64의 4분의 1 걸음
히트박스와 활성 프레임, 스윕 판정
액션 게임에서 “분명 닿았는데 안 맞았다”는 불만은 대개 버그가 아니다. 화면에 그리는 캐릭터와 맞았는지를 가리는 판정은 처음부터 서로 다른 물건이고, 둘이 갈리는 자리가 정해져 있다.
판정은 수천 개의 삼각형이 아니라 캡슐·구·상자 같은 단순한 도형으로 한다. 계산이 싸고, 옷자락이 스친 것까지 맞은 것으로 치지 않아도 되기 때문이다. 공격하는 쪽 도형을 히트박스(hitbox), 맞는 쪽 도형을 허트박스(hurtbox)라고 부르는 경우가 많지만, 업계 표준 용어는 아니어서 게임마다 둘 다 히트박스라고 부르기도 한다.
- 활성 프레임: 공격 한 번은 준비(startup) → 판정(active) → 회수(recovery) 순서의 여러 프레임짜리 동작이다. 히트박스는 판정 구간에만 켠다. 팔을 들어 올리는 동안이나 거두는 동안에도 맞으면 공격의 타이밍이 사라지므로, 그림이 상대와 겹쳐 보여도 판정 구간 밖이면 일부러 맞지 않게 한다. 격투 게임의 프레임 데이터가 바로 이 세 구간의 길이다.
- 터널링(tunneling): 게임은 1초에 정해진 횟수만 세상을 본다. 무기나 탄이 한 프레임에 움직이는 거리가 표적의 두께보다 크면, 이번 프레임엔 앞에 있고 다음 프레임엔 뒤에 있어서 겹친 순간이 아예 없다. 프레임마다 지금 위치만 보는 이산 판정(discrete)은 이것을 놓친다.
- 스윕 판정(sweep): 지난 위치에서 지금 위치까지 도형을 쓸어(sweep) 그 사이에 닿은 것을 찾는다. 점이면 선분 검사, 반지름이 있으면 캡슐 검사가 된다. 물리 엔진은 같은 일을 연속 충돌 검사(CCD, continuous collision detection)라는 이름으로 제공하며, Box2D·Unity·Unreal 모두 빠른 물체에 켜는 옵션이 있다.
판정 도형의 크기도 손잡이다. 맞는 쪽은 그림보다 조금 작게 두어 스친 공격을 흘리고, 때리는 쪽은 조금 크게 두어 아슬아슬한 공격을 맞게 하는 식으로 관용을 공격자 쪽에 기울이는 경우가 많다. 맞았어야 할 공격이 안 맞는 경험이 그 반대보다 훨씬 나쁘게 느껴지기 때문이다.
스윕에도 한계가 있다. 회전하며 휘두르는 무기는 끝이 직선이 아니라 호를 그리므로, 선분 하나로는 궤적을 정확히 덮지 못한다. 그래서 무기 끝 여러 점을 따로 스윕하거나 프레임을 잘게 나눠 검사한다. 또 판정은 화면 프레임이 아니라 게임 로직의 고정 박자에 묶어야 결과가 프레임률에 따라 바뀌지 않는다. 온라인 게임에서는 “누구의 화면 기준으로 판정하느냐”가 한 겹 더 붙는데, 그것은 멀티플레이 노트의 랙 보상 이야기다.
래그돌: 몸을 도형과 관절로 나눈다
쓰러지는 동작을 전부 미리 만들어 두면 계단에서 구르거나 난간에 걸리는 장면마다 따로 만들어야 한다. 래그돌(ragdoll)은 반대로 몸을 물리에 맡긴다. 머리·몸통·팔다리를 캡슐이나 상자 같은 강체(rigid body) 몇 개로 나누고, 그 사이를 관절 제약(joint constraint)으로 이어 중력과 충돌에 반응하게 한다. 강체의 움직임은 다시 뼈대(skeleton)에 옮겨져 겉모습도 따라 움직인다.
- 몸체: 모든 뼈에 강체를 둘 필요는 없다. 손가락처럼 작은 뼈는 빼고, 골반·척추 몇 마디·머리·위팔·아래팔·허벅지·종아리 정도를 쓰는 구성이 흔하다. 언리얼은 이 묶음을 Physics Asset에, 유니티는 Ragdoll Wizard가 만든 Collider·Joint 조합에 담는다.
- 관절: 연결 지점이 떨어지지 않게 위치를 묶고, 회전은 축마다 범위를 둔다. 흔히 원뿔 모양으로 휘는 범위(swing)와 뼈를 축으로 비트는 범위(twist)를 따로 정한다. 무릎은 범위를 좁히는 것만으로는 부족하고, 어느 방향으로 접히는지 기준 자세까지 맞춰야 사람처럼 꺾인다.
- 몸 안 충돌: 이웃한 캡슐은 관절 근처에서 서로 겹치기 마련이라, 바로 이어진 몸체끼리는 충돌을 꺼 두는 경우가 많다. 끄지 않으면 충돌이 관절을 밀어내 몸이 떨거나 튄다.
애니메이션에서 물리로 넘기는 방식도 여러 가지다. 가장 단순한 것은 죽는 순간 애니메이션을 멈추고 몸 전체를 시뮬레이션으로 바꾸는 것이다. 맞은 부위 아래만 잠깐 물리로 돌리고 물리 비중(blend weight)을 올렸다 내리면 걷던 캐릭터가 팔만 휘청였다가 원래 동작으로 돌아오는 피격 반응이 된다. 관절에 모터를 달아 애니메이션 자세를 따라가게 하는 액티브 래그돌(active ragdoll)은 GTA IV 같은 게임이 쓴 Euphoria 미들웨어로 널리 알려졌다.
대가도 있다. 같은 조건이라도 접촉 순서나 부동소수점 차이로 결과가 달라질 수 있어, 여러 기기에서 똑같이 재현해야 하는 장면에는 쓰기 어렵다. 또 쓰러진 뒤 일어나는 동작은 래그돌만으로 나오지 않아, 쓰러진 자세(엎드림·누움)를 보고 가까운 기상 애니메이션으로 섞어 넘기는 처리가 따로 필요하다. 관절 각을 직접 계산하는 쪽은 애니메이션 노트의 IK를 보면 된다.
보로노이 파괴: 무늬를 맞은 자리에 대고 자른다
벽이 실제로 깨지는 과정을 응력 해석으로 풀면 실시간 예산을 넘는다. 그래서 게임의 파괴는 보통 세 단계로 나눈다. 조각 모양은 기하 트릭으로 만들고, 어디가 잘게 부서질지는 점의 밀도로 정하고, 물리는 떨어지는 조각에만 켠다.
모양을 만드는 흔한 트릭이 보로노이 분할(Voronoi)이다. 점을 뿌린 뒤, 벽의 각 자리를 가장 가까운 점에 나눠 준다. 점 하나의 영역(셀)은 다른 점들과의 수직이등분선으로 벽을 차례로 잘라 남은 부분이라 언제나 볼록하고, 볼록한 조각은 그대로 싼 충돌체가 된다. 점이 몰린 곳은 조각이 잘고, 성긴 곳은 크다. 같은 방식은 절차 생성 노트에서 지형을 나눌 때도 쓴다.
문제는 어디를 맞을지 미리 모른다는 점이다. 해법은 두 갈래다.
| 방식 | 하는 일 | 장단점 |
|---|---|---|
| 조각까지 미리 자르기 | 제작 단계에서 물체를 계층으로 잘라 둔다(큰 덩어리 → 그 안의 작은 조각). 실행 중에는 충격이 연결 그래프로 퍼지며 한계를 넘은 연결만 끊는다 | 실행 비용이 싸고 아트가 결과를 미리 본다. 대신 맞은 자리에 맞춘 무늬가 아니라 계층과 연결 강도로 그럴듯하게 보이게 한다. 언리얼 Chaos의 Geometry Collection이 이 방식이다 |
| 무늬만 미리, 자르기는 맞는 순간 | 충격점을 원점으로 한 점 무늬를 미리 만들어 두고, 맞은 자리로 옮기고 돌려서 반경 안의 셀로만 물체를 자른다 | 어디를 맞아도 맞은 자리가 잘게 부서진다. 점 집합을 통째로 옮기면 셀 모양도 그대로 따라오므로 무늬 계산은 한 번이면 된다. 실시간 절단 비용이 든다(SIGGRAPH 2013에 실린 실시간 파괴 논문이 대표적이다) |
잘랐다고 다 떨어지는 것은 아니다. 조각끼리 면을 공유하면 이웃으로 적어 연결 그래프를 만들고, 바닥이나 기둥 같은 고정점에서 그래프를 따라 닿는 조각만 붙어 있는 것으로 친다. 고정점과 끊긴 덩어리(섬)는 통째로 한 몸이 되어 떨어진다. 이 판정이 없으면 밑동을 다 부숴도 윗부분이 허공에 떠 있다.
모양이 매번 같아 보이지 않게 무늬를 여러 벌 두거나 회전해서 대는 것이 보통이다. 아예 다른 길도 있다. Teardown처럼 세상을 작은 복셀로 만들면 어디든 아무 모양으로 팔 수 있지만, 칸 수가 많아 연결 판정과 메모리가 비싸다. 보로노이는 조각 수가 적어 싸고, 정해진 조각 단위로만 깨진다.
모래가 흐르는 세계: 셀룰러 오토마타와 더티 렉트
모래가 쏟아지고 물이 고이고 불이 번지는 세계는 강체 덩어리로 만들기 어렵다. 그래서 세계를 칸(셀)으로 나누고 칸마다 재질 하나를 적은 뒤, 칸마다 주변 칸만 보는 짧은 규칙을 매 프레임 돌린다. 이것이 셀룰러 오토마타(cellular automaton)이고, “모래 떨어지기(falling sand)” 장르와 모든 픽셀을 시뮬레이션하는 Noita가 이 위에 서 있다.
- 모래: 아래 칸이 비면 내려가고, 막히면 왼쪽 아래나 오른쪽 아래로 간다.
- 물: 아래와 대각선 아래를 먼저 보고, 막히면 옆으로 흐른다.
- 가스: 물 규칙을 위아래로 뒤집는다. 불은 옆의 탈 것에 확률로 옮겨 붙는다.
- 무거운 재질이 가벼운 재질 위에 있으면 둘을 바꾼다(밀도 비교).
규칙은 몇 줄이지만 훑는 순서가 결과를 바꾼다. 위 줄부터 훑으면 한 칸 내려간 모래를 다음 줄에서 또 만나 한 프레임에 바닥까지 떨어진다. 그래서 아래 줄부터 훑거나, 이번 프레임에 움직인 칸에 표시를 해 두고 건너뛴다. 줄 안에서 늘 왼쪽부터 훑으면 물이 한쪽으로 쏠리므로, 훑는 방향을 프레임마다 뒤집거나 무작위로 섞는다.
성능의 핵심은 “안 움직이는 곳은 보지 않는 것”이다. 세계를 일정한 크기의 조각(chunk)으로 나누고, 조각마다 이번 프레임에 바뀐 칸을 감싸는 사각형(더티 렉트, dirty rect)을 적어 두었다가 다음 프레임엔 그 안만 훑는다. 다 쌓여 멈춘 모래밭은 거의 비용이 들지 않는다. 물리 엔진이 멈춘 물체를 잠재웠다가(sleep) 부딪히면 깨우는 것과 같은 생각이다.
대가는 “아무도 깨우지 않은 칸은 아무도 보지 않는다”는 점이다. 마인크래프트에서 세계 생성 때 만들어진 모래가 아래가 비어 있는데도 떠 있다가, 옆 블록을 건드리는 순간(블록 업데이트) 무너지는 것이 같은 성질이다. 처음부터 전부 훑으면 생기지 않지만, 그 대신 매 프레임 모든 칸을 보는 비용을 낸다.
여러 코어로 나눌 때도 함정이 있다. 이웃한 조각을 동시에 돌리면 두 스레드가 경계의 같은 칸을 동시에 고쳐 모래가 사라지거나 복제된다. 흔한 해법은 조각의 가로·세로 번호가 짝수인지 홀수인지로 네 무리를 만들고, 같은 무리끼리만 동시에 돌린 뒤 다음 무리로 넘어가는 것이다. 같은 무리 조각은 한 조각씩 떨어져 있어, 칸이 조각 밖으로 반 조각 이내만 움직이면 쓰는 범위가 겹치지 않는다. 흑백 두 색으로만 나누면 대각선으로 붙은 조각이 같은 차례가 되어 모서리에서 겹친다. 갱신을 줄이는 다른 기법은 엔진 구조 노트에 있다.
물속 총알: 이차 항력과 프레임 적분
물속으로 뛰어들면 총알이 안 닿는 게임이 많은데, 이것은 꾸며 낸 규칙만은 아니다. 탄을 늦추는 힘은 속도의 제곱에 비례하는 이차 항력(quadratic drag)이다.
감속은 a = −k·v²로 쓰고, 계수 k는 매질의 밀도·항력 계수·단면적에 비례하고 탄의 질량에 반비례한다(k = ρ·Cd·A ÷ 2m). 물의 밀도는 공기의 800배가 넘으므로 같은 탄이라도 k가 그만큼 커진다. 이 식의 해는 거리에 대해 지수로 줄어(v = v₀·e−k·x), 공기에서 수백 m를 날아갈 탄이 물속에서는 1~2m 남짓에 힘을 잃는다. 실제 실험에서도 대부분의 총탄은 물속 몇 m 안에서 살상력을 잃는다.
게임에서 이걸 프레임마다 계산할 때 함정이 둘 있다.
- 그냥 빼면 터진다. 가장 단순한 갱신 v ← v − k·v²·dt는 k·v·dt가 1보다 작을 때만 안정하다. 공기에서는 문제없지만 물에서는 이 값이 1을 훌쩍 넘어, 한 프레임에 지금 속도보다 큰 양을 빼서 부호가 뒤집힌다. 물에 닿는 순간 탄이 뒤로 튀어 사라지는 버그가 된다. 이 식은 닫힌 해가 있어서 v ← v ÷ (1 + k·v·dt)로 갱신하면 dt가 얼마든 부호가 바뀌지 않는다. 프레임을 잘게 나눠 여러 번 적분해도 되지만 나누는 수가 적으면 똑같이 터진다.
- 수면을 지나는 프레임은 나눈다. 빠른 탄은 한 프레임에 몇 m를 간다. 그 사이에 수면이 있으면 프레임 끝의 매질로 통째로 계산해서는 안 된다. 물로 치면 아직 공중에 있는 탄이 미리 느려지고, 공기로 치면 수면 아래 몇 m를 그대로 관통한다. 수면과 만나는 점을 구해 앞부분은 공기 식, 뒷부분은 물 식으로 나눠 계산한다.
| 게임이 고르는 방식 | 물속 대상 | 특징 |
|---|---|---|
| 투사체로 날리며 매질별 항력 적분 | 짧은 거리 안에서만 맞는다 | 가장 그럴듯하지만 위의 두 함정을 처리해야 한다 |
| 히트스캔 선을 수면에서 끊기 | 맞지 않는다 | 싸고 단순하다. 물이 완전한 방패가 된다 |
| 판정은 그대로, 물속 탄은 그림만 | 물 밖과 똑같이 맞는다 | 물이 안전하지 않다. 판정과 화면이 서로 다른 말을 한다 |
| 물속 사격 자체를 막기 | 해당 없음 | 전용 수중 총기만 쏠 수 있게 하는 밀리터리 게임(아르마 3 등)이 있다 |
움직이는 발판: 캐릭터 컨트롤러와 속도 상속
현실에서는 움직이는 발판 위에 서 있으면 마찰 덕분에 그냥 같이 간다. 게임 캐릭터는 대개 그렇지 않다. 캐릭터의 몸은 물리 엔진이 미는 강체가 아니라, 이동 코드가 매 프레임 “이번에 갈 자리”를 정해 옮기는 캡슐이기 때문이다. 이런 몸을 운동학적 캐릭터 컨트롤러(kinematic character controller)라고 한다. 가려는 방향으로 캡슐을 미리 쓸어 보고(스윕), 벽에 닿으면 미끄러뜨리고, 낮은 턱은 올라탄다.
캐릭터를 굳이 이렇게 만드는 이유는 조작감이다. 물리 몸으로 만들면 발판은 공짜로 타지만, 점프가 물렁해지고 계단에서 튀고 모서리에 낀다. 대신 마찰이 없으니 발판을 타는 일을 코드로 따로 짜야 한다.
- 발밑의 베이스 찾기: 컨트롤러는 어차피 매 프레임 발밑으로 짧게 쓸어내려 바닥을 찾는다. 그때 닿은 물체를 적어 두고, 움직일 수 있는 물체면 베이스(base)로 삼는다. 검사는 발바닥보다 조금 위에서 시작해야 한다. 바닥에 딱 붙은 채 시작하면 처음부터 겹친 것으로 나와 바닥을 놓친다.
- 발판이 간 만큼 먼저 옮기기: 이번 프레임에 베이스가 움직인 양(델타)을 캡슐에 더한다. 자기 이동을 계산하기 전에 더해야 한다. 뒤에 더하면 한 프레임씩 늦게 따라가 앞뒤로 떨린다. 회전 발판이면 델타가 회전이므로 발판 중심을 축으로 캐릭터의 자리와 바라보는 방향을 함께 돌린다. 발을 땅에 고정하는 발 IK를 쓴다면 디딘 발도 같은 델타로 옮겨야 다리가 늘어나지 않는다.
- 떠날 때 속도 물려주기: 달리는 발판에서 점프하면 베이스가 사라지니 더할 델타가 없고, 캐릭터는 제자리에 떠서 발판 뒤로 떨어진다. 점프하는 순간 발판 속도를 캐릭터 속도에 더해 관성을 흉내 낸다.
엔진마다 어디까지 자동인지가 다르다. 언리얼의 Character Movement는 바닥 검사 결과로 베이스를 정하고 자기 이동 전에 베이스 이동을 적용하며, 떠날 때 속도를 물려줄지는 축마다 옵션이다. 고도(Godot)의 CharacterBody는 발판 속도를 자동으로 더하고, 떠날 때 “속도를 더한다 / 위쪽 성분만 더한다 / 아무것도 안 한다” 중에서 고른다. 위쪽 성분만 더하면 내려가는 엘리베이터에서 뛰어도 아래로 끌려 내려가지 않는다. 유니티의 기본 CharacterController는 발판 처리가 없어 직접 넣어야 한다.
캐릭터를 발판의 자식 오브젝트로 붙이는 간단한 방법도 있지만, 발판이 크기를 바꾸거나 물리 몸일 때 문제가 생기기 쉽다. 베이스가 물리로 움직이는 물체라면 물리 계산이 끝난 뒤에 델타를 적용해야 한 프레임 어긋나지 않는다. 옆에서 미는 문이나 발판은 델타가 아니라 캡슐의 밀어내기(스윕과 미끄러뜨리기) 문제다.
궤적 예측: 같은 물리를 미래로 미리 돌린다
수류탄을 던지기 전에 뜨는 점선이 벽에 튕기는 것까지 맞는다면, 그것은 포물선 공식으로 그린 선이 아니다. 공식에는 벽도 바닥도 없다. 그 점선은 물체가 실제로 날아갈 때와 똑같은 물리 단계를 미래로 수백 번 미리 밟아 본 자취다.
- 속도에 중력 × dt를 더한다(dt는 단계 하나의 시간).
- 바뀐 속도로 dt 동안 위치를 옮긴다.
- 지난 위치에서 새 위치까지 선(또는 물체 반지름만큼의 구)을 쏴 장애물이 있는지 묻는다.
- 닿았으면 그 점으로 되돌리고 튕긴다. 면에 수직인 속도 성분은 뒤집어 반발 계수를 곱하고, 면을 따라가는 성분에는 마찰을 곱한다.
핵심은 예측과 실제가 같은 함수, 같은 dt를 쓰는 것이다. 이렇게 일정한 가속도를 단계마다 더해 가는 적분(반암시적 오일러)은 위치에 간격에 비례하는 오차를 남긴다. 위 순서대로 하면 정확한 포물선보다 ½·g·dt·t만큼 더 떨어진다. 그래서 예측만 싸게 하려고 단계를 성기게 잡으면 점선이 가리키는 곳과 물체가 떨어지는 곳이 어긋나고, 조준선으로서 쓸모가 없어진다.
비용은 단계 수에 비례한다. 단계마다 선 검사를 한 번씩 하므로, 조준하는 동안에만 돌리고 단계 수(내다보는 시간)에 상한을 둔다. 멀리 갈수록 점을 성기게 찍는 것은 그리는 양만 줄일 뿐 계산 정확도와 상관없다. 언리얼은 이 구조를 PredictProjectilePath 같은 함수로 기본 제공한다.
한계도 분명하다. 회전이 붙은 물체의 비대칭 튕김, 움직이는 발판, 다른 물체와의 충돌은 단순 예측에 잘 안 들어간다. 여러 기기가 같은 궤적을 봐야 한다면 부동소수점까지 맞춰야 하는데, 그것은 멀티플레이 노트의 결정적 재생과 같은 문제다.
레이캐스트 서스펜션: 자동차는 바퀴 위에 서 있지 않다
레이싱 게임의 차는 바퀴가 땅을 밀어 나아가는 것처럼 보이지만, 많은 차량 물리는 바퀴 모양을 땅과 충돌시키지 않는다. PhysX와 Bullet의 차량 기능은 차를 상자 하나(차체 강체)로 보고, 바퀴 자리마다 아래로 레이를 쏴 땅까지 거리를 잰다. 그 거리에서 스프링이 얼마나 눌렸는지를 구하고, 그만큼의 힘으로 상자를 밀어 올린다. 차체가 레이 위에 떠서 간다고 보면 된다.
- 서스펜션 힘: F = k × 압축량 − c × 압축 속도. 앞 항이 스프링, 뒤 항이 출렁임을 줄이는 댐퍼다. 힘은 레이가 닿은 자리에서 차체에 준다. 이 힘이 곧 그 바퀴가 땅을 누르는 하중이 되어 타이어 힘 계산에 쓰인다.
- 바퀴는 숫자로 돈다: 바퀴마다 회전 속도 하나를 갖는다. 바퀴 둘레가 도는 속도와 차가 실제로 가는 속도의 차이가 슬립 비(slip ratio)이고, 바퀴가 향한 쪽과 실제로 가는 쪽 사이의 각이 슬립 각(slip angle)이다. 미끄러진 정도를 가로축으로 하는 타이어 곡선에서 힘을 읽고 하중을 곱하면 타이어 힘이 나온다. 실차 모델로는 Pacejka의 “매직 포뮬러”가 유명하지만, 게임은 손으로 다듬기 쉬운 단순한 곡선을 쓰는 경우가 많다.
- 구동 방식: 전륜·후륜·사륜 구동은 엔진 토크를 어느 바퀴의 회전에 더하느냐의 차이다. 앞뒤 슬립을 함께 보는 모델에서는 휠스핀이 난 바퀴가 옆으로 버티는 힘도 잃어서, 후륜 차는 가속을 끝까지 밟으면 뒤가 미끄러지고(오버스티어) 전륜 차는 덜 돈다(언더스티어).
타이어 힘을 직접 계산하는 구조라서 현실에 없는 어시스트를 넣기도 쉽다. 아케이드 레이싱은 그립을 현실보다 높이되 차가 쉽게 뒤집히지 않도록 타이어 힘이 차체를 기울이는 몫을 줄이고, 드리프트하는 동안 깎이는 속도를 보태 주는 힘을 더한다. 게임패드로 조작하는 차가 손에 붙는 것은 이런 보정 덕이 크다.
반대쪽 선택지도 있다. 바퀴를 진짜 원통 강체로 만들고 관절로 차체에 달면 바퀴가 실제로 턱에 걸리고 구르지만, 관절이 많아 계산이 비싸고 조율이 어렵다. BeamNG.drive는 더 나아가 차 전체를 질량을 가진 점(노드)과 스프링 역할을 하는 막대(빔)로 계산해 찌그러짐까지 만든다. 레이캐스트 차는 가볍고 다루기 쉬운 대신, 바퀴의 옆면이 벽에 닿는 일 같은 것은 따로 처리해야 한다.
레이캐스트 바퀴로 달리는 탱크
탱크의 궤도는 땅을 물고 돌며 차체를 끄는 것처럼 보이지만, 많은 게임에서 탱크를 움직이는 계산에는 궤도가 없다. PhysX의 탱크 차량은 앞 절의 레이캐스트 차에서 한쪽 바퀴들이 모두 같은 속도로 돌도록 묶은 것이다. 이 묶음이 궤도의 효과를 흉내 내고, 관절로 이은 궤도 링크 수십 개를 시뮬레이션하는 비용을 피한다.
- 돌기: 좌우 궤도(바퀴 묶음)의 속도 차이로 돈다(스키드 스티어, skid steer). 양쪽에 반대 방향 속도를 주면 제자리에서 돈다.
- 받치기: 바퀴 자리마다 레이와 스프링을 두는 것은 자동차와 같다. 보기륜이 여섯 개면 레이가 한쪽에 여섯 줄이다.
- 보이는 궤도: 따로 그린다. 텍스처만 흘리는 방법, 바퀴를 지나는 곡선(스플라인)을 따라 링크 모델을 늘어놓는 방법, 링크를 스프링으로 이은 체인을 땅에 부딪혀 처지고 출렁이게 하는 방법이 있다. 뒤로 갈수록 그럴듯하지만, 그리기만 바뀔 뿐 탱크의 움직임은 그대로다.
그래서 이 구조에서는 보이는 궤도를 꺼도 탱크가 똑같이 달린다. 끊어진 궤도도 그림일 뿐이라 그대로 달릴 수 있어, 게임은 보통 궤도가 파괴되면 해당 쪽 구동을 끄는 규칙을 따로 둔다.
반대로 궤도 자체가 땅을 밟게 하는 게임도 있다. 링크를 하나하나 시뮬레이션해 궤도가 땅에 닿은 곳마다 받치는 힘과 견인력이 생기게 하면, 바퀴 위치에서 아래로만 쏘는 레이가 알지 못하는 턱의 앞면을 궤도가 타고 넘을 수 있다. 궤도의 무게가 가속에 영향을 주고, 끊어진 궤도가 실제로 바퀴 밑에서 빠져나가 탱크가 선다. 대신 체인을 안정적으로 풀려면 계산을 훨씬 자주 해야 하고, 강체 수가 많아 비용이 크다. 그래서 궤도 링크를 몇 개씩 묶어 강체 수를 줄이는 엔진도 있다.
거미줄 스윙: 진자를 속인다
거미줄 스윙은 진자 시뮬레이션이다. 몸이 추, 줄이 붙은 점이 피벗(pivot)이다. 진자에 걸리는 힘은 중력과 줄의 장력 둘이고, 장력은 몸을 피벗 둘레의 원(구) 위에 묶어 둔다. 게임에서는 장력을 힘으로 계산하기보다, 매 프레임 중력으로 몸을 움직인 뒤 피벗과의 거리가 줄 길이를 넘으면 몸을 구 위로 되돌리고 바깥으로 나가는 속도 성분을 지우는 거리 제약(distance constraint)으로 푸는 경우가 많다.
마블 스파이더맨(2018)은 GDC 2019 발표(영상)에서 이 구조와 속임수를 공개했다. 첫 문제는 줄을 어디에 붙일지다. 플레이어는 가고 싶은 방향만 스틱으로 전하고, 붙는 점은 게임이 고른다. 레이를 여러 발 쏴서 찾으면 줄이 길수록 레이 사이 틈이 커지고, 스윙하기 좋은 건물 모퉁이를 잘 못 찾는다. 그래서 건물을 단순한 상자로 감싸 두고, 가까운 상자의 면 위에 후보 점을 만든 뒤 점수를 매긴다. 점수에는 입력 방향과 얼마나 맞는지, 줄 길이와 기울기가 알맞은지 같은 항목이 들어가고, 가장 높은 후보 하나에만 마지막으로 레이를 쏴 실제 붙는 점을 얻는다.
두 번째 문제는 물리 그대로면 벽 쪽으로 끌려간다는 것이다. 진자는 피벗 바로 밑으로 몸을 데려가려 하므로, 길 한가운데로 가려는 캐릭터가 줄을 건 건물 쪽으로 계속 휘고, 반대로 조종하면 다음 줄은 반대편 건물에 걸려 지그재그가 된다. 해법은 속임수다.
- 피벗 옮기기: 화면의 줄 끝은 벽에 그대로 두고, 계산에 쓰는 피벗만 벽에서 떼어 몸이 가려는 선 위쪽으로 옮긴다. 그 뒤 시간이 지나거나 방향을 틀면 천천히 벽의 점으로 되돌린다.
- 피벗 올리기: 지붕 높이쯤에서는 붙을 곳이 몸보다 충분히 높지 않아 줄이 안 걸린다. 피벗이 지붕 모서리보다 위 허공에 놓이는 것을 몰래 허용한다.
- 줄 줄이기: 아크의 가장 낮은 곳이 땅이나 넘어갈 건물 지붕에 닿을 것 같으면 줄을 줄여 피해 간다.
원칙은 “되도록 보이지 않게 속인다”이다. 물리에서 너무 멀리 벗어나면 플레이어가 알아채고 싫어하지만, 피벗을 옮기는 정도는 오히려 기대에 더 가깝게 움직인다고 느낀다. 진자의 장점(속도가 붙고 풀리는 리듬)은 남기고, 진자의 단점(원치 않는 쪽으로 끌림)만 고치는 것이다. 이런 보정은 조작감 노트의 코요테 타임과 같은 계열이다.
투사체 판정: 화면의 총알은 총알이 아니다
총 쏘는 게임의 탄은 크게 두 가지로 처리한다.
| 방식 | 하는 일 | 쓰는 곳 |
|---|---|---|
| 히트스캔(hitscan) | 쏘는 프레임에 총구(또는 카메라)에서 선을 한 번 쏴서 처음 닿은 것을 맞은 것으로 한다. 이동 시간도 낙차도 없다 | 근거리 교전이 많은 FPS의 소총·권총. 판정이 싸고 예측하기 쉽다 |
| 투사체(projectile) | 위치와 속도를 가진 점을 여러 프레임에 걸쳐 날린다. 중력·공기 저항으로 속도가 바뀌어 낙차와 도달 시간이 생긴다 | 로켓·화살·수류탄, 장거리 저격이 중요한 밀리터리·배틀로얄 게임 |
투사체라고 해서 총알 모양의 물리 몸을 날리는 것은 아니다. 판정하는 쪽은 보통 위치·속도·수명 몇 개의 값이고, 매 틱 속도에 중력을 더하고 그만큼 위치를 옮긴다. 빠른 탄은 한 프레임에 몇 m를 건너뛰므로 위치만 보면 표적을 지나친다. 그래서 앞의 스윕 판정처럼 지난 위치에서 새 위치까지 선을 그어(레이캐스트) 걸린 것을 찾는다. 퀘이크 III의 공개 소스도 로켓을 옮길 때마다 이렇게 하고, 언리얼의 ProjectileMovement도 기본으로 스윕하며 옮긴다. 굵기가 필요하면 선 대신 작은 구로 스윕하면 된다.
화면에 보이는 총알(예광탄, 이펙트)은 그 점을 따라 그리는 그림이다. 판정과 그림을 떼어 두면 그림은 마음대로 길게 늘이거나 빛나게 할 수 있고, 판정은 단순한 값 몇 개로 남는다. 판정 점이 먼저 표적에 걸리고 그림은 한 프레임쯤 뒤에 도착하는 일도 생긴다.
멀티플레이에서는 같은 점을 서버도 따로 계산한다. 쏜 사람의 클라이언트가 즉시 탄을 보여 주고(클라이언트 예측), 서버는 쐈다는 메시지를 받아 같은 발을 다시 계산하거나 간단한 모델로 검증한다. 내 화면에서 피가 튀었는데 피해가 안 들어가는 것은 서버가 그 명중에 동의하지 않은 것이다. 같은 결과를 내려면 서버와 클라이언트가 같은 물리 식과 같은 시간 간격을 써야 한다. 더 자세한 내용은 멀티플레이 노트와 네트워크 노트에 있다.
적분: 같은 두 줄도 순서에 따라 에너지가 샌다
게임 물리는 “가속도가 속도를 바꾸고 속도가 위치를 바꾼다”는 미분 방정식을 dt 칸으로 끊어 푼다. 이것을 수치 적분(numerical integration)이라고 한다. 식은 같아도 어떤 값을 먼저 갱신하느냐에 따라 오래 돌렸을 때의 결과가 크게 갈린다.
- 명시적 오일러(explicit Euler): 위치는 단계 시작의 속도로, 속도는 단계 시작의 가속도로 옮긴다. 가장 먼저 떠올리는 방식이지만, 용수철이나 궤도처럼 되돌리는 힘이 있으면 단계마다 에너지가 조금씩 불어난다. 위 그림의 30fps 용수철은 1.5초 만에 진폭이 2.6배가 된다. 아무도 밀지 않은 진자가 점점 크게 흔들리는 버그가 여기서 나온다.
- 반암시적 오일러(semi-implicit Euler, symplectic Euler): 속도를 먼저 바꾸고 바뀐 속도로 위치를 옮긴다. 코드로는 두 줄의 순서만 다른데, 에너지 오차가 한쪽으로 쌓이지 않고 위아래로 오르내리기만 해서 오래 돌려도 터지지 않는다. 계산량은 명시적 오일러와 같아서 게임 물리 엔진이 흔히 기본으로 쓴다.
- 베를레(Verlet): 속도를 따로 들고 있지 않고, 지금 위치와 바로 전 위치의 차이를 속도로 본다(새 위치 = 2 × 지금 − 전 + a·dt²). 점을 직접 옮겨도 속도가 저절로 따라오므로, 줄 길이를 지키려고 점을 끌어당기는 거리 제약과 잘 맞는다. 천·밧줄·간단한 래그돌에 많이 쓰이고, 위의 거미줄 스윙도 같은 생각이다. 대신 dt가 바뀌면 “전 위치와의 차이”의 뜻이 달라져 고정 간격이 거의 필수다.
- 룽게-쿠타 4차(RK4): 한 단계 안에서 기울기를 네 번 재 평균한다. 같은 dt에서 훨씬 정확하지만 힘 계산이 네 배이고, 충돌과 접촉이 끼어들면 그 정확도를 살리기 어렵다. 궤도 시뮬레이션처럼 매끄러운 힘만 있는 곳에서 이득이 크다.
어느 방식이든 결과는 dt에 따라 달라진다. 같은 점프가 기기마다 다른 높이로 뛰지 않게 하려면 물리를 화면 프레임이 아니라 정해진 크기의 dt로만 돌려야 하는데, 흐른 시간을 모아 두었다가 고정 칸으로 잘라 쓰는 누산기와 그 사이를 섞어 그리는 보간은 엔진 구조 노트에 있다. 적분 방식을 손으로 비교해 보고 싶다면 Integration Basics와 Fix Your Timestep! 두 글이 출발점으로 좋다.
충돌 검사의 두 단계: 넓게 거르고 좁게 잰다
물체가 1,000개면 두 개씩 짝지은 쌍은 50만 개쯤 된다. 매 프레임 모든 쌍의 실제 도형을 비교할 수는 없으니, 물리 엔진은 검사를 두 단계로 나눈다. 싸고 거친 검사로 “닿을 수도 있는 쌍”만 남기고, 남은 쌍만 비싸고 정확한 검사로 잰다.
- 넓은 단계(broad phase): 모든 물체를 축에 나란한 상자(AABB)로 감싸 상자끼리 겹치는 쌍만 후보로 남긴다. 상자를 빨리 찾는 구조로는 균일 격자, 한 축으로 구간의 시작·끝을 정렬해 겹치는 구간만 훑는 정렬 후 훑기(sweep and prune), 상자를 나무 모양으로 묶는 경계 볼륨 계층(BVH)이 있다. 움직이는 물체의 상자를 조금 넉넉하게 잡아 두면 매 프레임 나무를 고치지 않아도 된다.
- 좁은 단계(narrow phase): 후보 쌍의 실제 도형을 비교해 닿았는지, 얼마나 파고들었는지, 어느 쪽으로 밀어내야 하는지를 구한다. 구와 캡슐은 중심(또는 중심선) 사이 거리를 반지름 합과 견주면 끝난다.
상자나 다각형처럼 모서리가 있는 볼록 도형에는 분리축 정리(SAT, separating axis theorem)를 많이 쓴다. 두 볼록 도형이 떨어져 있으면 둘 사이를 가르는 직선(3D에서는 평면)이 반드시 있고, 그 직선에 수직인 축으로 두 도형의 그림자를 떨어뜨리면 두 구간이 떨어진다. 그래서 후보 축마다 그림자 구간을 견주어 하나라도 떨어지면 곧바로 “안 닿음”이고, 모든 축에서 겹치면 닿은 것이다.
- 2D 다각형은 두 도형의 변마다 법선 방향만 축으로 보면 충분하다. 사각형 둘이면 축은 많아야 네 개다.
- 3D 상자 둘은 각 상자의 면 법선 3개씩에, 두 상자의 모서리 방향끼리 외적한 9개를 더한 15개 축을 본다. 모서리끼리 비스듬히 부딪는 경우는 외적 축에서만 잡힌다.
- 겹친 양이 가장 작은 축이 밀어낼 방향이고, 그 겹친 양이 파고든 깊이다. 둘을 곱한 것이 최소 이동 벡터(MTV)로, 충돌 해소가 이 값만큼 물체를 떼어 놓는다.
오목한 도형은 SAT가 통하지 않아 볼록 조각 여러 개로 나눠 둔다(볼록 분해). 둥근 면이 섞인 일반 볼록 도형에는 GJK와 EPA 같은 반복 알고리즘을 쓰는 엔진이 많다. 어느 쪽이든 이 검사는 프레임 끝의 자리만 보므로, 빠른 물체가 얇은 벽을 건너뛰는 터널링은 따로 막아야 한다. 맨 위 절의 스윕 판정이 그 답이다.
고전 게임이 벽을 뚫는 이유: 마리오 64의 4분의 1 걸음
옛 게임의 벽 통과 버그는 대개 판정을 싸게 만들려고 고른 단순화의 이면이다. 슈퍼 마리오 64는 좋은 예다. 이 게임은 디컴파일 코드가 공개되어 있어 구조를 직접 확인할 수 있다.
- 한 프레임을 네 걸음으로: 마리오의 이동은 프레임마다 속도를 네 등분해 네 번 옮기고, 걸음마다 벽과 바닥을 찾는다. 터널링을 줄이는 서브스텝(substep)이다. 그래도 각 걸음의 끝자리만 검사하므로, 한 걸음이 벽 두께와 판정 반지름을 합친 것보다 길어지면 벽 건너편에 내려앉는다. 뒤로 멀리뛰기를 되풀이해 속도 상한 없이 속도를 쌓는 기술이 벽과 문을 통과하는 이유다.
- 벽은 2D로 판정: 벽 삼각형을 법선이 X축과 Z축 가운데 어느 쪽에 가까운지로 나누고, 그 축에 수직인 평면으로 눕혀 2D로 안팎을 따진다. 3D 거리 계산보다 싸지만, 비스듬한 벽과 모서리에서 판정이 고르지 않은 자리가 생긴다.
- 16비트 좌표: 바닥을 찾을 때 위치를 16비트 정수로 잘라 쓴다. 그래서 맵에서 65,536 단위씩 떨어진 허공에서도 원래 맵의 바닥이 잡혀, 보이지 않는 바닥 위에 서는 현상이 생긴다.
오늘의 엔진은 같은 문제를 스윕·연속 충돌 검사, 속도 상한, 얇은 벽을 두껍게 만드는 레벨 규칙으로 막는다. 다만 원리는 그대로여서, 고정 간격으로 끊어 보는 한 “한 단계에 얼마나 멀리 갈 수 있는가”는 언제나 판정의 약점이다. 벽 판정이 실제로 어디서 새는지는 The Problems with Walls in Super Mario 64와 Mario 64's Physics are not perfect 영상이 게임 화면과 함께 보여 준다.
- 분명 맞췄는데 빗나간 이유 - 활성 프레임과 스윕 판정
- 쓰러지는 것도 애니메이션일까 - 래그돌과 관절 제약
- 게임에서 벽은 어떻게 부술까 - 보로노이 파괴
- 공중에 뜬 모래는 왜 건드려야 떨어질까 - 바뀐 곳만 다시 보는 더티 렉트
- 물속으로 도망가면 왜 총알이 안 닿을까 - 항력과 히트스캔
- 움직이는 발판을 타는 건 게임에선 당연하지 않습니다 - 캐릭터 컨트롤러와 속도 상속
- 수류탄 점선은 미래를 미리 돌려 본 겁니다 - 궤적 예측 시뮬레이션
- 게임 자동차는 바퀴 위에 서 있지 않습니다 - 레이캐스트 서스펜션
- 게임 탱크는 궤도를 떼어 내도 똑같이 달립니다 - 레이캐스트 바퀴로 달리는 탱크
- 스파이더맨의 거미줄은 붙은 곳에 매달려 있지 않습니다 - 진자 시뮬레이션과 피벗
- 게임 속 총알은 총알이 아닙니다 - 투사체 판정과 레이캐스트
- Integration Basics — Gaffer On Games
- Fix Your Timestep! — Gaffer On Games
- The Problems with Walls in Super Mario 64 — Kaze Emanuar
- Mario 64's Physics are not perfect — Kaze Emanuar
- Super Mario 64 decompilation — n64decomp (GitHub)
- Concrete Jungle Gym: Building Traversal in Marvel's Spider-Man — GDC
- Real Time Dynamic Fracture with Volumetric Approximate Convex Decompositions — SIGGRAPH History Archive
- Quake III Arena source code — id Software (GitHub)