KoreanFoodie's Study

4-4. 시간 측정과 애니메이션 (타이머, GameTimer 클래스) 본문

Game Dev/DirectX

4-4. 시간 측정과 애니메이션 (타이머, GameTimer 클래스)

GoldGiver 2021. 11. 18. 15:06

DirectX 12 3D 게임 프로그래밍의 바이블

'DirectX 12를 이용한 3D 게임 프로그래밍 입문'을 읽으며 내용을 정리하고 중요한 부분을 기록하는 글입니다.


4-4. 시간 측정과 애니메이션 (타이머, GameTimer 클래스)

 

알아 두어야 할 개념들 : 

 

1. 성능 타이머

정밀한 시간 측정을 위해, 이 책의 예제들은 Windows가 제공하는 성능타이머(perfomance timer)를 사용한다. 이를 성능 카운터(performance counter)라고도 부른다(Windows.h 를 include).

성능 타이머의 시간 측정 단위는 '지나간 클럭 틱(tick)'들의 개수(count)이다. 성능 타이머로부터 틱 수 단위의 현재 시간을 얻을 때에는 다음과 같이 QueryPerformanceCounter 함수를 사용한다.

__int64 currTime;
QueryPerformanceCounter((LARGE_INTEGER*)&currTime);

이 함수는 반환값이 아니라 매개변수를 통해 64비트 정수로 현재 시간 값을 돌려준다.

QueryPerformanceFrequency 함수는 초 단위 틱 수를 돌려준다. 이 값의 역수를 취해 틱당 초 수를 구한 다음, 실제 틱 수 valueInCounts를 곱하면 초 단위 시간이 나온다. 

 

어떤 작업에 걸린 시간을 계산할 때는 다음과 같은 값을 활용한다.

__int64 A = 0;
QueryPerformanceCounter((LARGE_INTEGER*)&A);

/* 어떤 작업을 수행한다. */

__int64 B = 0;
QueryPerformanceCounter((LARGE_INTEGER*)&B);

어떤 작업에 걸린 시간은 (B-A) * mSecondsPerCount 초이다.

 

 

2. GameTimer 클래스

GameTimer 클래스의 구현을 보자.

class GameTimer
{
public:
  GameTimer();
  float GameTime()const; // in seconds
  float DeltaTime()const; // in seconds
  void Reset(); // Call before message loop.
  void Start(); // Call when unpaused.
  void Stop(); // Call when paused.
  void Tick(); // Call every frame.
private:
  double mSecondsPerCount;
  double mDeltaTime;
  __int64 mBaseTime;
  __int64 mPausedTime;
  __int64 mStopTime;
  __int64 mPrevTime;
  __int64 mCurrTime;
  bool mStopped;
};

 

생성자의 역할은 성능 타이머의 주파수를 조회해서 틱당 초 수를 설정하는 것이다.

GameTimer::GameTimer()
: mSecondsPerCount(0.0), mDeltaTime(-1.0), mBaseTime(0), 
 mPausedTime(0), mPrevTime(0), mCurrTime(0), mStopped(false)
{
  __int64 countsPerSec;
  QueryPerformanceFrequency((LARGE_INTEGER*)&countsPerSec);
  mSecondsPerCount = 1.0 / (double)countsPerSec;
}

 

 

3. 프레임 간 경과 시간

프레임 사이의 경과 시간을 구하는 코드를 보자.

void GameTimer::Tick()
{
  if( mStopped )
  {
    mDeltaTime = 0.0;
    return;
  }
  // Get the time this frame.
  __int64 currTime;
  QueryPerformanceCounter((LARGE_INTEGER*)&currTime);
  mCurrTime = currTime;
  // Time difference between this frame and the previous.
  mDeltaTime = (mCurrTime - mPrevTime)*mSecondsPerCount;
  // Prepare for next frame.
  mPrevTime = mCurrTime;
  // Force nonnegative. The DXSDK’s CDXUTTimer mentions that if the 
  // processor goes into a power save mode or we get shuffled to
  // another processor, then mDeltaTime can be negative.
  if(mDeltaTime < 0.0)
  {
    mDeltaTime = 0.0;
  }
}
float GameTimer::DeltaTime()const
{
  return (float)mDeltaTime;
}

 

응용 프로그램의 메시지 루프에서는 Tick 메서드를 다음과 같은 방식으로 호출한다.

int D3DApp::Run()
{
  MSG msg = {0};
 
  mTimer.Reset();
  while(msg.message != WM_QUIT)
  {
    // If there are Window messages then process them.
    if(PeekMessage( &msg, 0, 0, 0, PM_REMOVE ))
    {
      TranslateMessage( &msg );
      DispatchMessage( &msg );
    }
    // Otherwise, do animation/game stuff.
    else
    { 
      mTimer.Tick();
      if( !mAppPaused )
      {
        CalculateFrameStats();
        Update(mTimer); 
        Draw(mTimer);
      }
      else
      {
        Sleep(100);
      }
    }
  }
  return (int)msg.wParam;
}

 

 

4. 전체 시간

전체 시간은 타임어택 등의 구현에 꼭 필요하다.

 

또한 일시정지 상황에서 물체의 이동도 제어해야 한다.

 

응용 프로그램이 타이머를 일시 정지하거나 재개하는 코드를 보자.

void GameTimer::Stop()
{
  // If we are already stopped, then don’t do anything.
  if( !mStopped )
  {
    __int64 currTime;
    QueryPerformanceCounter((LARGE_INTEGER*)&currTime);
    // Otherwise, save the time we stopped at, and set 
    // the Boolean flag indicating the timer is stopped.
    mStopTime = currTime;
    mStopped = true;
  }
}
void GameTimer::Start()
{
  __int64 startTime;
  QueryPerformanceCounter((LARGE_INTEGER*)&startTime);
  // Accumulate the time elapsed between stop and start pairs.
  //
  //        |<-------d------->|
  // ---------------*-----------------*------------> time
  //       mStopTime    startTime   
  // If we are resuming the timer from a stopped state...
  if( mStopped )
  {
    // then accumulate the paused time.
    mPausedTime += (startTime - mStopTime); 
    // since we are starting the timer back up, the current 
    // previous time is not valid, as it occurred while paused.
    // So reset it to the current time.
    mPrevTime = startTime;
    // no longer stopped...
    mStopTime = 0;
    mStopped = false;
  }
}

 

마지막으로 TotalTime 멤버 함수는 Reset이 호출된 이후 흐른 시간에서 일시 정지된 시간을 제외한 시간을 돌려준다.

float GameTimer::TotalTime()const
{
// If we are stopped, do not count the time that has passed
// since we stopped. Moreover, if we previously already had
// a pause, the distance mStopTime - mBaseTime includes paused 
// time,which we do not want to count. To correct this, we can
// subtract the paused time from mStopTime: 
//
//        previous paused time
//         |<----------->|
// ---*------------*-------------*-------*-----------*------> time
// mBaseTime             mStopTime  mCurrTime
  if( mStopped )
  {
    return (float)(((mStopTime - mPausedTime)-
      mBaseTime)*mSecondsPerCount);
  }
// The distance mCurrTime - mBaseTime includes paused time,
// which we do not want to count. To correct this, we can subtract 
// the paused time from mCurrTime: 
//
// (mCurrTime - mPausedTime) - mBaseTime 
//
//           |<--paused time-->|
// ----*---------------*-----------------*------------*------> time
// mBaseTime    mStopTime    startTime   mCurrTime
 
  else
  {
    return (float)(((mCurrTime-mPausedTime)-
      mBaseTime)*mSecondsPerCount);
  }
}

 

Comments