《DirectX12 3D游戏开发实战》读书笔记4:常用计时器

文章目录

    • 性能计时器
    • 游戏计时器类
    • 帧与帧的时间间隔
    • 总时间
    • 声明

本来想直接填入书中标题“计时与动画”的,但是貌似这一节的内容与动画关系并不大,反倒是基本全在讲性能计时器和游戏计时器的使用方法及应用,所以将标题改为“常用计时器”

性能计时器

为了能够精确的度量时间,需要使用性能计时器。若需要调用查询性能计时器的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游戏开发实战》,本文内容是原文与自己添加的一些笔记与观点的整合,搜索并整合了一些源于网络的内容,用于学习交流,侵删

你可能感兴趣的:(3d,游戏,c++,microsoft)