=================================
=================================
=================================
출처 :
** 가변 프레임율이 지원되는 FrameSkip **
copyrightⓒ Gamza
요즘 게임은 더 좋은 사양의 컴퓨터를 사용할때 더 좋은 장면을 보여주기위해 프레임율에 의존적이지 않게 루프를 작성하게 됩니다. 간단하게 말하자면 이전 프레임과 현재 프레임의 시간차(dt)를 이용하는거죠. 다만 이렇게 하면 약간 신경쓰이는것들이 생기게되고, 확실히 프레임 스키핑을 이용한 고정 프레임율을 사용하는것이 편할때가 있습니다. 그래서 가변프레임율 과 고정 프레임율을 동시에 적용하는 방법을 생각해보았습니다. 기존에 '프레임 스키핑'이란 기법은 낮은 사양의 컴퓨터에서 고정프레임율을 보장하기 위한 기법으로, 메인루프가 설정해놓은 최대 프레임율보다 더 빨리돌수 없다는 한계를 가졌다고 알려져 있었습니다. 여기서는 특정코드들은 프레임스키핑을 사용해서 컴퓨터의 사양에 상관없이 고정프레임율을 가지고 동작하면서도, 기타 다른 코드들은 가능한한 빠르게 동작하도록 하는 코드를 소개합니다. 다음은 VC++6 에서 컴파일 테스트 해본 예제입니다.
물론 [프레임율 의존적인 코드] 는 메인루프가 빠르던,느리던 상관없이 10fps로 돌아갑니다. 아래는 가변 프레임율이 지원되는 FrameSkip 클래스 입니다.
이런게 간단하게 구현됩니다.
|
=================================
=================================
=================================
FPS 제어와 프레임 스킵 구현
CPU의 처리 속도가 빠르면 빠를수록 프로그램은 빠르게 처리 된다.
하지만 게임 프로그램의 경우 대개 정해진 게임 플레이 속도가 있기 때문에 CPU 성능에 따라 게임 속도가 제멋대로 치솟는 일이 있어서는 안된다.
그렇기 때문에 이를 제어 하기 위한 제어 장치가 필요하다.
FPS 제어 기법은 바로 위와 같은 상황에서 필요한 기법이다.
물론 그 방법상에는 여러가지 종류의 것들이 있겠지만 이 게임을 만들면서 내가 사용한 방법은 주어진 FPS에 맞게 프로그램을 일정 시간 동안 강제로 블로킹 상태로 만들어 속도를 조절하는 방법이다.
예를 들어, 프로그램의 속도를 30FPS로, 다시 말해 핵심 로직의 루프문의 순회 속도를 초당 30회전 정도로 제어 하기 위해 일정 ms(millisecond : 1/1000초)시간 동안 프로그램을 강제로 블로킹 상태로 집어 넣어 대기 시키는 방식이다.
이를 정리하면 다음과 같다.
프로그램 대기(목표 FPS를 위해 필요한 루프 1순회 당 대기 시간);
다음은 온라인 상에서 어렵지 않게 구할 수 있는 FPS 조절 코드들로 본인 입맛에 맞게 Win32 API 기준으로 재작성 된 것들이다.
보면 알 수 있듯이 이들 코드는 그 스타일에서나 조금씩의 차이가 있을 뿐, 모두 위의 로직을 충실히 따르고 있다.
1) 예제 코드1 (원소스 출처 : http://pjc0247.blog.me/80156597192)
DWORD dwStartTick;
DWORD dwDelay;
DWORD dwInterval = 1000 / FPS;//목표 FPS 유지를 위해 루프 1순회 마다 대기 해야하는 루프 지연 시간, 예를 들어 FPS 30이 목표일때 dwInterval은 값 33(단위:ms(1/1000초))을 갖게 된다.
while(1)
{
dwStartTick = GetTickCount();
dwDelay = dwInterval - (GetTickCount() - dwStartTick);//이번 루프에서 대기 해야 할 시간 = 지정된 FPS 유지를 위해 루프 1순회당 지연 되어야 하는 시간 - 중간에 어떤 이유로 지연 되었을 경우 그 지연된 시간
if(dwDelay > 0)//이 조건문은 지연된 시간이 FPS 유지를 위한 루프 1순회당 지연 시간 보다 클 경우 dwDelay 값이 음수가 되어 Sleep()함수 호출시 무한 블록킹 상태에 빠지는 사태를 방지 하기 위한 용도로 쓰인다.
Sleep(dwDelay);
}
2) 예제 코드2 (원소스 출처 :http://archive.tcltk.co.kr/misc/sdl/7.pdf)
DWORD dwElapsedTicks = 0;
DWORD dwCurrentTicks = 0;
DWORD dwInterval = 1000 / FPS;
while(1)
{
dwElapsedTicks = GetTickCount() - dwCurrentTicks;//마지막 루프 순회 직후 부터 루프를 다시 시작하는 시점 까지의 경과 시간, 즉 공백 시간이다.
if((1000 / (float)dwElapsedTicks) > FPS)
Sleep(dwInterval - dwElapsedTicks);
//위의 조건문은 dwCurrentTicks을 초기화 하지 않고서 최초로 루프에 진입 하였을 경우 경과 시간이 굉장히 큰 값이 나와 Slepp()의 인자 값이 음수가 되어 기약 없이 블럭 상태에서 헤어 나오지 못하는 일이 생긴다. 위의 조건문은 이를 방지하기 위한 조건문이다.
//그러나 처음에 dwCurrentCount 값을 0으로 초기화 한다고 하더라도 대개 dwElapsedTicks의 값이 0이 되기 때문에 1000/(float)dwElapsedTicks은 1000/0.0이 되어 그 결과 값은 0으로 나눴을때의 에러 값인 무한대(1.#INF)가 나오므로 이 조건문은 처음 한 번과 경과 시간이 FPS를 위한 대기 시간(30FPS일때 33ms)보다 큰 경우를 제외 하고는 매번 참이 되어 Sleep() 함수가 호출 된다.
//(1000/(float)dwElapsedTicks) : 지금 현재의 FPS를 나타내며 목표 FPS와는 별개다.
//1000/FPS:해당 FPS를 유지하기 위해 루프 1순회 당 소요 되어야 하는 ms 시간----A
//A식 결과 값 - 루프간 공백 시간 : 30프레임에 맞추기 위해서는 루프 1순회당 33ms 씩 지연 되어야 하므로 공백 시간이 없으면 33ms 동안 그대로 대기
//만약 루프간 공백 시간이 10ms가 생겼다면 정해진 지연시간인 33ms를 맞추기 위해 33ms-10ms의 결과 값인 23ms 동안만 대기 시킨다.
dwElapsedTicks = (GetTickCount() - dwCurrentTicks);
dwCurrentTicks += dwElapsedTicks;//Sleep()에서 지연된 시간을 다시금 경과 시간에 누적 시킨다.
}
3) 예제 코드3 (원소스 출처 : https://sites.google.com/site/sdlgamer/intemediate/lesson-7)
DWORD dwInterval = 1000 / FPS; DWORD dwNextTick = 0; //다음 번 루프 순회가 시작 되어야 하는 시간
while(1)
{
if(dwNextTick > GetTickCount()) Sleep(dwNextTick - GetTickCount());//dwNextTick - GetTickCount() : 현재의 FPS가 너무 높게 나와 이번 루프에서 지연 되어야 하는 시간 dwNextTick = GetTickCount() + dwInterval; }
이들의 공통된 특징은 Sleep() 함수 호출을 통해 프로세스나 쓰레드를 대기 상태로 만들어 실행 속도를 조절 하여 FPS를 맞춘다는 것이다.
이를 도식화 하면 다음과 같다.
만약 루프 순회 간에 지연 시간이 20ms 발생 하였다면, 33ms 동안 대기 시키기 위해 앞으로 13ms만 더 지연 시키면 된다.
물론 Sleep() 함수가 아닌 for문 같은 반복문을 이용해 장시간 루프를 돌게 하여 프로그램의 진행을 지연 시키는 방법도 생각해 볼 수 있겠지만 그러한 busy waiting 방식은 매번 가변적으로 변하는 필요한 대기 시간을 정확하게 예측하기도, 이를 적용하기도 어려울 뿐더러 다른 프로세스들에게 실행 시간을 양보 하지 않고 시스템의 자원을 독점하여 전체적인 스템 자원 활용의 효율성을 떨어 뜨리기 때문에 가급적이면 지양하는 것이 좋다.
어찌 되었든 이렇게 프로그램의 전체적인 속도가 FPS에 맞게 동기화 되면, 이후 부터는 캐릭터나 다른 오브젝트들의 에니메이션을 위해 준비된 이미지들의 수량에 맞추어 프레임 재생 속도를 재량껏 조절해 나가면 된다.
예를 들어 30 FPS로 동작하는 프로그램에서 캐릭터의 어떤 한 동작을 표현 하기 위해 준비된 이미지가 단 4장 뿐이라면, 프로그램 로직을 한 번 순회 하는데 약 33ms씩 걸린다는 점을 감안하여 루프 순회 7~8회 정도를 주기로 하여 이미지를 한 장씩 교체해 나가면 되는 것이다.
이에 대한 알고리즘은 다음과 같다.
루프 순회 횟수 = (루프 순회 횟수 + 1) % 이미지 교체 주기
if(루프 순회 횟수 == 0)
이미지 번호 = (이미지 번호 + 1) % 전체 이미지 개수
이미지 출력(이미지 번호);
2. 프레임 스킵핑
어떠한 문제로 인해 어느 시점에서 프로세스가 장시간 blocked 되었다가 구동 되는 등 FPS가 저하 되는 현상이 생겼을 경우 만약 이 프로세스가 싱글 게임이라면 blocked 되기 직전의 상태 부터 게임을 재개 하더라도 크게 문제 될 것이 없다.
하지만 다른 유저들과 함께 실시간 환경에서 진행 되는 온라인 게임의 경우에는 그렇지 않다.
내 프로세스가 blocked 상태에 놓여 있는 동안에도 다른 유저들은 계속 해서 게임을 진행하고 있기 때문에 중간에 생겨버린 공백 시간으로 인한 문제들이 발생할 수 있는 것이다.
일례로 게임 소켓의 수신 버퍼에는 서버로 부터 받은 데이터들이 차곡 차곡 쌓여 마냥 클라이언트의 처리를 기다리고 있어 서버로 부터 최신의 정보를 제 때에 피드백 받을 수 없게 된다거나 프로그램의 처리가 하염 없이 뒤로 밀려 버리는 일이 발생 할 수도 있다.
결국 프로세스가 장시간의 blocked 상태에서 구동 상태로 복구 되었을때 그 공백 시간 동안 쌓여버린 데이터들을 어떻게 처리해야 할 것인가 하는 것이 문제가 된다.
프레임 스킵은 바로 이러한 문제를 해결 하기 위한 기법이다.
이름에서 느껴지듯이 지연된 시간 동안 쌓여버린 프레임들을 스킵해 버리는 방법이다.
기존에 쌓인 것들을 모두 스킵하여 무시해 버리고 현재 시점 부터 진행을 이어 나간다거나, FPS 조절을 위해 제한된 루프당 대기 시간을 무시하고 쌓인 데이터들을 빠르게 처리하여 공백을 좁혀 없앤다거나 하는 방식이다.
예를 들어 몇몇 온라인 게임들을 보면 장시간 알트탭을 눌렀다가 들어 왔을때 게임이 일정 시간 동안 빨리 감기 처럼 진행 되는 경우가 있는데 이것이 바로 프레임 스킵이 적용된 사례다.
다음은 위와 같이 빨리 감기 식의 프레임 스킵 기법을 아주 간단하게 구현해 본 코드다 .
float dwElapsedTicks = 0; //루프 순회간 경과된 시간
DWORD dwLastTicks = 0;//이전 루프 순회가 끝난 시간
DWORD dwInterval = 1000 / FPS; //루프 순회당 소요 되어야 하는 시간
dwLastTicks = GetTickCount();
while(1)
{
dwElapsedTicks += (GetTickCount() - dwLastTicks);
if(dwElapsedTicks < dwInterval)//경과된 시간이 루프 1순회당 지연 되어야 할 시간 보다 작은 경우, 다시 말해 FPS가 빠르게 나오게 될 경우 이를 정해진 FPS 크기로 낮춘다.
{
Sleep(dwInterval - dwElapsedTicks);
dwElapsedTicks = 0;//경과된 시간 만큼 대기 했으므로 경과된 시간을 무효화 한다.
}
else//FPS가 저하 되어 지연 시간이 커졌을때, 지연으로 인해 처리가 늦어진 데이터들을 Sleep() 함수 호출을 스킵하는 방식으로 대기 시간 없이 빠르게 처리한다.
{
dwElapsedTicks -= dwInterval;//루프를 한 번 순회 할 때마다 기존의 경과 시간에서 루프 1순회당 소요되어야 하는 시간을 차감한다.
}
dwLastTicks = GetTickCount();//루프문 1순회 종료 시점을 기록
}
만약 루프문의 시작과 끝이 아닌 중간에서 우선순위에 밀려 context switching 되거나 하는 등의 예상치 못한 지연 시간이 추가로 발생하는 경우가 우려 된다면 루프문 중간에서 발생할지 모르는 추가적인 지연 시간도 측정하여 경과 시간에 누적시켜 처리 하는 방법도 생각해 볼 수 있을 것이다.
=================================
=================================
=================================
'프로그래밍 관련 > 프로그래밍 관련팁' 카테고리의 다른 글
윈도우 2008 에러발생시 복구 모드로 회복하기(복구모드 백업시 가능) (0) | 2011.02.28 |
---|---|
델파이와 C 비교 (0) | 2011.02.15 |
서버 프로그래밍 책 추천 (0) | 2011.01.29 |
비쥬얼 스튜디오 2010 UI추가시(비쥬얼어시스트X) 지울떄 유의 사항 (0) | 2011.01.20 |
비쥬얼스튜디오 2010 창화면이 안 바꾸어지거나 빌드가 느릴때 (0) | 2011.01.19 |
댓글 영역