상세 컨텐츠

본문 제목

게임 프레임 관련gamza.net : 가변 프레임율이 지원되는 FrameSkip

프로그래밍 관련/프로그래밍 관련팁

by AlrepondTech 2011. 2. 7. 12:19

본문

반응형

 

 

 

=================================

=================================

=================================

 

 

 

출처 : 

http://www.gamza.net/bbs/view.php?id=Article&page=1&sn1=&divpage=1&sn=off&ss=on&sc=on&select_arrange=headnum&desc=asc&no=14

 

** 가변 프레임율이 지원되는 FrameSkip **
copyrightⓒ Gamza

 

요즘 게임은 더 좋은 사양의 컴퓨터를 사용할때 더 좋은 장면을 보여주기위해 프레임율에 의존적이지 않게 루프를 작성하게 됩니다. 간단하게 말하자면 이전 프레임과 현재 프레임의 시간차(dt)를 이용하는거죠. 
다만 이렇게 하면 약간 신경쓰이는것들이 생기게되고, 확실히 프레임 스키핑을 이용한 고정 프레임율을 사용하는것이 편할때가 있습니다. 

그래서 가변프레임율 과 고정 프레임율을 동시에 적용하는 방법을 생각해보았습니다. 

기존에 '프레임 스키핑'이란 기법은 낮은 사양의 컴퓨터에서 고정프레임율을 보장하기 위한 기법으로, 메인루프가 설정해놓은 최대 프레임율보다 더 빨리돌수 없다는 한계를 가졌다고 알려져 있었습니다.

여기서는 특정코드들은 프레임스키핑을 사용해서 컴퓨터의 사양에 상관없이 고정프레임율을 가지고 동작하면서도, 기타 다른 코드들은 가능한한 빠르게 동작하도록 하는 코드를 소개합니다.

다음은 VC++6 에서 컴파일 테스트 해본 예제입니다.
 void   Main( HWND hWnd, float dt ) {     //******************************************************************     //   프레임율 비의존적인 코드     //      dt에 의해 진행되기때문에 가능한한 빨리도는 코드들.     //      시스템이 좋을수록 더 자주 호출된다.     //******************************************************************     // Animation( dt );          if( fs.Update(dt) )     {         //******************************************************************         //   프레임율 의존적인 코드         //      이부분은 frame skip에 의해 고정 프레임율을 갖고 동작한다.         //******************************************************************         static long counter=0;         counter++;         if( counter%100 == 0 )          printf( "static frame rate code : %d\n", counter );      }           if( ! fs.IsFrameSkip() )      {          //******************************************************************          //   경우에 따라 호출되지 않아도 되는 코드          //      이부분은 frame skip에 의해 skip될수 있다.          //      렌더링따위의 시간을 많이 잡아먹는 코드를 넣는다.          //      역시 시스템이 좋을수록 더 자주 호출된다.          //      숫자는 제시스템에서 나온 fps입니다.          //******************************************************************          Sleep( 0.8f*1000 );   // 10fps          //Sleep( 0.2f*1000 );   // 10fps          //Sleep( 0.05f*1000 );   // 20fps          //Sleep( 0.02f*1000 );   // 50fps          //Sleep( 0.002f*1000 );   // 100fps      }           // printf frame / sec      PrintFPS( dt );  } 
테스트는 프레임 스키핑을 10fps로 설정해 놓고 Sleep을 이용해서 전체적인 프레임율을 조절해보았습니다. 주석에도 써있다시피 Sleep값을 키워줘도 프레임 스키핑에 의해 10fps는 유지되고, 빨리돌수 있을때는 [프레임율 비의존적인 코드]와 [경우에 따라 호출되지 않아도 되는 코드]가 가능한한 자주 호출되게 됩니다. 

물론 [프레임율 의존적인 코드] 는 메인루프가 빠르던,느리던 상관없이 10fps로 돌아갑니다. 

아래는 가변 프레임율이 지원되는 FrameSkip 클래스 입니다. 
 class FrameSkip  {  public:      void Clear()      {          SetFramePerSec( 60.0f );          m_Timer = 0.0f;      }      void SetFramePerSec( float fps )      {          m_SecPerFrame = 1.0f/fps;      }           // 원하는 프레임보다 너무 빠르면,      // false를 반환해서 코드를 동작시키지 않도록 한다.      // dt는 '초'단위 (밀리초 아님!!!)      bool Update( float dt )      {          m_Timer += dt;                if( m_Timer<0 ) return false;                // 한프레임에 해당하는 시간을 뺀다.          m_Timer -= m_SecPerFrame;          return true;      }           // Update후에 호출해서 frame skip을 수행해야 하는지 검사한다.      // frame skip이 일어나야 한다면 true를 반환한다.      bool IsFrameSkip()      {          return m_Timer >= 0;      }           // 멤버변수와 생성/소멸자.      FrameSkip(){ Clear(); }      virtual ~FrameSkip(){}  protected:      float m_Timer;      float m_SecPerFrame;  };  
이 클래스를 이용하면 사용자 입력은 10fps로 동작시키면서, AI는 30fps로... 카메라이동은 가능한한 부드럽게 하는식으로.....
이런게 간단하게 구현됩니다.
 FrameSkip fs[2];  ...  fs[0].SetFramePerSec( 10 );  fs[1].SetFramePerSec( 30 );  ...  게임루프( float dt )  {      카메라이동( dt ); // 가능한한 빨리동작하는 부분           if( fs[0].Update(dt) ) 사용자 입력처리; // 10fps로 동작 하는 부분           if( fs[1].Update(dt) ) AI; // 30fps로 동작 하는 부분           if( ! ( fs[0].IsFrameSkip() || fs[1].IsFrameSkip() ) )      {          렌더링따위의 코드.....      }  } 

 

=================================

=================================

=================================

 

 

 

 

반응형

 

 

728x90

 

 
출처: http://data-forge.blogspot.kr/2013/09/fps.html

FPS 제어와 프레임 스킵 구현

 
1. 프레임 조절

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를 맞춘다는 것이다.

이를 도식화 하면 다음과 같다.

 

 

 
예를 들어 FPS 값으로 30이 주어질 경우, 프로그램은 초당 30 프레임의 실행 속도를 유지하기 위해 루프를 한 번 순회 할 때마다 약 33ms 씩, 60 FPS인 경우에는 약 16ms 씩의 시간이 소요 되도록 Sleep() 함수에 의해 대기 상태에 빠짐으로써 지연이 강제 되는 것이다.

만약 루프 순회 간에 지연 시간이 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 되거나 하는 등의 예상치 못한 지연 시간이 추가로 발생하는 경우가 우려 된다면 루프문 중간에서 발생할지 모르는 추가적인 지연 시간도 측정하여 경과 시간에 누적시켜 처리 하는 방법도 생각해 볼 수 있을 것이다.
 

 

 

=================================

=================================

=================================

 

 

 

반응형


관련글 더보기

댓글 영역