Direct3D11学习:(四)计时和动画

转载请注明出处:http://www.cnblogs.com/Ray1024

 

一、概述

接触过游戏开发的人都知道,在游戏中,计时器是一个非常重要的工具,用来精确地控制游戏帧数和动画的播放。要正确实现动画效果,我们就必须记录时间,尤其是要精确测量动画帧之间的时间间隔。当帧速率高时,帧之间的时间间隔就会很短;所以,我们需要一个高精度的游戏计时器。

在我们D3D11的学习过程中,会经常用到计时器,因此设计一个具备基本功能的方便使用的计时器类很有必要。我们现在使用一个篇幅来介绍一个简单游戏计时器的实现。

 

二、计时和动画

2.1 系统高精度计时器

我们使用系统高精度计时器来实现时间的精确测量。为了使用用于查询系统高精度计时器的Win32函数,我们必须在代码中添加包含语句“#include<windows.h>”。

高精度计时器采用的时间单位称为计数(count)。我们使用QueryPerformanceCounter函数来获取以计数测量的当前时间值:

1
2
__int64 currTime;
QueryPerformanceCounter((LARGE_INTEGER*)&currTime);

注意,该函数通过它的参数返回当前时间值,该参数是一个64位整数。我们使用QueryPerformanceFrequency函数来获取高精度计时器的频率(每秒的计数次数):

1
2
__int64 countsPerSec;
QueryPerformanceFrequency((LARGE_INTEGER*)&countsPerSec);

而每次计数的时间长度等于频率的倒数(这个值很小,它只是百分之几秒或者千分之几秒):

1
mSecondsPerCount = 1.0 / (double)countsPerSec;

这样,要把一个时间读数valueInCounts转换为秒,我们只需要将它乘以转换因子 mSecondsPerCount:

1
valueInSecs = valueInCounts * mSecondsPerCount;

由QueryPerformanceCounter函数返回的值本身不是非常有用。我们使用QueryPerformanceCounter函数的主要目的是为了获取两次调用之间的时间差——在执行一段代码之前记下当前时间,在该段代码结束之后再获取一次当前时间,然后计算两者之间的差值。也就是,我们总是查看两个时间戳之间的相对差,而不是由系统高精度计时器返回的实际值。下面的代码更好地说明了这一概念:

1
2
3
4
5
__int64 A = 0;
QueryPerformanceCounter((LARGE_INTEGER*)&A);
/* Do work */
__int64 B = 0;
QueryPerformanceCounter((LARGE_INTEGER*)&B);

这样我们就可以知道执行这段代码所要花费的计数时间为(B−A),或者以秒表示的时间为(B−A)*mSecondsPerCount。

 

2.2 游戏计时器类

下面是游戏计时器类的代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class GameTimer
{
public:
    GameTimer();
 
    float TotalTime()const// 单位为秒
    float DeltaTime()const// 单位为秒
 
    void Reset();   // 消息循环前调用
    void Start();   // 取消暂停时调用
    void Stop();    // 暂停时调用
    void Tick();    // 每帧调用
 
private:
    double m_secondsPerCount;
    double m_deltaTime;
 
    __int64 m_baseTime;
    __int64 m_pausedTime;
    __int64 m_stopTime;
    __int64 m_prevTime;
    __int64 m_currTime;
 
    bool m_stopped;
};

后面几节中,我们将讨论游戏计时器的实现。

 

2.3 查询高精度计时器的频率

我们在构造函数中来查询系统高精度计时器的频率。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
GameTimer::GameTimer()
    : m_secondsPerCount(0.0)
    , m_deltaTime(-1.0)
    , m_baseTime(0)
    , m_pausedTime(0)
    , m_prevTime(0)
    , m_currTime(0)
    , m_stopped(false)
{
    __int64 countsPerSec;
    QueryPerformanceFrequency((LARGE_INTEGER*)&countsPerSec);
    m_secondsPerCount = 1.0 / (double)countsPerSec;
}

如上代码中,获取了系统高精度计时器在每秒钟的计时次数countsPerSec,由此,可以得出每两次计时所用的时间(秒)m_secondsPerCount。

 

2.4 帧之间的时间间隔

当渲染动画帧时,我们必须知道帧之间的时间间隔,以使我们根据逝去的时间长度来更新游戏中的物体。我们可以采用以下步骤来计算帧之间的时间间隔:

  时间间隔deltaTime = 当前帧的时间值time1 - 前一帧的时间值time2

对于实时渲染来说,我们至少要达到每秒30帧的频率才能得到比较平滑的动画效果(我们一般可以达到更高的频率);所以时间间隔deltaTime通常是一个非常小的值。

GameTimer::Tick函数中示范了间隔时间deltaTime的计算过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void GameTimer::Tick()
{
    if( m_stopped )
    {
        m_deltaTime = 0.0;
        return;
    }
 
    __int64 currTime;
    QueryPerformanceCounter((LARGE_INTEGER*)&currTime);
    m_currTime = currTime;
 
    // 当前帧和上一帧之间的时间差
    m_deltaTime = (m_currTime - m_prevTime)*m_secondsPerCount;
 
    // 为计算下一帧做准备
    m_prevTime = m_currTime;
 
    // 确保不为负值。DXSDK中的CDXUTTimer提到:如果处理器进入了节电模式
    // 或切换到另一个处理器,m_deltaTime会变为负值。
    if(m_deltaTime < 0.0)
    {
        m_deltaTime = 0.0;
    }
}

Tick函数在应用程序消息循环中调用,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
int D3D11App::Run()
{
    MSG msg = {0};
  
    m_timer.Reset();
 
    while(msg.message != WM_QUIT)
    {
        // 如果接收到Window消息,则处理这些消息
        if(PeekMessage( &msg, 0, 0, 0, PM_REMOVE ))
        {
            TranslateMessage( &msg );
            DispatchMessage( &msg );
        }
        // 否则,则运行动画/游戏
        else
        {  
            m_timer.Tick();
 
            if( !m_appPaused )
            {
                CalculateFrameStats();
                UpdateScene(m_timer.DeltaTime());  
                DrawScene();
            }
            else
            {
                Sleep(100);
            }
        }
    }
 
    return (int)msg.wParam;
}

通过这一方式,每帧都会计算出一个deltaTime并将它传送给UpdateScene方法,根据当前帧与前一帧之间的时间间隔来更新场景。

注意,在游戏刚开始时,对于第一帧来说,没有之前的帧了,也就是说没有前面的时间戳。m_prevTime必须在消息循环开始之前初始化。上面消息循环中调用的Reset函数作用就是将m_prevTime被初始化为当前时间。下面是Reset方法的实现代码:

1
2
3
4
5
6
7
8
9
10
void GameTimer::Reset()
{
    __int64 currTime;
    QueryPerformanceCounter((LARGE_INTEGER*)&currTime);
 
    m_baseTime = currTime;
    m_prevTime = currTime;
    m_stopTime = 0;
    m_stopped  = false;
}

 

2.5 游戏时间

游戏计时器类的成员函数GameTimer::TotalTime()介绍一下。

功能是获取自从调用Reset之后经过的时间总量,其中不包括暂停时间(即从应用程序开始运行时起经过的时间总量)。我们将这一时间称为游戏时间。游戏时间的用途有两种:限时游戏和通过时间函数来驱动动画执行。

 

在这里完整代码代码就不贴出了,有兴趣的朋友可以点击此处下载Demo源码,Demo源码是2_D3DTimingAndAnimation文件。

 

 1 #include "../Common/D3D11App.h"
 2 #pragma comment(lib,"d3d11.lib")
 3 #pragma comment(lib,"d3dx11d.lib")
 4 #pragma comment(lib,"D3DCompiler.lib")
 5 #pragma comment(lib,"Effects11d.lib")
 6 #pragma comment(lib,"dxerr.lib")
 7 #pragma comment(lib,"dxgi.lib")
 8 #pragma comment(lib,"dxguid.lib")
 9 
10 
11 class TestApp : public D3D11App
12 {
13 public:
14     TestApp(HINSTANCE hInstance);
15 
16     void UpdateScene(float dt);
17     void DrawScene(); 
18 };
19 
20 // 程序入口
21 int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE prevInstance,
22     PSTR cmdLine, int showCmd)
23 {
24 #if defined(DEBUG) | defined(_DEBUG)
25     _CrtSetDbgFlag( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF );
26 #endif
27 
28     TestApp theApp(hInstance);
29 
30     if( !theApp.Init() )
31         return 0;
32 
33     return theApp.Run();
34 }
35 
36 //
37 // TestApp Implement
38 //
39 
40 TestApp::TestApp(HINSTANCE hInstance)
41     : D3D11App(hInstance)
42 {
43     m_mainWndCaption = L"2_D3DTimingAndAnimation";
44 }
45 
46 void TestApp::UpdateScene(float dt)
47 {
48 
49 }
50 
51 void TestApp::DrawScene()
52 {
53     assert(m_pD3DImmediateContext);
54     assert(m_pSwapChain);
55 
56     m_pD3DImmediateContext->ClearRenderTargetView(m_pRenderTargetView, reinterpret_cast<const float*>(&Colors::LightSteelBlue));
57     m_pD3DImmediateContext->ClearDepthStencilView(m_pDepthStencilView, D3D11_CLEAR_DEPTH|D3D11_CLEAR_STENCIL, 1.0f, 0);
58 
59     HR(m_pSwapChain->Present(0, 0));
60 }
View Code

注意:

1.Common文件夹拷贝到如图文件夹里

 

 2.项目中添加文件夹并在文件夹中添加如图文件到项目中

 

 

 

 

 

 

三、结语

我们演示了一个游戏计时器GameTimer用于计算应用程序开始后的总时间和两帧之间的时间;在游戏中,你也可以创建额外的实例作为通用的秒表使用。

一个简单高精度计时器的实现就完成了。我们在之后的学习中,就可以直接使用这个计时器类了。

posted @ 2022-04-01 22:35  szmtjs10  阅读(164)  评论(0编辑  收藏  举报