Performance Timer

Để có được thời gian chính xác thì chúng ta sẽ dùng performance timer.

Performance Timer đo thời gian theo đơn vị gọi là counts. Chúng ta lấy thời gian hiện tại được đo bằng counts bằng hàm QueryPerformanceCounter() như sau:


__int64 currTime;

QueryPerformanceCuonter((LARGE_INTEGER*)&currTime);

Hàm này trả về thời gian hiện tại qua tham số của nó có kiểu dữ liệu là số nguyên 64bit.

Để lấy được tần số (Số count mỗi giây) của performance timer thì chúng ta sử dụng hàm QueryPerformanceFrequency(). Cách dùng như sau:


__int64 countsPerSec;

QueryPerformanceFrequency((LARGE_INTEGER*)&countsPerSec);

Thời gian một count được tính bằng ngịch đảo của countPerSec;


mSecondPerCount = 1.0 / (double) countsPerSec;

Như thế để chuyển count sang đơn vị giây thì ta chỉ việc nhân cho mSecondPerCount:


valueInSec = valueInCounts * mSecondInCount;

Các giá trị trả về của hàm QueryPerformanceCounter không đặc biệt thú vị trong bản thân chúng. Những gì chúng ta làm là có được các giá trị thời gian hiện tại sử dụng QueryPerformanceCounter, và sau đó nhận được các giá trị thời gian hiện tại sau đó một chút bằng QueryPerformanceCounter một lần nữa. Sau đó, thời gian đó trôi qua giữa hai lần lấy thời gian thời gian chính là sự thay đổi. Chúng ta luôn luôn nhìn vào sự khác biệt tương đối giữa hai lần đo thời gian, không phải là giá trị thực tế được trả về performance couner . Dưới đây code minh họa ý tưởng:


__int64 A = 0;

QueryPerformanceCounter((LARGE_INTEGER*)&A);

/* Do work */

__int64 B = 0;

QueryPerformanceCounter((LARGE_INTEGER*)&B);

Vì vậy thì thời gian chúng ta thực hiện là B – A counts hoặc là (B-A)*mSecondPerCount giây.

Chý ý  là MSDN có một lưu ý với hàm QuerPerformanceCounter() đó là trên máy tính có nhiều CPU thì không quan trọng Vi xử lý nào được gọi. Tuy nhiên thì có thể có nhiều kết qua khác nhau trên các vi xử lý khác nhau do vấn đề trong BIOS hoặc HAL. Bạn có thể sử dụng hàm SetThreadAffinityMask() để ứng dụng không chuyển sang các CPU khác.

Game Timer Class

Tiếp theo sau đây chúng ta sẽ thảo luận về Game Timer Class


class GameTimer
{
public:
 GameTimer();

float getGameTime()const; // in seconds
 float getDeltaTime()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;
};

Ở Constructor thì chúng ta sẽ lấy thông tin về tần số của Performance counter. Những biến khác sẽ được giải thích sau.

 


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

Thời gian trôi qua giữa những khung hình

Khi chúng ta đang vẽ khung hình của game thì chúng ta cần phải biết là bao nhiêu thời gian đã trôi qua giữa khung hình để mà chúng ta có thể cập nhật những sự kiện trong game dựa trên lượng thời gian đã mất. Việc tính thời gian mỗi frame được tính như sau : Giả sử t(i) là thời gian ở khung hình thư i và t(i-1) là thời gian của khung hình trước đó (tức là khùng hình i-1) Khi đó thì thời gian giữa 2 khung hình sẽ là  t(i) – t(i-1) Trong Real Time Rendering thì chúng ta thường dùng 30 frame mỗi giây để có được sự chuyển động mượt mà. Đoạn code ví dụ dưới đây sẽ giúp bạn hiểu rõ hơn.


void GameTimer::tick()
{
 if( mStopped )
 {
 mDeltaTime = 0.0;
 return;
 }

// Lấy thời gian hiện tại.
 __int64 currTime;
 QueryPerformanceCounter((LARGE_INTEGER*)&currTime);
 mCurrTime = currTime;

// Thời gian giữa 2 khung hình
 mDeltaTime = (mCurrTime - mPrevTime)*mSecondsPerCount;

// Chuẩn bị cho frame tiếp theo.
 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::getDeltaTime()const
{
 return (float)mDeltaTime;
}

Hàm tick() được gọi trong vòng lặp chính như sau:


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 )
 updateScene(mTimer.getDeltaTime());
 else
 Sleep(50);

drawScene();
 }
 }
 return (int)msg.wParam;
}

Cấu trúc hàm reset()


void GameTimer::reset()
{
 __int64 currTime;
 QueryPerformanceCounter((LARGE_INTEGER*)&currTime);

mBaseTime = currTime;
 mPrevTime = currTime;
 mStopTime = 0;
 mStopped = false;
}

Ở đây thì một vài biến vẫn chưa được đề cập đến nhưng bạn yên tâm tiếp sau đây sẽ có thôi.Tuy nhiên các bạn có thể thấy là biến mPrevTime được gán bằng thời gian hiện tại khi hàm reset được gọi. Điều này khá là quan trọng bởi vì ở khung hình đầu tiên thì không có khung hình nào trước nó cả. Và vì thế chúng ta cần gọi hàm reset() trước khi vòng lặp bắt đầu.

Game Time

Bây giờ chúng ta sẽ tính thời gian chạy xuyên suốt chương trình – không tính thời gian chương trình tạm dừng. Ví dụ dưới đây sẽ cho thấy việc tính thời gian có ích như thế nào.

Giả sử người chơi có 300 giây để hoàn thành màn chơi. Khi màn chơi bắt đầu chúng ta có thể lấy  t_start làm thời gian bắt đầu. Khi màn chơi bắt đầu ta có thể lấy thời gian hiện tại t để kiểm tra. Nếu t – t_start >  300 khi đó người chơi hết thời gian và thua.

ScreenShot_20160606171459.png

3 biến cốt lõi của Game Timer là :


__int64 mBaseTime;

__int64 mPausedTime;

__int64 mStopTime;

Như đã thấy ở trên thì biến mBasedTime được gán bằng thời gian hiện tại khi hàm reset() được gọi. Chúng ta có thể tưởng tượng đó là thời điểm mà ứng dụng bắt đầu chạy. Trong hầu hết trường hợp thì ta chỉ gọi reser() một lần trước khi vòng lặp Chương trình bắt đầu. Biến mPausedTime nắm dữ toàn bộ thời gian tạm dừng của chương trình. Chúng ta cần biến này để mà có thể tính thời gian chạy dựa vào tổng thời gian. mStopTime dùng để lưu thời diểm mà chương trình tạm dừng.

Hai hàm quan trọng của GameTimer đó là start() và stop() chúng sẽ được gọi khi CT tiếp tục chạy và khi tạm dừng. Code dưới đây cho thấy cách hoạt động của 2 hàm này:


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;
 }
}

 

Cuối cùng là hàm getGameTime() trả về thời gian chạy kể từ khi reset(). Không tính thời gian tạm dừng.


float GameTimer::getGameTime()const
{
// If we are stopped, do not count the time that has passed since
// we stopped.
//
// ----*---------------*------------------------------*-----> time
// mBaseTime     mStopTime                         mCurrTime

if( mStopped )
{
return (float)((mStopTime - 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
//
//                |<-----d----->|
// ----*----------*-------------*---------*-----> time
// mBaseTime    mStopTime    startTime   mCurrTime
else
{
return (float)(((mCurrTime-mPausedTime)-mBaseTime)*mSecondsPerCount);
}
}