타이머

WanJu Kim·2022년 12월 27일

Direct3D

목록 보기
5/29

애니메이션을 수행하려면 시간을 측정해야 한다. 특히 프레임과 프레임 사이의 시간을 측정할 수 있어야 한다. 이번 포스팅에서는 그걸 알아본다.

정확한 시간을 측정하기 위해서, <windows.h> 라이브러리가 제공하는 함수들을 이용해야 한다. QueryPerformanceCounter()를 통해 성능 타이머(performance timer)를 얻을 수 있다. 성능 카운터(performance counter)라고도 부른다. 코드는 이렇게 작성한다.

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

몇 가지 궁금증이 생긴다. __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으로 초기화, 그리고 리셋할 때마다 초기화한다.

profile
Question, Think, Select

0개의 댓글