本来想直接填入书中标题“计时与动画”的,但是貌似这一节的内容与动画关系并不大,反倒是基本全在讲性能计时器和游戏计时器的使用方法及应用,所以将标题改为“常用计时器”
性能计时器
为了能够精确的度量时间,需要使用性能计时器。若需要调用查询性能计时器的win32函数,需要包含windows.h文件
性能计时器所用的时间度量单位称为计数。可以调用QueryPerformanceCounter函数来获取性能计时器测量的当前时刻值(以计数为单位):
__int64 currTime;
QueryPerformanceCounter((LARGE_INTEGER*)&CURRtIME);
此函数返回的当前时刻值是一个64位的整数
再用QueryPerformanceFrequency函数来获取性能计时器的频率(单位为计数/秒):
__int64 countsPerSec;
QueryPerformanceFrequency((LARGE_INTERGER*)&countsPerSec);
每个计数所代表的秒数即为上述性能计时器的频率的倒数:
mSecondPerCount=1.0/(double)countsPerSec;
将读取的时刻计数值valueInCount乘以转换因子mSecondsPerCount即可转换为秒:
valueInSecs=valueInCounts*mSecondsPerCount;
单次调用QueryPerformanceCounter函数所返回的时刻值并无太大意义,一般来说,是在较短的时间内调用两次,求出差值来计算两次操作的时间间隔
注意:
对于一台拥有多个处理器的设备,由于基本输入/输出系统(BIOS)或硬件抽象层(HAL)上的缺陷,导致了在不同的处理器上可能会得到不同的结果,对此可以使用SetThreadAffinityMask函数,防止应用程序的主线程切换到其他的处理器上执行指令,从而保证每次都能获得同一个处理器的性能计时器的返回值,从而得到正确的时间间隔
D3D12已经封装了一些对应的API,搜索《Timing》(dn903946)
注:直接在MSDN里面搜索“Direct3D Timing”就可以看到相关的描述了,链接如下:
https://learn.microsoft.com/zh-cn/windows/win32/direct3d12/timing
游戏计时器类
接下来将讨论游戏计时器类的实现
class GameTimer
{
public:
GameTimer();
float TotalTime()const;//秒为单位
float DeltaTime()const;//秒为单位
void Reset();//开始消息循环之前调用
void Start();//解除计时器暂停时调用
void Stop();//暂停计时器时调用
void Tick();//每帧调用
private:
double mSecondsPerCount;
double mDetalTime;
__int64 mBaseTime;
__int64 mPausedTime;
__int64 mStopTime;
__int64 mPervTime;
__int64 mCurrentTime;
bool mStopped;
};
帧与帧的时间间隔
计算帧与帧之间的间隔的流程如下:
在帧显示时性能计数器返回时刻,以此帧与之前帧返回的时刻值计算出帧与帧之间的时间间隔,计算时间间隔:
void GameTimer::Tick()
{
if(mStopped)
{
mDeltaTime=0.0;
return;
}
//获得本帧开始显示的时刻
__int64 currTime;
QueryPerformanceCounter((LARGE_INTERGER*)&currTime);
mCurrentTime=currTime;
//本帧与前一帧的时间差
mDeltaTime=(mCurrTime-mPrevTime)*mSecondsPerCount;
//准备计算本帧与下一帧的时间差
mPrevTime=mCurrTime;
//使时间差为非负值。DXSDK中的CDXUTTimer示例注释里提到:若处理器处于节能模式,或者在计算两帧间时间差的过程中切换到另一个处理器时(即两次调用在不同的处理器上),则mDeltaTime有可能会成为负值
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(PeekMessage(&msg,0,0,0,PM_REMOVE))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
//否则执行动画与游戏的相关逻辑
else
{
mTimer.Tick();
if(!mAppPaused)
{
CalculateFrameStates();
Update(mTimer);
Draw(mTimer);
}
else
{
Sleep(100);
}
}
}
return (int)msg.wParam;
}
采用这种方法时,需要在每一帧都计算Δt,并将其送入Update方法。只有这样才可以根据前一帧动画帧所花费的时间对场景进行更新。以下是Reset方法的具体实现
void GameTimer::Reset()
{
__int64 currTime;
QueryPerformanceCounter((LARGE_INTERGER*)&currTime);
mBaseTime=currTime;
mPrevTime=currTime;
mStopTime=0;
mStopped=false;
}
里面的一些变量将在下一主题进行解释,但可以看出,在调用Reset时会将mPrevTime初始化为当前时刻,这一步十分关键,因为在第一帧之前并不存在任何画面帧,同时前一个时间戳也不存在,因此在开始消息循环之前需要使用Reset方法对mPrevTime进行初始化
tips:
可以统计较近一段时间的Δt,以某种统计函数(比如说划分一个ΔT值,从当前时刻向前的几倍ΔT值分成的几段内的Δt求平均值,距离现在时刻越近的Δt值所占的权重值越大,以权重比例求算出加权平均Δt,以对后续的Δt进行预测,ΔT的值应按照临近的Δt的变化进行调整,防止Δt出现稳定突变造成的误差,同样,ΔT的调整需要有多种数学模型的拟合分析以增加精度)
总时间
为了统计总时间,使用以下几个变量
__int64 mBaseTime;
__int64 mPauseTime;
__int64 mStopTime;
如以上reset方法,在调用reset方法时,将mBaseTime初始化为当前时刻,这个时刻可以被认为是应用程序的开始时刻。大多数情况下reset方法只在消息循环前被调用一次,所以在应用程序的整个生命周期中,mBaseTime一般会保持不变。变量mPausedTime储存的是所有暂停时间之和。mStopTime会给出计时器停止(应用程序暂停)的时刻,借此可记录暂停的时间。
Stop和Start是GameTimer类中的两个关键方法。当程序处于未暂停与暂停的状态时可以分别调用它们,以令GameTimer能够记录暂停的时间。
void GameTimer::Stop()
{
//如果已经处于停止状态那就什么也不做
if(!mStopped)
{
__int64 currTime;
QueryPerformanceCounter((LARGE_INTERGER*)&currTime);
//否则保存停止的时刻,设置标志指示计时器已经停止
mStopTime=currTime;
mStopped=ture;
}
}
void GameTimer::Start()
{
__int64 startTime;
QueryPerformanceCounter((LARGE_INTERGER*)&startTime);
/*
累加调用stop和start这两个方法之间的暂停时间间隔
|<----d----->|
----*----------*------------*-------------->时间
mBaseTime mStopTime startTime
*/
//如果从停止状态继续计时
if(mStopped)
{
//累加暂停时间
mPausedTime+=(startTime-mStopTime);
//在重新开启计时器时,前一帧的时间mPrevTime是无效的,这是因为它储存的是暂停时前一帧的开始时刻,因此需要将它重置为当前时刻
mPrevTime=startTime;
//已经不是停止状态
mStopTime=0;
mStopped=false;
}
}
最后就可以用成员函数TotalTime返回自调用Reset函数开始不计暂停时间的总时间了,它的具体实现如下:
float GameTimer::TotalTime()const
{
/*
如果正处于停止状态,则忽略本次停止时刻至当前时刻这段时间。此外,如果之前已有过暂停的情况那么也不应该统计mStopTime-mBaseTime这段时间内的暂停时间,为了做到这一点,可以从mStopTime中再减去暂停时间mPauseTime
前一次暂停时间 当前的暂停时间
|<------------>| |<------------>|
-------*-------------*--------------*--------------*--------------*----------->
mBaseTime mStopTime0 startTime mStopTime mCurrTime
*/
if(mStopped)
{
return(float)(((mStopTime-mPausedTime)-mBaseTime)*mSecondsPerCount);
}
/*
并不希望统计mCurrTime-mBaseTime内的暂停时间,可以通过从mCurrTime中再减去暂停时间mPausedTime来实现这一点。(mCurrTime-mPausedTime)-mBaseTime
|<---暂停时间-->|
----------*----------*--------------*-------------*----------->时间
mBaseTime mStopTime startTime mCurrTime
*/
else
{
return (float)(((mCurrTime-mPausedTime)-mBaseTime)*mSecondsPerCount);
}
}
在发生某事件时,可以重新创建一个GameTimer实例用于计时
但是实际上这样有点杀鸡用牛刀了,需要有一个专门用于事件计时的类以轻量化提升性能
声明
内容来自《DirectX12 3D游戏开发实战》,本文内容是原文与自己添加的一些笔记与观点的整合,搜索并整合了一些源于网络的内容,用于学习交流,侵删