c++ 计时器_DX12初始化篇:添加计时器

今天继续我们的初始化工作。我们将通过在消息循环中的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*)&currentTime);
	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随着时间会不断更新,不要质疑帧率,就是那么高的,因为几乎没有渲染计算。背景色由于之后要渲染物体,所以改成了浅蓝。

ae23f6fbe5b5171f509b1efa9294c690.png

到这儿,我们的初始化就完成了(其实还要处理一个WM_SIZE消息,但是由于现在并不会因为缺少这个而出问题,绘制几何体的时候才会有问题,所以就放到绘制里再加,这样也比较符合逻辑),之后要重构代码,尽量贴近龙书的代码结构,为之后的学习做准备。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值