Direct3D 10学习笔记(二)——计时器

2023-05-11,,

本篇将简单整理Direct3D 10的计时器实现,具体内容参照《 Introduction to 3D Game Programming with DirectX 10》(中文版有汤毅翻译的电子书《DirectX 10 3D游戏编程入门》)。

1.高精度性能计数器

Direct3D10使用高精度性能计数器(精度达微秒级)实现精确时间测量,为了调用下面介绍的两个Win32计数器API,需要添加包含语句“#include <windows.h>”

 BOOL QueryPerformanceCounter(LARGE_INTEGER * lpPerformanceCount);

lpPerformanceCount:参数指向计数器的值,即该参数将用于返回当前的计时,为一个64位整型。其计时单位称为计数,即所谓的“滴答”。

由于性能计数器与系统有关,需要知道每秒滴答声的个数,即滴答的频率,然后用前后两次计时的差值去除以频率才能得到这段时间一共走过了多少秒,故还需要计算计数频率。

 BOOL QueryPerformanceFrequency(LARGE_INTEGER * lpFrequency);

lpFrequency:参数指向计数器频率的值,即该参数将用于返回系统计数频率。

计时示例:

     long long preTime,currTime,valueInSecs,valueInCounts;
QueryPerformanceCounter(reinterpret_cast<LARGE_INTEGER*>(&preTime));
long long CountsPerSec;
QueryPerformanceFrequency(reinterpret_cast<LARGE_INTEGER*>(&CountsPerSec));
//计算出转换因子SecondsPerCount,避免重复进行除法计算
double SecondsPerCount = 1.0f / static_cast<double>(countsPerSec);
QueryPerformanceCounter(reinterpret_cast<LARGE_INTEGER*>(&currTime));
//计算两次计时差值
valueInCounts = currTime - preTime;
//将两次计时差值转换成以秒为单位
valueInSecs = valueInCounts * SecondsPerCount;

2.设计游戏计时器类(GameTimer类)

类的头文件为GameTimer.h,具体实现放在GameTimer.cpp中。

一个计时器,需要的功能有初始化、开始计时、停止计时、恢复计时、计算两次计时时间差以及计算总计时时长。

初始化由构造函数完成,该过程中可将计时频率、计时转换因子等必要的值先求出来。需要添加的成员有:每次计时所需时间SecondsPerCount,初始计时BaseTime等。

计时过程中可一并计算两次计时的时间差。需要添加的成员有:前一次计时PreTime,当前计时CurrTime,两次计时时间间隔DeltaTime。

停止计时需要用一个状态量来阻止计时函数的执行,同时还应记录停止时刻的CurrTime,则需要添加的成员有:StopTime,StoppedState。

恢复计时需要额外计算暂停了多长时间,则需要添加的成员有:PauseTime。

同时,还需要添加一些返回计时的函数以及重置函数。

所以其头文件可以设计如下:

 GameTimer.h

 #include <windows.h>

 class GamerTimer
{
public:
GamerTimer();
~GamerTimer(); FLOAT getGameTime()const;
FLOAT getDeltaTime()const;
VOID reset();
VOID start();
VOID stop();
VOID tick(); private:
DOUBLE m_dSecondsPerCount;
DOUBLE m_dDeltaTime; LONGLONG m_llBaseTime;
LONGLONG m_llPauseTime;
LONGLONG m_llStopTime;
LONGLONG m_llPreTime;
LONGLONG m_llCurrTime; BOOL m_bStopped;
};

下面逐个讲解类方法的实现:

 GamerTimer::GamerTimer()
:m_dSecondsPerCount(0.0f), m_dDeltaTime(0.0f), m_llBaseTime(),
m_llPauseTime(), m_llStopTime(), m_llPreTime(), m_llCurrTime(),
m_bStopped(FALSE)
{
LONGLONG countsPerSec;
QueryPerformanceFrequency(reinterpret_cast<LARGE_INTEGER*>(&countsPerSec));
m_dSecondsPerCount = 1.0f / static_cast<DOUBLE>(countsPerSec);
}

在这个构造函数中,将所有数据初始化完毕后,还需计算出前面重复提到的转换因子,当计时单位由计数转换为秒时,就需要乘以这个转换因子。获取频率时用了reinterpret_cast进行指针类型的强制转换,不能用static_cast等,这属于C++的内容,今后不再累赘。

 VOID GamerTimer::tick()
{
//计时停止则重置时间间隔
if (m_bStopped)
{
m_dDeltaTime = 0.0f;
return;
} //否则,计算时间间隔 //获取当前帧时间计数
LONGLONG currTime;
QueryPerformanceCounter(reinterpret_cast<LARGE_INTEGER*>(&currTime));
m_llCurrTime = currTime; //计算当前帧与上一帧时间差,单位为秒
m_dDeltaTime = (m_llCurrTime - m_llPreTime)*m_dSecondsPerCount; //为下一帧计时做准备
m_llPreTime = m_llCurrTime; //负值处理,当处理器进入省电模式或切换到另一处理器,时间间隔可能为负
if (m_dDeltaTime < 0.0f)
{
m_dDeltaTime = 0.0f;
} return;
}

大部分内容都在注释里呈现了,值得留意的是,该方法的负值处理部分,提到了切换到另一处理器,意味着我们可能会使用多线程计时,只要保证计时器在子线程内一直正常运作,条件是该子线程未结束或未被阻塞/锁上(当然也还是有办法通过使用额外的计时来消除这段误差)。C++11/14标准提供了很方便的线程库,所以将计时器分离出来或许是个不错的尝试。

 FLOAT GamerTimer::getDeltaTime()const
{
return static_cast<FLOAT>(m_dDeltaTime);
}

返回两次计时的时间差,单位为秒。

 VOID GamerTimer::reset()
{
LONGLONG currTime;
QueryPerformanceCounter(reinterpret_cast<LARGE_INTEGER*>(&currTime)); m_llBaseTime = currTime;
m_llPreTime = currTime;
m_llStopTime = ;
m_bStopped = FALSE; return;
}

用于重置计时器,而不必销毁原来的计时器再构造一个。

 VOID GamerTimer::stop()
{
//如果已停止,不做任何操作;否则停止计时
if (!m_bStopped)
{
LONGLONG currTime;
QueryPerformanceCounter(reinterpret_cast<LARGE_INTEGER*>(&currTime)); //记录停止时刻,更新状态
m_llStopTime = currTime;
m_bStopped = TRUE;
} return;
}

停止/暂停计时的方法,记录调用时刻,并将停止状态设置为TRUE。

 VOID GamerTimer::start()
{
LONGLONG startTime;
QueryPerformanceCounter(reinterpret_cast<LARGE_INTEGER*>(&startTime)); //从初始状态开始计时或从暂停状态恢复计时
if (m_bStopped)
{
//计算暂停时长
m_llPauseTime = startTime - m_llStopTime; //由于中途暂停,上一帧时刻已无效,需更新到恢复计时时刻
m_llPreTime = startTime; //更新停止状态
m_llStopTime = ;
m_bStopped = FALSE;
} return;
}

开始/恢复计时的方法,主要注意需计算出暂停的时长。该方法将开始计时和恢复计时合并,原因是两种计时方法的前一时刻的停止状态m_bStopped均为FALSE,故它们能有相同的行为。

 FLOAT GamerTimer::getGameTime()const
{
//如果处于停止状态
if (m_bStopped)
{
return static_cast<FLOAT>((m_bStopped - m_llBaseTime - m_llPauseTime)*m_dSecondsPerCount);
}
//如果仍在计时
else
{
return static_cast<FLOAT>((m_llCurrTime - m_llBaseTime - m_llPauseTime)*m_dSecondsPerCount);
}
}

计算出总计时时长的方法。计算时注意应该剔除暂停总共花去的时间。

完整的实现代码如下:

 GameTimer.cpp

 #include "GameTimer.h"

 GamerTimer::GamerTimer()
:m_dSecondsPerCount(0.0f), m_dDeltaTime(0.0f), m_llBaseTime(),
m_llPauseTime(), m_llStopTime(), m_llPreTime(), m_llCurrTime(),
m_bStopped(FALSE)
{
LONGLONG countsPerSec;
QueryPerformanceFrequency(reinterpret_cast<LARGE_INTEGER*>(&countsPerSec));
m_dSecondsPerCount = 1.0f / static_cast<DOUBLE>(countsPerSec);
} VOID GamerTimer::tick()
{
//计时停止则重置时间间隔
if (m_bStopped)
{
m_dDeltaTime = 0.0f;
return;
} //否则,计算时间间隔 //获取当前帧时间计数
LONGLONG currTime;
QueryPerformanceCounter(reinterpret_cast<LARGE_INTEGER*>(&currTime));
m_llCurrTime = currTime; //计算当前帧与上一帧时间差,单位为秒
m_dDeltaTime = (m_llCurrTime - m_llPreTime)*m_dSecondsPerCount; //为下一帧计时做准备
m_llPreTime = m_llCurrTime; //负值处理,当处理器进入省电模式或切换到另一处理器,时间间隔可能为负
if (m_dDeltaTime < 0.0f)
{
m_dDeltaTime = 0.0f;
} return;
} FLOAT GamerTimer::getDeltaTime()const
{
return static_cast<FLOAT>(m_dDeltaTime);
} VOID GamerTimer::reset()
{
LONGLONG currTime;
QueryPerformanceCounter(reinterpret_cast<LARGE_INTEGER*>(&currTime)); m_llBaseTime = currTime;
m_llPreTime = currTime;
m_llStopTime = ;
m_bStopped = FALSE; return;
} VOID GamerTimer::stop()
{
//如果已停止,不做任何操作;否则停止计时
if (!m_bStopped)
{
LONGLONG currTime;
QueryPerformanceCounter(reinterpret_cast<LARGE_INTEGER*>(&currTime)); //记录停止时刻,更新状态
m_llStopTime = currTime;
m_bStopped = TRUE;
} return;
} VOID GamerTimer::start()
{
LONGLONG startTime;
QueryPerformanceCounter(reinterpret_cast<LARGE_INTEGER*>(&startTime)); //从初始状态开始计时或从暂停状态恢复计时
if (m_bStopped)
{
//计算暂停时长
m_llPauseTime = startTime - m_llStopTime; //由于中途暂停,上一帧时刻已无效,需更新到恢复计时时刻
m_llPreTime = startTime; //更新停止状态
m_llStopTime = ;
m_bStopped = FALSE;
} return;
} FLOAT GamerTimer::getGameTime()const
{
//如果处于停止状态
if (m_bStopped)
{
return static_cast<FLOAT>((m_bStopped - m_llBaseTime - m_llPauseTime)*m_dSecondsPerCount);
}
//如果仍在计时
else
{
return static_cast<FLOAT>((m_llCurrTime - m_llBaseTime - m_llPauseTime)*m_dSecondsPerCount);
}
} GamerTimer::~GamerTimer(){}

Direct3D 10学习笔记(二)——计时器的相关教程结束。

《Direct3D 10学习笔记(二)——计时器.doc》

下载本文的Word格式文档,以方便收藏与打印。