今天继续我们的初始化工作。我们将通过在消息循环中的Draw函数,获取它的执行时间,来计算得到FPS,即每秒平均执行帧数,和其倒数MSPF,即每帧平均执行时间。
首先我们创建一个GameTime类,专门管理游戏中的时间。头文件代码如下:
#pragma once
#include <Windows.h>
class GameTime
{
public:
GameTime();
float TotalTime()const; //游戏运行的总时间(不包括暂停)
float DeltaTime()const; //获取mDeltaTime变量
bool IsStoped(); //获取isStoped变量
void Reset(); //重置计时器
void Start(); //开始计时器
void Stop(); //停止计时器
void Tick(); //计算每帧时间间隔
private:
double mSencondsPerCount; //计数器每一次需要多少秒
double mDeltaTime; //每帧时间(前一帧和当前帧的时间差)
__int64 mBaseTime; //重置后的基准时间
__int64 mPauseTime; //暂停的总时间
__int64 mStopTime; //停止那一刻的时间
__int64 mPrevTime; //上一帧时间
__int64 mCurrentTime; //本帧时间
bool isStoped; //是否为停止的状态
};
然后我们挨个实现。首先是构造函数,构造函数会查询性能计数器的频率(一次多少秒)。核心函数QueryPerformanceCounter返回当前时间的计数器次数值,即mSencondsPerCount。这个值乘以次数便是时间。注意,传入其的参数,系统会用时间值自动赋值并随着运行时间增长而增长,从而改变输出时的次数值,所以输入的参数并不需要自己初始化。
GameTime::GameTime() : mSencondsPerCount(0.0), mDeltaTime(-1.0), mBaseTime(0),
mPauseTime(0), mStopTime(0), mPrevTime(0), mCurrentTime(0), isStoped(false)
{
//计算计数器每秒多少次,并存入countsPerSec中返回
//注意,此处为QueryPerformanceFrequency函数
__int64 countsPerSec;
QueryPerformanceFrequency((LARGE_INTEGER*)&countsPerSec);
mSencondsPerCount = 1.0 / (double)countsPerSec;
}
接着我们实现Tick函数,它是用来计算每帧的时间间隔,即mDeltaTime。
void GameTime::Tick()
{
if ( isStoped )
{
//如果当前是停止状态,则帧间隔时间为0
mDeltaTime = 0.0;
return;
}
//计算当前时刻的计数值
__int64 currentTime;
QueryPerformanceCounter((LARGE_INTEGER*)¤tTime);
mCurrentTime = currentTime;
//计算当前帧和前一帧的时间差(计数差*每次多少秒)
mDeltaTime = (mCurrentTime - mPrevTime) * mSencondsPerCount;
//准备计算当前帧和下一帧的时间差
mPrevTime = mCurrentTime;
//排除时间差为负值
if (mDeltaTime < 0)
{
mDeltaTime = 0;
}
}
随后将计算得到的mDeltaTime封装成函数,让外部只读调用。
float GameTime::DeltaTime()const
{
return (float)mDeltaTime;
}
然后实现Reset函数,重置计时器。这步在消息循环之前必须执行,且只执行一次,即运行一次游戏只执行一次重置。
void GameTime::Reset()
{
__int64 currTime;
QueryPerformanceCounter((LARGE_INTEGER*)&currTime);
mBaseTime = currTime; //当前时间作为基准时间
mPrevTime = currTime; //当前时间作为上一帧时间,因为重置了,此时没有上一帧
mStopTime = 0; //重置停止那一刻时间为0
isStoped = false; //重置后的状态为不停止
}
接下来实现Start和Stop函数,这两个函数是开始和暂停计时器,相当于修改其状态的开关。为了便于理解,我们将现在处于暂停的状态称为停止,将之前暂停的状态称为暂停。详细请看注释。
void GameTime::Stop()
{
if (!isStoped)//如果没有停止,则让其停止(如果停止则什么都不做)
{
__int64 currTime;
QueryPerformanceCounter((LARGE_INTEGER*)&currTime);
mStopTime = currTime; //将当前时间作为停止那一刻的时间(次数)
isStoped = true; //修改为停止状态
}
}
void GameTime::Start()
{
__int64 startTime;
QueryPerformanceCounter((LARGE_INTEGER*)&startTime);
if (isStoped)//如果是停止状态,则让其解除停止,保存暂停时间,修改停止状态
{
//计算出暂停的总时间(保存停止时间)
mPauseTime += (startTime - mStopTime);
//修改停止状态
mPrevTime = startTime;//相当于重置上一帧时刻
mStopTime = 0; //相当于重置停止的时刻
isStoped = false; //停止状态为假
}
//如果不是停止状态,则什么都不做
}
然后我们实现了TotalTime函数,这个函数是游戏运行的总时间,但不包括暂停时间。同样,为了便于理解,我们将现在处于暂停的状态称为停止,将之前暂停的状态称为暂停。
float GameTime::TotalTime()const
{
if (isStoped) //如果此时在暂停状态,则用停止时刻的时间去减之前暂停的总时间
{
return (float)((mStopTime - mPauseTime - mBaseTime) * mSencondsPerCount);
}
else
{
//如果不在暂停状态,则用当前时刻的时间去减暂停总时间
return (float)((mCurrentTime - mPauseTime - mBaseTime) * mSencondsPerCount);
}
}
最后我们还封装了获取isStoped变量的函数。
bool GameTime::IsStoped()
{
return isStoped;
}
到这儿,GameTime类封装完毕,回顾一下,我们发现它可以控制计时器的开关,可以计算帧与帧的时间间隔,还可以计算游戏运行总时间。
接下来,我们回到我们的主文件,实现一个CalculateFrameState函数,它主要是通过GameTime类中的数据计算FPS和MSPF,并显示在窗口栏上。注意:我开始运行到这里时,并没有出现fps和mspf,通过注释掉的调试模块,实时获得并显示了TotalTime();的值,最后才发现是在计算mSencondsPerCount时候用错了函数。所以这个调试模块很有用,关键它可以实时显示数值到窗口栏。
void CalculateFrameState()
{
static int frameCnt = 0; //总帧数
static float timeElapsed = 0.0f; //流逝的时间
frameCnt++; //每帧++,经过一秒后其即为FPS值
//调试模块
/*std::wstring text = std::to_wstring(gt.TotalTime());
std::wstring windowText = text;
SetWindowText(mhMainWnd, windowText.c_str());*/
//判断模块
if (gt.TotalTime() - timeElapsed >= 1.0f) //一旦>=0,说明刚好过一秒
{
float fps = (float)frameCnt;//每秒多少帧
float mspf = 1000.0f / fps; //每帧多少毫秒
std::wstring fpsStr = std::to_wstring(fps);//转为宽字符
std::wstring mspfStr = std::to_wstring(mspf);
//将帧数据显示在窗口上
std::wstring windowText = L"D3D12Init fps:" + fpsStr + L" " + L"mspf" + mspfStr;
SetWindowText(mhMainWnd, windowText.c_str());
//为计算下一组帧数值而重置
frameCnt = 0;
timeElapsed += 1.0f;
}
}
最后我们修改下Run函数。首先在消息循环前调用GameTime的Reset函数,重置计时器,然后进入循环后,计算mDeltaTime(即Tick函数),接下来通过判断游戏是否停止,如果不是停止状态我们才运行游戏,并计算帧率。
int Run()
{
//消息循环
//定义消息结构体
MSG msg = { 0 };
//每次循环开始都要重置计时器
gt.Reset();
//如果GetMessage函数不等于0,说明没有接受到WM_QUIT
while (msg.message != WM_QUIT)
{
//如果有窗口消息就进行处理
if (PeekMessage(&msg, 0, 0, 0, PM_REMOVE))//PeekMessage函数会自动填充msg结构体元素
{
TranslateMessage(&msg); //键盘按键转换,将虚拟键消息转换为字符消息
DispatchMessage(&msg); //把消息分派给相应的窗口过程
}
//否则就执行动画和游戏逻辑
else
{
gt.Tick(); //计算每两帧间隔时间
if (!gt.IsStoped())//如果不是暂停状态,我们才运行游戏
{
CalculateFrameState();
Draw();
}
//如果是暂停状态,则休眠100秒
else
{
Sleep(100);
}
}
}
return (int)msg.wParam;
}
运行结果如下,可以看到fps和mspf随着时间会不断更新,不要质疑帧率,就是那么高的,因为几乎没有渲染计算。背景色由于之后要渲染物体,所以改成了浅蓝。
到这儿,我们的初始化就完成了(其实还要处理一个WM_SIZE消息,但是由于现在并不会因为缺少这个而出问题,绘制几何体的时候才会有问题,所以就放到绘制里再加,这样也比较符合逻辑),之后要重构代码,尽量贴近龙书的代码结构,为之后的学习做准备。