물리 스텝
고친 사람 github-actions[bot]
물리 스텝은 게임 속 세계의 물리 계산을 정해진 짧은 시간만큼 앞으로 넘깁니다. 그 사이 물체를 움직이고 부딪힌 물체를 떼어 놓습니다. 이 계산을 1초에 수십 번 되풀이해서 공이 떨어지고 튕기는 모습을 만듭니다. 화면을 그리는 주기와는 따로 돕니다.
쉽고 빠른 이해
무슨 일을 하나 — 게임 속 시간을 짧은 간격만큼 한 번 흘려보내는 계산입니다. 떨어지는 공이라면 한 번 돌 때마다 공에 속도가 조금 붙고 공이 조금 내려갑니다. 바닥에 닿았으면 튕겨 올립니다.
왜 이렇게 하나 — 물리 계산은 한 번에 흘려보내는 시간이 길수록 크게 틀립니다. 날아가는 총알이 얇은 벽을 건너뛰기도 합니다. 화면을 한 장 그릴 때마다 흐른 시간만큼 흘려보내면 그 시간은 컴퓨터 성능을 따라 들쭉날쭉해집니다. 그러면 같은 게임이 컴퓨터마다 다르게 움직입니다. 이를 막으려고 간격을 짧게, 늘 같게 묶어 둡니다.
어떻게 도나
- 실제로 흐른 시간을 모아 둡니다
- 모인 시간이 한 간격을 넘을 때마다 물리 계산을 한 번 돌립니다
- 화면은 가장 최근 두 계산 결과 사이를 메워서 그립니다
대가 — 화면을 그리는 주기와 물리 계산 주기가 따로 돌아서 둘을 잇는 코드가 붙습니다. 간격을 촘촘히 잡을수록 계산이 많이 듭니다. 물체끼리 부딪히는 계산이 없는 게임은 물리 스텝을 따로 두지 않습니다.
상세
이 절은 물리 스텝 한 번 안에서 어떤 계산이 도는지부터 봅니다. 그다음 스텝의 간격을 왜 고정하는지, 화면을 그리는 주기와 어떻게 맞물리는지, 간격을 얼마로 잡는지를 차례로 봅니다. 예는 떨어지는 공과 벽을 향해 날아가는 총알입니다.
눈을 가리고 방을 걷는 사람은 한 걸음 내딛고 멈춰서 손으로 주위를 더듬습니다. 무엇에 닿았으면 몸을 조금 물린 뒤 다음 걸음을 내딛습니다. 걸음 폭은 매번 같습니다.
게임 속 세계의 물리는 시간을 잘게 나눠 한 조각씩 계산합니다. 한 조각을 계산하는 일이 물리 스텝(physics step)입니다. 한 조각의 길이는 스텝 간격 또는 타임스텝(timestep)입니다.
앞의 사람으로 치면 걸음 하나가 물리 스텝 하나입니다. 걸음 폭은 스텝 간격입니다. 손으로 더듬는 일은 물체끼리 부딪혔는지 찾는 계산입니다. 몸을 물리는 일은 부딪힌 물체를 떼어 놓는 계산입니다.
이 계산은 대개 물리 엔진이 맡습니다. 물리 엔진은 중력과 충돌 같은 물리 법칙을 흉내 내서 물체의 움직임을 계산해 주는 라이브러리입니다. 게임 말고도 로봇이나 차량을 컴퓨터 안에서 미리 굴려 보는 시뮬레이션이 같은 방식으로 시간을 나눕니다.
한 스텝 안의 계산
물리 스텝 한 번은 네 가지 일을 차례로 합니다. 표의 오른쪽 칸은 떨어지는 공에 일어나는 일입니다.
| 순서 | 하는 일 | 떨어지는 공이라면 |
|---|---|---|
| 1 힘 모으기 | 물체에 걸린 힘을 모두 더한다 | 중력이 공을 아래로 당긴다 |
| 2 움직이기 | 힘으로 속도를, 속도로 위치를 한 간격만큼 고친다 | 공에 속도가 붙고 공이 조금 내려간다 |
| 3 충돌 감지 | 움직인 뒤 서로 겹친 물체 쌍을 찾는다 | 공이 바닥에 조금 파고들었다 |
| 4 충돌 응답 | 겹친 물체를 떼어 놓고 속도를 바꾼다 | 공을 바닥 위로 올리고 위로 튕긴다 |
둘째 단계의 계산은 수치 적분(numerical integration)입니다. 움직임을 식 하나로 풀어 한 번에 구하지 않습니다. 짧은 간격마다 조금씩 더해 나가서 답에 다가갑니다. 물체끼리 부딪히고 사람이 조작까지 하는 세계는 식 하나로 풀리지 않아서 이렇게 계산합니다.
아래는 공 하나를 두 스텝 움직인 계산입니다. 중력이 1초에 속도를 10씩 붙인다고 잡았습니다. 스텝 간격은 0.1초로, 위쪽 방향을 양수로 잡았습니다.
let v = 0, y = 10; // 멈춘 공, 높이 10
v += -10 * 0.1; // v = -1
y += v * 0.1; // y = 9.9
v += -10 * 0.1; // v = -2
y += v * 0.1; // y = 9.7
속도를 먼저 고쳤습니다. 그다음 방금 고친 속도로 위치를 옮겼습니다. 이렇게 속도부터 고치는 계산법이 반암시적 오일러(semi-implicit Euler) 방법입니다.
이 순서가 왜 중요한지는 용수철처럼 되돌아오는 움직임에서 드러납니다. 위치를 먼저 옮기면 계산이 스텝마다 에너지를 조금씩 더해 흔들림이 점점 커집니다. 속도를 먼저 고치면 간격이 충분히 짧은 동안 흔들림이 불어나지 않습니다. 게임 물리가 흔히 이 순서를 쓰는 까닭입니다.
셋째 단계인 충돌 감지는 두 물체가 겹쳤는지 알아내는 계산입니다. 넷째 단계인 충돌 응답은 겹친 물체를 떼어 놓고 튕겨 나가는 속도를 정합니다. 둘 다 움직인 결과를 바로잡는 일입니다.
물체가 여럿 쌓여 있으면 하나를 떼어 놓다가 다른 물체와 다시 겹칩니다. 물리 엔진은 넷째 단계를 한 스텝 안에서 여러 번 되풀이해 겹침을 줄여 나갑니다. 이 계산을 맡는 부분이 솔버(solver)입니다.
간격을 고정하는 까닭
물리 스텝의 간격은 대개 고정합니다. 매번 같은 길이의 시간만 흘려보낸다는 뜻입니다. 간격이 길 때 깨지는 것 둘과 간격이 들쭉날쭉할 때 깨지는 것 하나를 차례로 봅니다.
첫째로 간격이 길면 계산이 크게 틀립니다. 수치 적분은 한 간격 동안 속도가 변하지 않는다고 보고 더합니다. 간격이 길수록 그 가정에서 벗어나는 몫이 커집니다.
앞에서 본 용수철 같은 움직임에서는 이 오차가 더 크게 드러납니다. 반암시적 오일러로 순서를 바로 잡아도 간격이 너무 길면 흔들림이 스텝마다 불어납니다. 끝내 물체가 튕겨 나가기도 합니다.
둘째로 속도가 큰 물체가 얇은 벽을 건너뜁니다. 충돌 감지는 스텝이 끝난 뒤의 위치만 봅니다. 한 스텝에 움직이는 거리가 벽 두께보다 길면 벽 앞에 있던 총알이 다음 스텝에는 벽 뒤에 있습니다. 두 위치 어디서도 벽과 겹치지 않으므로 부딪힌 적이 없는 것으로 계산됩니다.
이 현상이 터널링(tunneling)입니다. 값을 넣어 보면 이렇습니다. 초속 600 m 로 나는 총알을 1초에 60번 스텝으로 계산하면 한 스텝에 10 m 를 갑니다. 두께 20 cm 인 벽은 그 10 m 사이에 들어가 버립니다.
셋째로 간격이 들쭉날쭉하면 결과가 컴퓨터마다 달라집니다. 간격을 고정하지 않는 게임은 대개 화면을 한 장 그리는 사이에 흐른 시간을 간격으로 씁니다. 그 시간은 컴퓨터 성능을 따라 길어지기도 짧아지기도 합니다.
오차의 크기는 간격에 따라 달라집니다. 같은 점프가 성능이 높은 컴퓨터와 낮은 컴퓨터에서 조금씩 다른 높이에 닿습니다.
같은 입력에 같은 결과
간격을 고정하면 얻는 것이 하나 더 있습니다. 같은 처음 상태에서 같은 입력을 같은 스텝에 넣으면 같은 결과가 나옵니다. 이 성질이 결정성(determinism)입니다.
게임의 리플레이가 이 성질에 기댑니다. 화면을 영상으로 녹화하는 대신 스텝마다 들어간 입력만 적어 둡니다. 나중에 그 입력을 같은 스텝에 다시 넣으면 같은 장면이 되살아납니다.
여러 사람이 하는 게임도 같은 방법을 씁니다. 모든 컴퓨터가 입력만 주고받습니다. 그다음 각자 같은 스텝을 돌려 같은 세계를 얻습니다. 이 방식이 락스텝(lockstep)입니다.
간격을 맞춰도 결과가 어긋나는 경우가 있습니다. 컴퓨터가 소수를 나타내는 방식인 부동소수점은 계산 결과의 끝자리가 달라질 수 있습니다. CPU(Central Processing Unit, 중앙 처리 장치)나 컴파일러가 바뀌면 그렇습니다. 그 작은 차이가 스텝마다 불어나 컴퓨터끼리 결과가 갈라집니다.
이 어긋남을 막으려고 락스텝 게임은 고정소수점을 쓰기도 합니다. 소수점 위치를 못 박아 두고 정수로 계산하는 방식입니다. 정수 계산은 어느 컴퓨터에서나 같은 답을 내서 끝자리가 갈라지지 않습니다.
화면 갱신 주기와 물리 스텝
게임은 게임 루프라는 반복문 안에서 돕니다. 한 바퀴마다 입력을 읽고, 세계를 갱신하고, 화면을 한 장 그립니다.
그린 한 장이 프레임입니다. 1초에 그리는 장 수는 프레임률입니다. 프레임률은 컴퓨터 성능과 장면에 따라 오르내립니다.
물리 스텝은 간격이 고정이라 프레임과 주기가 맞지 않습니다. 스텝 간격을 고정해 두고 흐른 시간을 쌓아서 프레임과 잇는 방식 전체를 고정 타임스텝(fixed timestep)이라 부릅니다.
방법은 흐른 시간을 모아 두는 것입니다. 프레임마다 지난 프레임부터 흐른 시간을 쌓인 시간에 더합니다. 쌓인 시간이 스텝 간격 이상이면 물리 스텝을 한 번 돌리고 간격만큼 뺍니다. 모자랄 때까지 되풀이한 뒤 화면을 그립니다.
시간은 1000분의 1초를 한 단위로 셉니다. 스텝 간격을 10으로 잡았습니다. 아래 표는 세 프레임 동안 쌓인 시간이 어떻게 바뀌는지 보입니다.
| 프레임 | 흐른 시간 | 쌓인 시간 | 돈 물리 스텝 | 남은 시간 |
|---|---|---|---|---|
| 1 | 25 | 25 | 2번 | 5 |
| 2 | 4 | 9 | 0번 | 9 |
| 3 | 7 | 16 | 1번 | 6 |
한 프레임에 물리 스텝이 두 번 돌기도 하고 한 번도 안 돌기도 합니다. 그래도 세 프레임 동안 흐른 36 가운데 30이 스텝 셋으로 계산됐습니다. 남은 6은 버리지 않고 다음 프레임으로 넘깁니다.
그릴 때는 남은 시간을 씁니다. 셋째 프레임에 남은 6은 간격 10의 0.6배입니다. 물체는 직전 스텝의 위치에서 마지막 스텝의 위치 쪽으로 0.6만큼 간 곳에 그립니다. 두 값 사이를 계산해 채우는 이 일이 보간(interpolation)입니다.
보간을 안 하면 물체는 스텝이 돌 때만 옮겨집니다. 프레임률이 스텝 횟수보다 높아도 움직임이 뚝뚝 끊겨 보입니다. 보간의 대가로 화면은 물리보다 최대 한 스텝 늦은 모습을 보여 줍니다.
flowchart TD
A["흐른 시간을 쌓인 시간에 더한다"] --> B{"쌓인 시간이 스텝 간격 이상인가"}
B -->|예| C["물리 스텝 한 번 · 쌓인 시간에서 간격을 뺀다"]
C --> B
B -->|아니오| D["남은 시간으로 보간해서 그린다"]
D -->|다음 프레임| A
그림에서 물리 스텝은 한 프레임 안에서 몇 번이든 돌 수 있습니다. 화면을 그리는 일은 프레임마다 한 번입니다.
스텝마다 부르는 갱신 함수
게임 엔진은 대개 게임 코드가 채울 갱신 함수를 둘 둡니다. 하나는 프레임마다 불리고, 다른 하나는 물리 스텝마다 불립니다.
물체를 미는 힘처럼 물리에 넣는 코드는 스텝마다 불리는 쪽에 둡니다. 프레임마다 불리는 쪽에 두면 프레임률에 따라 한 스텝에 힘이 두 번 들어가거나 한 번도 안 들어갑니다.
앞의 표로 보면 첫째 프레임은 스텝이 둘입니다. 힘은 그중 한 번만 들어갑니다. 둘째 프레임에 넣은 힘은 스텝이 없어 쌓여 있다가 셋째 프레임의 스텝 하나에 한꺼번에 들어갑니다.
스텝 간격을 고르는 기준
간격을 줄이면 계산이 정확해지는 대신 1초에 돌릴 스텝이 늘어납니다. 세 가지를 견주면 이렇습니다.
| 간격을 줄이면 | 간격을 늘리면 | |
|---|---|---|
| 계산 오차 | 줄어든다 | 커진다 |
| 터널링 | 덜 난다 | 더 난다 |
| 1초에 드는 계산 | 늘어난다 | 줄어든다 |
게임은 흔히 1초에 수십 번 물리 스텝을 돌립니다. 간격 전체를 줄이지 않고 필요한 곳만 손보는 방법도 둘 있습니다.
하나는 한 스텝 안에서 물리 계산만 더 작은 간격으로 여러 번 돌리는 서브스텝(substep)입니다. 바깥에서 보면 스텝은 여전히 한 번이라 앞 절의 스텝마다 불리는 갱신 함수도 한 번만 불립니다. 게임 코드가 기대는 주기는 바꾸지 않고 물리 계산만 촘촘해집니다. 쪼갠 만큼 계산은 늘어납니다.
다른 하나는 속도가 큰 물체만 따로 챙기는 연속 충돌 감지입니다. 스텝이 끝난 위치만 보지 않고 움직인 경로 전체가 무엇과 스치는지 봅니다. 총알처럼 작고 속도가 큰 물체에 붙여 터널링을 막습니다.
스텝이 간격보다 오래 걸릴 때
고정 타임스텝은 스텝 한 번이 스텝 간격보다 빨리 끝난다고 믿습니다. 앞의 계산처럼 간격을 10으로 잡습니다. 물체가 너무 많아 스텝 한 번에 15가 걸린다고 해 봅시다.
간격 10은 게임 속 시간이고, 15는 컴퓨터가 스텝을 계산하는 데 쓴 시간입니다. 스텝 하나가 쌓인 시간에서 10을 빼는 동안 쌓인 시간에는 15가 새로 더해집니다. 스텝을 돌릴수록 쌓인 시간이 5씩 불어나 끝내 따라잡지 못합니다.
그러면 게임은 스텝만 돌다가 화면을 못 그리고 얼어붙습니다. 이 되먹임이 죽음의 나선(spiral of death)입니다.
이를 막으려고 한 프레임에 돌릴 스텝 수에 상한을 둡니다. 상한을 넘은 시간은 버립니다. 게임 속 시간이 잠깐 느리게 흐르는 대신 화면은 계속 움직입니다.
물리 스텝을 쓰는 곳과 안 쓰는 곳
여러 사람이 하는 게임의 서버도 물리 스텝을 돌립니다. 서버는 정한 주기로 세계를 한 칸씩 넘깁니다. 그 결과를 접속한 컴퓨터들에 보냅니다.
서버에서는 스텝 한 번을 흔히 틱(tick)이라 부릅니다. 1초에 도는 틱 수는 틱레이트(tick rate)입니다.
모든 게임이 물리 스텝을 따로 두지는 않습니다. 카드 게임이나 한 수씩 두는 턴제 게임은 입력이 올 때만 상태를 바꾸면 됩니다. 물체끼리 부딪혀 튕기는 계산이 없기 때문입니다.
움직임이 단순한 게임은 프레임마다 흐른 시간인 델타 타임을 곱해 물체를 옮기는 것으로 충분하기도 합니다. 간격이 들쭉날쭉해도 결과가 크게 안 틀리는 움직임이라면 스텝을 따로 둘 까닭이 없습니다. 물리 스텝을 따로 두면 두 주기를 잇는 코드와 보간이 따라붙기 때문입니다.
관련 항목
물리 스텝 한 번이 거치는 처리 단계
수치 적분 · 충돌 감지 · 충돌 응답 · 솔버 · 조인트
물리 스텝을 프레임과 잇는 시간 관리 방식
고정 타임스텝 · 가변 타임스텝 · 델타 타임 · 보간 · 틱
물리 스텝의 오차를 줄이는 기법
반암시적 오일러 · 서브스텝 · 연속 충돌 감지 · 수치 안정성
물리 스텝이 밀리거나 틀릴 때 나는 장애
터널링 · 죽음의 나선 · 스터터링 · 프레임 드롭
물리 스텝의 결정성에 기대는 기법
결정성 · 리플레이 · 락스텝 · 부동소수점 · 고정소수점 · 롤백 넷코드
물리 스텝의 주기를 재는 지표
프레임률 · 프레임 시간 · 프레임 예산 · 틱레이트 · 주사율
물리 스텝을 품고 돌리는 실행 환경
물리 스텝이 속하는 상위 분류
게임 개발 · 게임 물리 · 시뮬레이션 · 실시간 시스템
다른 이름: physics step · 물리 틱 · physics tick · 물리 업데이트 · 시뮬레이션 스텝 · simulation step