GameLoop
- 게임 시간 진행을 유저 입력, 프로세서 속도와 디커플링.
Motivation
거의 모든 게임에서 사용
- 어느 것도 서로 똑같지 않음.
- 전형적인 게임패턴
배치모드(batch mode) 프로그램이 대부분이였음
- 모든 작업이 끝나면 프로그램이 멈춤
- 예시: 저자의 프로그램
Interview with a CPU
- 즉각적인 피드백을 위해 대화형(interactive) 프로그램이 만들어짐. (게임 또한 있었음.)
- 프로그램은 입력을 대기, 반응
1
2
3
4
5
while (true)
{
char* command = readCommand();
handleCommand(command);
}
Event loops
- 최신 GUI 애플리케이션 내부도 비슷함.
1
2
3
4
5
while (true)
{
Event* event = waitForEvent();
dispatchEvent(event);
}
하지만 게임은 입력이벤트가 없어도, 돌아간다.
게임에선 루프는 끊임없이 돌아간다.
1
2
3
4
5
6
while (true)
{
processInput();
update();
render();
}
- processInput(): 이전 호출 이후의 유저 입력 처리
- update(): 게임 시뮬레이션을 한단계 시뮬 (Ai, 물리)
- render(): 게임화면에 그림
A world out of time
루프 한 바퀴 == 틱 or frame
- 게임루프 측정 == 초당 프레임 수(fps)를 얻을 수 있다.
루프가 빠르면 부드럽고 빠른 화면, 느리면 반대
- 한 프레임에 얼마나 많은 작업을 하는가가 중요
- 코드가 실행되는 플랫폼의 속도 또한 중요
- 하드 웨어, os 등
Seconds per second
- 어떤 하드웨어에서라도 일정한 속도로 실행될 수 있도록 하는 것이 중요 `
The Pattern
- 게임 루프는 게임하는 내내 실행됨.
- 유저 입력을 처리한 뒤 게임 상태를 업데이트하고 게임화면을 렌더링한다.
- 시간 흐름에 따라 게임플레이 속도를 조절한다.
When to Use It
- 게임에서는 안쓰는 경우는 거의 없음.
루프 코드에 따라 ‘엔진’ 과 ‘라이브러리’를 나눈다.
라이브러리: 게임 메인 루프를 들고 있으면서, 라이브러리 함수를 호출
엔진: 스스로가 루프를 들고 있으면서 사용자 코드를 호출
Keep in Mind
- 최적화가 중요.
You may need to coordinate with the platform’s event loop
- 그래픽 UI와 이벤트 루프가 들어있는 OS나 플랫폼에서는 애플리케이션 루프가 두개 있는 셈.
- 서로 잘맞아야함.
- 제어권을 가져와, 루프 하나만 남겨놓을 수 있음.
- 오래된 윈도우 API로 게임을 만든다면 main()에 게임루프를 두고, 루프안에서 PeekMessage()를 호출해 OS로부터 이벤트를 가져와 전달 가능.
- GetMessage와는 달리 PeekMessage는 유저 입력이 올때까지 기다리지 않음.
- 플랫폼에 따라 내부이벤트 무시하기 어려울 수 있음.
- 웹브라우저 == 이벤트 루프가 브라우저 실행 모델 깊숙한 곳에서 모든 것을 좌우하므로, 이것을 게임루프로 삼아야함.
- requestAnimationFrame()같은 걸 호출, 브라우저가 코드를 콜백으로 호출해주기를 대기.
Sample Code
- 여러 게임 시스템을 진행.
Run, run as fast as you can
1
2
3
4
5
6
while (true)
{
processInput();
update();
render();
}
- 가장 간단한 이 방식은 게임 실행 속도 제어 불가능
고정 시간 간격: Take a little nap
60FPS로 돌린다면, 한 프레임에 16ms
- 그동안 게임 진행과 렌더링을 다 할 수 있다면, 프레임 레이트를 유지할 수 있다.
아래처럼 프레임 사이 남은 시간 대기
- 코드는 다음과 같음.
1
2
3
4
5
6
7
8
9
while (true)
{
double start = getCurrentTime();
processInput();
update();
render();
sleep(start + MS_PER_FRAME - getCurrentTime());
}
sleep(): 게임이 너무 빨라지지 않게 대기
너무 느려지는 것은 막지 못함.
가변 시간 간격: One small step, one giant step
문제
- 업데이트할 때마다 정해진 만큼 게임 시간이 진행
- 업데이트하는 데에는 현실 세계의 시간이 어느 정도 걸림.
- 2 > 1 이면 게임은 느려짐.
- 한 번에 게임 시간을 16ms 이상 진행하면, 업데이트 횟수가 적어도 따라잡기 가능
- 실제 시간이 얼마나 지났는지에 따라 시간 간격을 조절하면 된다.
- 프레임이 오래 걸릴수록, 게임 간격을 길게
- 필요에 따라
업데이트 단계를 조절
할 수 있다.
가변 시간 간격
,유동 시간 간격
이라고 함.
1
2
3
4
5
6
7
8
9
10
double lastTime = getCurrentTime();
while (true)
{
double current = getCurrentTime();
double elapsed = current - lastTime;
processInput();
update(elapsed);
render();
lastTime = current;
}
- 매 프레임마다 실제 시간이 얼마나 지났는지를 elapsed에 저장.
- 게임 상태를 업데이트할 때 elapsed를 같이 넘겨, 지난 시간만큼 게임월드 상태를 진행
- 고정시간간격: 매 프레임마다 총알 속도에 맞춰 총알이 움직임
- 가변시간간격: 속도와 지나간 시간을 곱해, 이동거리를 구함.
- 시간 간격이 커지면 총알을 더 많이 움직임.
장점
- 다양한 하드웨어에서 비슷한 속도로 게임이 돌아감.
- 더 빠른 하드웨어인 경우 더 부드러운 게임플레이.
단점
- 게임이 비결정적, 불안정
- 비결정적: 버그 재현 힘들다.
- 부동소수점=> 반올림 오차 가능성
- 게임이 더 빨리 실행되면,오차가 더 크게 쌓임.(결국 PC에 따라 같은 총알의 위치가 달라짐.)
- 실시간으로 실행하기 위해 게임 물리 엔진은 실제 물리 법칙의 근사치를 취함.
- 근사치가 튐(blowing up)을 막기 위해, 감쇠(dumping)을 적용한다.
- 감쇠: 시간 간격에 맞춰 세심하게 조정해야함.
- 감쇠값이 바뀌다보면, 물리가 불안정해짐.
고정 시간 간격: Play catch up
- 렌더링: 가변시간 간격에 영향을 받지않음
- 때가 오면 렌더링할 뿐.
- 모션블러 같은 경우에는 영향 받지만, 미세한 차이
물리, AI등을 고정시간 + 렌더링 간격은 유연 => 프로세서 낭비 줄임
- 루프 이후로 실제 시간이 얼마나 지났는지를 확인
- 게임의 현재가 실제 시간의 현재를 따라잡을 때까지 고정 시간 간격만큼 게임 시간을 여러 번 시뮬레이션.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
double previous = getCurrentTime();
double lag = 0.0;
while (true)
{
double current = getCurrentTime();
double elapsed = current - previous;
previous = current;
lag += elapsed;
processInput();
while (lag >= MS_PER_UPDATE)
{
update();
lag -= MS_PER_UPDATE;
}
render();
}
- 프레임 시작시 실제 시간이 얼마나 지났는지를 lag변수에 저장.
- 이 값은 실제 시간에 비해 게임 시간이 얼마나 뒤쳐졌는지를 의미.
- 고정 시간 간격 방식으로 루프를 돌며서, 실제 시간을 따라잡을 때까지 게임을 업데이트.
- 다 따라잡으면 렌더링하고 다시 루프를 실행.
시간 간격(MS_PER_UPDATE)은 더 이상 시각적 프레임 레이트가 아님.
- 얼마나 촘촘하게 업데이트할 지에 대한 값.
- 짧을수록 실제 시간을 따라잡기 더 오래걸림.
- 길수록 게임 플레이가 끊겨보임.
- 60FPS보다 더 빠르게 돌아가도록,
시간 간격을 충분히 짧게 잡아
좋은 PC에서 더 나은 시뮬레이션을 보여주도록하는게 이상적
시간 간격이 너무 짧으면 안됨.
- 가장 느린 하드웨어에서도 update()를 실행하는 데 걸리는 시간보다는 시간 간격이 커야함.
- 그렇지 않으면 게임 시간은 계속 뒤쳐짐.
내부 업데이트에 보호장치: 최대 업데이트 횟수 설정
렌더링을 뒤로 뺌 =>
CPU 시간에 여유
가 생김.- 느린 PC: 화면 조금 끊김.
- 안전한 고정 시간 간격 => 여러 hw에서 일정한 속도로 게임 시뮬
유니티의 MonoBehaviour::FixedUpdate() 가 이런 방식
자투리 시간 문제: Stuck in the middle
- 유저 입장에서는 두 업데이트 사이에 렌더링되는 경우가 종종 있음.
- 업데이트: 정확, 고정간격 / 렌더링: 가능할 때마다
- 문제점: 항상 업데이트 후에 렌더링 되는 게 아님.
- 이렇기 때문에 움직임이 튀어보임.
- 하지만, 렌더링할 때 업데이트 프레임이 시간적으로 얼마나 떨어져 있는지 lag값을 보고 알 수 있음.
0 < lag < 업데이트 시간
일 경우 업데이트 루프를 빠져나옴. (lag == 다음 프레임까지 남은 시간)
- 하지만, 렌더링할 때 업데이트 프레임이 시간적으로 얼마나 떨어져 있는지 lag값을 보고 알 수 있음.
- 렌더링 시간에 다음 값을 넘김.
1
render(lag / MS_PER_UPDATE);
lag을 MS_PER_UPDATE로 나누어 정규화한것. render는 0.0~1.0 사이의 값을 처리한다.
- 렌더러는 게임 객체들과 각각의
현재 속도
를 안다.- 이를 통해
보간(extrapolation)
- 예상 위치를 렌더링
- 보간하지 않는것 보다는 나음.
- 이를 통해
Design Decisions
- 다루지 못한 내용: 화면 재생 빈도와의 동기화(refresh rate), 멀티스레딩, GPU 까지 고려할 경우 루프는 더 복잡해짐.
게임 루프를 직접 관리? 플랫폼이 관리?
- 웹브라우저는 직접 만들 가능성이 거의 없음.
- 기존 게임엔진 또한 엔진 루프를 그대로 사용할 가능성이 높음
플랫폼 이벤트 루프 사용
- 간단: 루프를 작성하고 최적화 고민 x
- 플랫폼에 잘맞음: 두 모델의 차이를 신경 쓰지 않아도됨.
- 시간을 제어할 수 없음: 대부분 게임을 고려하지 않고 설계 => 느릴 가능성
게임 엔진 루프 사용
- 코드 직접 작성 안함:
- 게임루프 만들기 쉽지 않음. 사소한 버그와 약간의 최적화에도 큰 영향
- 엔진루프에서 아쉬운게 있어도 건드릴 수 없음.
직접 만든 루프
- 완전한 제어
- 플랫폼과의 상호작용
- OS나 프레임워크에 시간이 주어지도록, 가끔 제어권을 넘겨줘야함.
전력소모 문제
- CPU를 가능한 적게 사용하도록 해야함.
- 한 프레임 후에 쉬어주도록해줘야 하는 성능 상한
최대한 빨리 실행
- 게임 루프에서 sleep호출 하지 않음
- 시간이 남으면 FPS나 그래픽 품질 더 높임
- 전력을 많이 사용
프레임 레이트 제한
- 모바일게임 == 게임 플레이 품질에 더 집중
- 프레임 레이트에 상한(30FPS, 60FPS)을 둠.
- 게임 루프에서 프레임 시간 안에 할 일이 전부 끝나면, 나머지 시간동안 sleep 호출
게임플레이 속도 제어
- 게임루프 = 비동기 유저 입력 + 시간 따라잡기
- 입력은 쉬움, 시간은 다루기 어려움.
- 다양한 플랫폼 지원이 핵심
동기화 없는 고정 시간 간격 방식
- 그냥 게임 루프를 최대한 빠르게.
- 간단.
- 게임 속도 == 하드웨어와 게임 복잡도에 영향
동기화하는 고정 시간 간격 방식
루프 마지막에 지연 or 동기화 지점 => 게임 시간 컨트롤
- 간단.
- 전력 효율 높음.
- 매 틱마다 좀 더 작업하기 보다 쉬어줘야 전력 아낌.
- 게임이 너무 빠르지 않음
- 게임이 너무 느려질 수 있다.
- 한 프레임에서 게임 업데이트 + 렌더링하는 게 오래걸리면 재생(playback)이 느려짐
- 업데이트와 렌더링을 분리하지 않음
- 시간을 따라잡기 위해 렌더링 프레임을 낮추지 않다 보니 게임 플레이가 느려짐
가변 시간 간격 방식
- 권장되지 않음.
- 너무 느리거나 너무 빠른 곳에서도 맞춰 플레이 가능.
- 게임 플레이를 불안정하고 비결정적으로 만든다.
업데이트는 고정시간, 렌더링은 가변시간
- 적응력이 가장 높다.
실제 시간을 따라잡아야 한다면 렌더링 프레임을 낮춤.
- 너무 느리거나 너무 빨라도 잘 적응
- 실시간 업데이트 시, 뒤처질 일은 없음.
- 최고사양 HW => 부드러운 화면
- 훨씬 복잡
- 구현 복잡
- 업데이트 시간 간격을 정할 때 고사양유저를 위해 최대한 짧게
- 하지만 저사양 유저가 너무 느려지지 않도록 주의
참고
MonoBehaviour lifecycle
은 복잡한 유니티 프레임워크 게임 루프를 그림으로 아주 잘 설명해놓은 글