소개
게임 루프는 모든 게임의 핵심입니다. 어떤 게임도 게임 루프 없이는 실행될 수가 없습니다. 그러나 불행히도 새로운 게임 프로그래머에게 인터넷 상에서 이러한 내용이 적절하게 나와있는 좋은 곳을 찾을 수 없습니다. 그러나 두려워하지 마세요. 왜냐하면 게임루프에 주목해 볼만한 글을 방금 발견했기 때문에요.
나의 직업이 게임 프로그래머인 것에 감사합니다. 소형 모바일 게임들을 위한 많은 코드를 접하게 되었습니다. 몇 몇의 게임 루프들이 나를 놀라게 만들었습니다. 아마 당신도 게임 루프들이 어떻게 다르게 쓰여지는 것에 대해 궁금할것입니다. 가장 인기있는 게임 루프 방법에 대해 찬반토론을 하겠습니다. 또한 게임루프 실행의 최고 방법을 알려 드리겠습니다.( 제 생각으로요.)
(브라질의 포루투칼어로도 이용하게 해줄수 있게 한 Kao Cardoso Felix 감사드립니다.)
게임 루프
모든 게임들은 사용자의 입력, 게임의 상태 수정, AI 조작, 배경음과 효과음들의 연속으로 이루어져 있다. 이 연속된 동작들은 게임 루프를 통해 처리된다. 소개에서 말한대로 게임 루프는 모든 게임의 핵심이다. 앞서 언급한 작업에 대해 자세하게 설명하지 않고, 게임루프 하나에 집중하도록 하겠다. 그래서 작업들을 두개의 기능으로 단순하게 했다 : 게임 업데이트와 디스플레이
최고로 단순한 게임 루프 예제 코드:
bool game_is_running = true;
while( game_is_running ) {
update_game();
display_game();
}
이 단순한 게임 루프의 문제는 시간을 고려하지 않고 있다. 그저 게임을 실행할 뿐이다. 느린 하드웨어에서는 느려지고, 빠른 하드웨어에서는 빨라진다. 예전 하드웨어의 속도를 알고 있다면 상관이 없다. 그러나 오늘날 많은 하드웨어 플랫폼이 존재한다. 우리는 속도를 다른 어떤 방법으로 실행해야 한다. 여기에 많은 방법들이 있고, 이 방법들에 대해 이어지는 섹션에서 토의해 보자
첫째, 이 글에서 사용하는 두 단어에 대해 설명하겠다 :
FPS
FPS는 Frames Per Second의 약어이다. display_game()의 초당 호출 횟수이다.
게임 속도
게임 속도는 초당 게임 상태의 갱신 속도이다. 다른 말로는 초당 update_game()의 호출 횟수이다.
FPS가 게임속도에 의존적
구현
타이밍 문제를 해결하기 위한 쉬운 방법은 단지 초당 25 프레임으로 게임을 실행하면 된다. 코드는 다음과 같다.
const int FRAMES_PER_SECOND = 25;
const int SKIP_TICKS = 1000 / FRAMES_PER_SECOND;
DWORD next_game_tick = GetTickCount();
// GetTickCount() returns the current number of milliseconds
// that have elapsed since the system was started
int sleep_time = 0;
bool game_is_running = true;
while( game_is_running ) {
update_game();
display_game();
next_game_tick += SKIP_TICKS;
sleep_time = next_game_tick - GetTickCount();
if( sleep_time >= 0 ) {
Sleep( sleep_time );
}
else {
// Shit, we are running behind!
}
}
이 방법은 큰 혜택이 있다. 참 쉽죠! update_game()이 초당 25번 불려진다는 것은 알기에 게임 코드를 작성하기에 쉽다. 예를 들면 이러한 게임 루프는 응답 구현 기능의 구현이 쉽다. 만약 게임 안에서 랜덤 값을 사용하지 않는다면, 단지 유저의 입력 변화 혹은 재생들을 기록하기만 하면 된다.
테스트하는 하드웨어에 FRAMES_PER_SECOND를 이상적인 값으로 적응시킬 수 있다. 하지만 보다 빠르거나 느린 하드웨어서는 무슨 일이 벌어질것인가? 그럼 이제 알아보자.
느린 하드웨어
하드웨어서 정해놓은 FPS를 다룰 수 있다면 문제가 없다. 그렇지만 하드웨어에서 FPS를 다룰 수 없을 때 문제가 발생한다. 게임은 느려질 것이다. 가장 나쁜 케이스는 게임이 무거운 부분에서는 느리게 움직이고 어떤 부분에서 정상적이게 게임이 돌아가는 것이다. 타이밍은 왜곡되어서 게임을 할 수 없게 만든다.
빠른 하드웨어
빠른 하드웨어에서는 문제가 없을 것이다. 하지만 정말로 많은 귀중한 클럭 사이클을 낭비하는 것이다. 쉽게 300 FPS를 만들 수 있을 때 25 또는 30FPS를 돌리는 것은... 부끄러운 줄 알아라! 특히 빠르게 움직이는 객체에 대해 시각적인 효과를 낭비하는 것이다.
다른 한편으로 모바일 장치에서 이것은 효과적일 수 있다. 게임을 계속 돌리는게 아니라서 배터리 시간을 유지할 수 있다.
결론
일정한 게임 속도에 FPS를 의존적이게 만드는 것은 쉽게 구현할 수있고 코드를 심플하게 하지만 문제가 약간 있다. 높은 FPS는 느린 하드웨어에서 문제를 야기할 수 있다. 그리고 느린 FPS는 빠른 하드웨어에서는 시각적 효과의 낭비로 이어진다.
게임속도가 가변FPS에 의존적
구현
또 다른 게임 루프의 구현은 가능한 빠르게 게임 루프를 도는 것이다. 그리고 FPS가 게임 속도를 결정한다. 바로 전 프레임과 다른 시간으로 게임은 갱신된다.
DWORD prev_frame_tick;
DWORD curr_frame_tick = GetTickCount();
bool game_is_running = true;
while( game_is_running ) {
prev_frame_tick = curr_frame_tick;
curr_frame_tick = GetTickCount();
update_game( curr_frame_tick - prev_frame_tick );
display_game();
}
게임코드가 약간 더 어려워 지고 있다. 왜냐하면 우리는 update_game()의 기능에 대해 고려해야 하기 때문이다. 그러나 아직까지 어렵지는 않다. 첫눈에 봐서 우리의 문제를 해결하기에 이상적인 방법으로 보인다. 나는 많은 똑똑한 프로그래머들이 이런 종류의 게임루프를 구현하는 것을 봤다. 그들 중 일부는 아마도 이러한 루프를 구현하기 전에 이 글을 봤어야 했는데 할 것이다. 내가 즉시 느린 하드웨어나 빠른 하드웨어( 맞다! 빠른!) 둘 모두에 심각한 문제를 발생하는 것을 보여주겠다.
느린 하드웨어
느린 하드웨어서 때때로 어떤 곳에서 확실하게 느려진다. 게임 내 무거운 곳에서. 이러한 현상은 3D게임에서 수많은 폴리곤들이 보여지는 일정한 시간동안 확실하게 느려진다. 이러한 프레임 하락은 입력 반응 시간에 영향을 미치게 되고 마찬지로 플레이어의 반응 시간에도 영양을 끼친다. 게임 업데이트에 딜레이를 느낄 것이고 게임 상태는 큰 시간뒤에 갱신될 것이다. 그 결과 플레이어 및 AI의 반응 시간 또한 느려진다. 그래서 단순한 이동 실패 심지어 이동 불가능이 될 것이다. 예를 들어 정상적인 FPS에서는 장애물을 피할 수 있는데 낮은 FPS에서는 불가능하게 된다. 좀 더 심각한 문제는 시뮬레이션이 폭발할 수도 있다는 것이다!
빠른 하드웨어
위에 나와있는 게임코드가 어떻게 빠른 하드웨어에 문제인지 궁금할 것이다. 불행하게도 가능하다. 첫째로 컴퓨터 위에서의 계산에 대해 설명하겠다.
float 또는 double형의 메모리 공간 한계 때문에 몇 몇 값들을 나타낼 수가 없다. 예를 들어 0.1은 이진수로 나타낼 수가 없어서 double에서 저장될때 어림수로 된다. pyton을 통해 보여주겠다. :
>>> 0.1
0.10000000000000001
이것은 별로 극적이지 않다. 예를 들면 레이싱카가 밀리세컨드당 0.001의 속도로 움직이는데 10초 후에 움직인 거리는 10.0이다. 만약 게임공간 내에서 쪼개서 계산을 한다면 초당 프레임으로 입력을 받는 다음과 같은 함수를 사용할 것이다.
>>> def get_distance( fps ):
... skip_ticks = 1000 / fps
... total_ticks = 0
... distance = 0.0
... speed_per_tick = 0.001
... while total_ticks < 10000:
... distance += speed_per_tick * skip_ticks
... total_ticks += skip_ticks
... return distance
이제 우리는 초당 40프레임 때의 거리를 계산 할 것이다. :
>>> get_distance( 40 )
10.000000000000075
잠시만 기다려봐라... 10.0이 아니네?? 무슨일인가? 흠, 왜냐하면 우리는 400 추가를 나누어서 계산했고 작은 에러들은 커진 것이다. 초당 100 프레임에서 무슨 일이 일어나는가...
>>> get_distance( 100 )
9.9999999999998312
머라고? 심지어 에러가 더 커졌다!! 흠, 왜냐하면 우리는 100fps에서 좀 더 더했기 때문이다. 라운딩 에러가 크게될 가능성이 있다. 그래서 게임에서 초당 40 이나 100프레임이 틀린것이다.
>>> get_distance( 40 ) - get_distance( 100 )
2.4336088699783431e-13
게임에서 이러한 차이가 너무 작다고 느낄 것이다. 그러나 진짜 문제는 잘못된 값으로 무엇을 계산하려고 할 때 생긴다. 이 방법으로는 작은 에러가 크게 된다. 그리고 높은 프레임 레이트에서는 게임을 불능으로 만들어 버린다. 이러한 일이 생기냐고? 고려하기에 충분하다! 이러한 게임 루프를 사용하는 게임을 보아왔고 정말로 높은 프레임 레이트에서 문제를 일으켰다. 후에 프로그래머가 게임 코어에 숨어있는 문제를 발견했다. 결국 많은 약간의 코드들을 수정했다.
결론
얼핏보면 이러한 게임 루프들은 좋다. 하지만 어리석게 굴지 마라. 느리거나 빠른 하드웨어 둘 모두 게임에 심각한 문제를 발생시킨다. 그리고 그거 외에도 고정 프레임 레이트를 사용할 때보다 구현이 더 어렵다. 그래서 굳이 이것을 사용 하겠는가?
최대FPS에 대한 일정한 게임 속도
구현
우리 첫번째 방법인 일정한 게임 속도에 의존적인 FPS의 경우 느린 하드웨에서 문제를 일으켰다. 그 경우 프레임레이트와 게임속도 둘 모두 저하됬다. 만일 그렇다면 가능한 방법은 게임 갱신은 그대로 두고 랜더링 프레임레이트를 줄이는 것이다. 이 것은 다음과 같은 게임루프를 사용한다 :
const int TICKS_PER_SECOND = 50;
const int SKIP_TICKS = 1000 / TICKS_PER_SECOND;
const int MAX_FRAMESKIP = 10;
DWORD next_game_tick = GetTickCount();
int loops;
bool game_is_running = true;
while( game_is_running ) {
loops = 0;
while( GetTickCount() > next_game_tick && loops < MAX_FRAMESKIP) {
update_game();
next_game_tick += SKIP_TICKS;
loops++;
}
display_game();
}
이 게임은 일정하게 초당 50번 갱신을 하고 랜더링은 가능한 빠르게 완료한다. 랜더링은 초당 50번보다 많게 이루어진다는 것을 알 수 있고 이어지는 몇몇 프레임도 비슷해진다. 그래서 실제 영상 프레임은 초당 50프레임의 최대로 표현되어 질것이다. 느린 하드웨어서 돌아갈 경우 프레임레이트는 게임 업데이트가 MAX_FRAMESKIP에 도착할 때까지 떨어질 것이다. 실제로 render FPS가 5까지 (= FRAMES_PER_SECOND / MAX_FRAMESKIP)까지 떨어진다는 것을 의미한다. 실제 게임은 느려질 것이다.
느린 하드웨어
느린 하드웨어에서 초당 프레임은 떨어질것이다. 그러나 게임은 아마도 정상적인 속도로 돌아갈 것이다. 만약 하드웨어가 여전히 정상적인 속도를 따라가지 못한다면 게임은 느려지고 프레임레이트는 전혀 부드럽지 않을 것이다.
빠른 하드웨어
빠른하드웨어에서 게임은 문제가 없을 것이다. 그러나 처음 해결책과 비슷하다. 높은 프레임 레이트로 쓸 수 있는 많은 귀중한 클럭 사이클을 낭비하고 있다. 빠른 갱신율과 느린 컴퓨터와의 균형을 발견하는게 중요하다.
결론
최대 FPS를 쓴 일정한 게임속도는 쉽게 구현을 할 수 있고 코드가 심플하다. 그러나 여전히 약간의 문제를 가지고 있다 : 높은 FPS를 설정할 경우 여전히 느린 하드웨어에서의 위험성을 가지고 있다(그러나 첫번째 해결책만큼 심각하지 않다) 그리고 낮은 FPS를 설정할 경우 높은 하드웨어에서는 영상효과의 낭비이다.
가변FPS에 비의존적인 일정한 게임 속도
구현
느린하드웨어서는 한층 더 빠르고 빠른 하드웨어에서는 시각적으로 좀 더 매력적이게 향상시키는 게 가능한가? 그럼 우리에게 행운이다! 가능하다. 게임 상태는 초당 60번의 갱신이 필요하지 않다. 사용자의 입력이나 AI 그리고 게임 상태는 초당 25프레임으로 충분하다. 그래서 초당 update_game()를 더적게 더 많이도 말고 25번 부르도록 하자. 한편으로 랜더링은 하드웨어가 다룰 수 있으면 있을 만큼 빠르게 되도록하자. 그러나 느린 프레임 레이트는 게임의 갱신을 방해할 것이다. 이것을 해결하는 하는 방법은 이 게임루프를 따르는 것이다.
const int TICKS_PER_SECOND = 25;
const int SKIP_TICKS = 1000 / TICKS_PER_SECOND;
const int MAX_FRAMESKIP = 5;
DWORD next_game_tick = GetTickCount();
int loops;
float interpolation;
bool game_is_running = true;
while( game_is_running ) {
loops = 0;
while( GetTickCount() > next_game_tick && loops < MAX_FRAMESKIP) {
update_game();
next_game_tick += SKIP_TICKS;
loops++;
}
interpolation = float( GetTickCount() + SKIP_TICKS - next_game_tick )
/ float( SKIP_TICKS );
display_game( interpolation );
}
이러한 게임 루프에서 update_game() 부분은 여전히 쉽다. 그러나 불행하게도 display_game() 함수는 좀 더 복잡하게 되었다. 보간 변수를 쓰는 예상함수를 구현해야 한다. 그러나 걱정하지 마라. 어렵지 않다. 단지 조금의 일을 더 하는 것 뿐이다. 보간과 예상하는 일을 어떻게 하는지에 대해 설명해 주겠다. 그러나 첫째 왜 이게 필요한지 보여주겠다.
보간의 필요성
게임 상태는 초당 25번 업데이트를 한다. 그래서 만약 랜더링에 보간을 사용하지 않는다면 마찬가지의 속도로 보여질 것이다. 예를 들어 영화의 경우 초당 24 프레임으로 실행되기에 25FPS가 어떤사람들은 느리지 않다고 생각한다. 그래서 25FPS가 시각적으로 만족스럽다고 생각한다. 그러나 빠르게 움직이는 객체에 대해서는 여전히 좀더 빠른 FPS를 쓰는게 개선된다는 것을 알 수있다. 그러므로 프레임 사이 마다의 빠른 움직임을 좀 더 부드럽게 하려면 무엇을 해야 한다. 그래서 보간과 예상 함수는 해결책을 제시해 쥔다.
보간과 예상
게임 코드는 초당의 현재 프레임으로 돌고 있다고 말했다. 그래서 프레임들 마다 draw/render할 때 2개의 게임시간이 가능하다. 예를 들어 게임상태를 10번 업데이트하고 난 후 장면을 나타내려고 한다. 랜더링은 10번째와 11번째 게임업데이트 사이에 될 것이다. 그래서 10.3 쯤에 랜더링이 가능 할 것이다. 보간 값은 그 때 0.3을 유지하게 된다. 예들 들자면 : 차가 다음과 같은 게임 시간으로 움직인다 :
position = position + speed;
만약 10번째 게임시간에 500이라는 위치에 있을때 속도는 100이다. 그 후 11번째 게임 시간에 위치는 600일 것이다. 그렇다면 차는 랜더링 시에 어느 위치에 있겠는가? 마지막 게임시간의 위치에 있게 할것이다(여기서는 500). 하지만 더 좋은 방법은 정확한 10.3 위치를 예상하는 것이다. 이와 같은 것이다 :
view_positon = position + (speed * interpolation))
차는 530 위치에 그려질 것이다. 그래서 기본적으로 보간 값은 이전 게임시간과 다음(previous = 0.0, next = 1.0) 사이에 값을 포함하고 있다. 랜더링 시간에 차/카메라/…가 배치될 때 "예상"함수를 어디에 만들어야 하는가? 예상함수를 객체의 속도 또는 조정, 회전 속도에 기초를 둬야 한다. 이것은 복잡함을 필요로 하지 않다. 왜냐하면 우리는 단지 프레임사이를 부드럽게 하려고 할 뿐이기 때문이다. 충돌을 감기 되기 전에 객체가 다른 객체로 표현되어지는 것이 실제로 가능하다. 그러나 이전에 봐왔듯이 게임은 초당 25프레임으로 업데이트 되고 있다. 그러므로 이와 같이 일이 일어날때 에러는 순식간에 보여진다. 사람 눈으로는 거의 알아차리기 어렵다.
느린 하드웨어
대부분의 경우 update_game()은 display_game()보다 훨씬 적은 시작을 필요로 한다. 사실상 우리는 심지어 느린 하드웨어 위에서도 update_game() 함수가 초당 25번 실행된다고 예상한다. 비록 게임이 초당 15프레임으로 화면을 보여줄지라도 많은 문제없이 플레이어의 입력을 다루고 게임상태를 업데이트할 것이다.
빠른 하드웨어
빠른 하드웨어 위에서 게임은 일정하게 초당 25프레임을 유지 할 것이다. 그러나 화면에 업데이트는 되는 것은 이것보다 빠를 것이다. 보간/예상 함수는 높은 프레임레이트에서 돌아가는 멋진 효과를 만들어 낼 것이다. FPS를 속이는 좋은 방법이다. 왜냐하면 매 프레임 레이트마다 게임 상태를 업데이트 하지 않는다. 내가 설명한 두번째 방법보다 높은 FPS를 가지고 있다.
결론
FPS에 독립적인 게임상태를 만드는 것은 가장 이상적인 게임루프 구현으로 보인다. 하지만 disply_game() 함수를 예상하는 것을 구현해야 한다. 그러나 어렵지 않다.
종합 결론
게임루프는 생각한 것보다 좀 더 많은 것을 가지고 있다. 우리는 4가지 가능한 구현을 살펴 보았다. 그 중 하나는 확실하게 피해야 할 것이다. 그 하나는 가변FPS에 따르는 게임속도이다. 모바일 장치에게 일정한 프레임레이트는 좋고 간단한 해결책이다. 그러나 모든 하드웨어에 쓰이기에는 별로이다. 가장 좋은 게임루프는 높은 프레임레이트를 위해 예상을 사용하는 게임속도에 독립적인 FPS이다.
만약 일부로 예상 함수를 사용하지 않는다면 최대 프레임레이트에서 작업할 수 있다. 하지만 적당한 게임 업데이트 레이트가 느리거나 빠른 하드웨어 모두 고장을 내는 것을 발견 할 수 있을 것이다. 이제 멋진 게임을 위한 코드를 생각해 보자!
Koen Witters
------------------------------------------------------------------------------------------
출처 : http://dewitters.koonsolo.com/gameloop.html
발번역이 올려도 될지 모르겠습니다. 이제 겨우 AI 공부를 끝내고 스프라이트 툴을 만지고 있습니다 ^^
말없이 많이 도움을 받아가서 부끄럽지만 올립니다 ^^;;;;;
의역 및 오역이 많으니 이상한 부분은 원문을... 번역이 더 머리 아플지도...
출처 : http://www.devpia.com/MAEUL/Contents/Detail.aspx?BoardID=30&MAEULNO=12&no=235&page=1