애니메이션을 수행하려면 시간을 측정해야 한다. 특히 프레임과 프레임 사이의 시간을 측정할 수 있어야 한다. 이번 포스팅에서는 그걸 알아본다.
정확한 시간을 측정하기 위해서, <windows.h> 라이브러리가 제공하는 함수들을 이용해야 한다. QueryPerformanceCounter()를 통해 성능 타이머(performance timer)를 얻을 수 있다. 성능 카운터(performance counter)라고도 부른다. 코드는 이렇게 작성한다.
__int64 currTime;
QueryPerformanceCounter((LARGE_INTEGER*)¤tTime);
몇 가지 궁금증이 생긴다. __int64, LARGE_INTEGER는 무엇인가? 8바이트 큰 자료형이다. 큰 숫자를 담아내기 위함이다. 그리고 성능 카운터 자체는 무엇인가? 답변을 위해 마이크로소프트님이 말씀하셨다.
A pointer to a variable that receives the current performance-counter value, in counts.
그냥 진짜 번역 그대로 '성능 카운터'이다. 그냥 시간 비례해서 무수히 커진다고 생각하면 된다. 이걸 어떻게 쓰는가? 여기서 또 다른 함수 QueryPerformanceFrequency()가 등장한다.
__int64 countsPerSec;
QueryPerformaceFrequency((LARGE_INTEGER*)&countsPerSec);
이건 뭘 반환하는가? '초당 카운터'를 반환한다. 이걸 역수 취하면 '카운터당 1초'다. 여기서 아까 구한 카운터를 곱하면 1초가 나온다.
mSecondsPerCount = 1.0 / (double)countsPerSec;
valueInSecs = valueInCounts * mSecondsPerCount;
어쨌든 우리가 중요시하는 건, 프레임간의 시간 차이다. 이를 델타 타임이라 부르겠다. 델타는 수학에서 '차이'를 의미하는 용어다.
다음으로는 클래스를 보면서 알아보자.
GameTimer.h
class GameTimer
{
public:
GameTimer();
float TotalTime() const; // 전체 시간.
float DeltaTime() const; // 델타 타임.
void Reset(); // 메시지 루프 이전.
void Start(); // 타이머 시작, 재개.
void Stop(); // 일시정지.
void Tick(); // 매 프레임.
private:
double mSecondsPerCount; // 초당 개수의 역수.
double mDeltaTime; // 델타 타임.
__int64 mPrevTime; // 프레임간 차이를 구하기 위한 이전 시간.
__int64 mCurrTime; // 현재 시간.
// 전체 시간용 변수.
__int64 mBaseTime;
__int64 mPausedTime;
__int64 mStopTime;
bool mStopped; // 정지시키면 true.
};
GameTimer.cpp
#include "GameTimer.h"
#include <Windows.h>
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;
}
생성자다. 모든 변수 값을 초기화시켜줬다. 참고로 변수 앞에 m이 적힌 건 '멤버 변수'라는 의미다. 가독성을 높여준다.
void GameTimer::Tick()
{
// 정지 됐다면, 델타 타임을 멈춘다.
if (mStopped)
{
mDeltaTime = 0.0;
return;
}
// 이번 프레임의 시간을 얻는다.
__int64 currTime;
QueryPerformanceCounter((LARGE_INTEGER*)&currTime);
mCurrTime = currTime;
// 이 시간과 이전 프레임의 시간의 차이를 구한다.
mDeltaTime = (mCurrTime - mPrevTime) * mSecondsPerCount;
// 다음 프레임 준비.
mPrevTime = mCurrTime;
// 음수가 될 가능성 제거.
if (mDeltaTime < 0.0)
mDeltaTime = 0.0;
}
float GameTimer::DeltaTime() const
{
return (float)mDeltaTime;
}
위의 코드에서 델타 타임을 구한다. 이는 응용프래그램의 메시지 루프에서 다음과 같이 이용한다.
int D3DApp::Run()
{
MSG msg = {0};
mTimer.Reset(); // 리셋.
while(msg.message != WM_QUIT)
{
// Windows 메시지가 있으면 처리한다.
if(PeekMessage( &msg, 0, 0, 0, PM_REMOVE ))
{
TranslateMessage( &msg );
DispatchMessage( &msg );
}
// 없으면 애니메이션 / 게임 작업을 수행한다.
else
{
mTimer.Tick(); // 여기서 갱신.
if( !mAppPaused )
{
CalculateFrameStats();
UpdateScene(mTimer.DeltaTime()); // 갱신된 시간으로 다시 갱신.
DrawScene();
}
else
{
Sleep(100);
}
}
}
return (int)msg.wParam;
}
이렇게 시간을 매번 계산해서 UpdateScene 함수에 넘겨준다. 메시지가 들어오기 전에 쓰인 리셋 함수는 다음과 같다.
void GameTimer::Reset()
{
__int64 currTime;
QueryPerformanceCounter((LARGE_INTEGER*)&currTime);
mBaseTime = currTime;
mPrevTime = currTime;
mStopTime = 0;
mStopped = false;
}
리셋은 무슨 역할을 하는가? 애니메이션의 첫 프레임은 이전 프레임이란 게 없으므로 델타 타임을 구하기 힘들다. 이를 리셋 함수가 mPrevTime 변수를 현재 시간으로 설정해줘서 델타 타임을 구할 수 있다.
스타크래프트 같은 거 하면 게임을 도중에 멈추고 재개할 수 있다. 그럴 때 게임 시간은 흐르지 않는다. 어떻게 하는가?
void GameTimer::Start()
{
__int64 startTime;
QueryPerformanceCounter((LARGE_INTEGER*)&startTime);
if (mStopped)
{
mPausedTime += (startTime - mStopTime);
mPrevTime = startTime;
mStopTime = 0;
mStopped = false;
}
}
void GameTimer::Stop()
{
if (!mStopped)
{
__int64 currTime;
QueryPerformanceCounter((LARGE_INTEGER*)&currTime);
mStopTime = currTime;
mStopped = true;
}
}
이런 함수를 만들면 된다. 게임을 멈추면, mStopTime에 그 시간을 기록해뒀다가 다시 시작할 때 mPausedTime에 멈춘 시간을 누적시킨다. 이는 게임 총 시간을 구할 때, 제외하는 방식으로 쓰인다. 바로 다음처럼 말이다.
float GameTimer::TotalTime() const
{
if (mStopped)
return (float)(((mStopTime - mPausedTime) - mBaseTime) * mSecondsPerCount);
else
{
return (float)(((mCurrTime - mPausedTime) - mBaseTime) * mSecondsPerCount);
}
}
mBaseTime은 맨 처음에 0으로 초기화, 그리고 리셋할 때마다 초기화한다.