65.9K
CodeProject 正在变化。 阅读更多。
Home

DirectDraw 和 Surface Blitting 入门

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.80/5 (68投票s)

2002 年 5 月 31 日

18分钟阅读

viewsIcon

550519

downloadIcon

12949

这是一个入门示例,演示如何使用 DirectDraw 库创建一个简单的应用程序,在全屏模式下生成一些动画。运行它需要 DirectX SDK 7 或更高版本。

Sample Image - BasicDD1.jpg

引言

很多人要求我撰写一篇关于 DirectDraw 编程和 Sprite 的入门文章,以便人们能够理解基本概念并开始从示例(MSDN 等)中探索 DirectX 的其他内容。对于所有要求我写这篇入门文章的人,这里就是。

WinMain 和消息循环 - 起点

由于我们正在处理一个 DirectX 应用程序,因此无需在程序中使用 MFC 库。不是说禁止在 DirectX 应用程序中使用 MFC,而是 MFC 包含大量针对桌面应用程序的代码,而不是图形密集型应用程序,所以最好坚持使用纯 Windows API 和 STL。我们将通过在 Visual C++ 界面中选择“Windows 应用程序”选项来启动我们的基本 DirectDraw 程序。在第一个屏幕上,我们将选择“简单 Win32 应用程序”选项,以便 Visual C++ 为我们创建一个 WinMain 函数。向导生成的代码如下所示:

#include "stdafx.h"

int APIENTRY WinMain(HINSTANCE hInstance,
                     HINSTANCE hPrevInstance,
                     LPSTR     lpCmdLine,
                     int       nCmdShow)
{
    // TODO: Place code here.

    return 0;
}

现在我们有了程序的 main 函数,我们需要为程序创建一个主窗口,以便允许 Windows OS 向我们的应用程序发送消息。即使您使用全屏 DirectX 应用程序,仍然需要在后台有一个主窗口,以便您的程序可以接收系统发送给它的消息。我们将把窗口初始化例程放在程序的另一个函数中,该函数将调用 InitWindow

HWND InitWindow(int iCmdShow)
{

   HWND      hWnd;
   WNDCLASS  wc;

   wc.style = CS_HREDRAW | CS_VREDRAW;
   wc.lpfnWndProc = WndProc;
   wc.cbClsExtra = 0;
   wc.cbWndExtra = 0;
   wc.hInstance = g_hInst;
   wc.hIcon = LoadIcon(g_hInst, IDI_APPLICATION);
   wc.hCursor = LoadCursor(NULL, IDC_ARROW);
   wc.hbrBackground = (HBRUSH )GetStockObject(BLACK_BRUSH);
   wc.lpszMenuName = TEXT("");
   wc.lpszClassName = TEXT("Basic DD");
   RegisterClass(&wc);

   hWnd = CreateWindowEx(
      WS_EX_TOPMOST,
      TEXT("Basic DD"),
      TEXT("Basic DD"),
      WS_POPUP,
      0,
      0,
      GetSystemMetrics(SM_CXSCREEN),
      GetSystemMetrics(SM_CYSCREEN),
      NULL,
      NULL,
      g_hInst,
      NULL);

   ShowWindow(hWnd, iCmdShow);
   UpdateWindow(hWnd);
   SetFocus(hWnd);

   return hWnd;

}

此函数首先在 Windows 环境中注册一个窗口类(这是创建窗口所必需的)。在窗口类中,我们需要将有关窗口的一些信息传递给 RegisterClass 函数。所有这些参数都包含在 WNDCLASS 结构中。请注意,在许多地方我使用了变量 g_hInst。该变量将具有全局作用域,并保存我们应用程序的实例句柄。我们将需要另一个全局变量来保存我们主窗口的句柄(我们即将创建)。要创建这些全局变量,只需在 winmain 定义上方声明它们即可,如下所示:

HWND        g_hMainWnd;
HINSTANCE    g_hInst;

别忘了,您需要在程序的最开始填充这些变量的内容,所以在我们的 winmain 函数中,我们将添加以下代码:

    g_hInst = hInstance;
    g_hMainWnd = InitWindow(nCmdShow);

    if(!g_hMainWnd)
        return -1;

请注意,我们将 InitWindow 函数的结果赋值给主窗口全局变量,因为该函数将返回我们新创建窗口的句柄。窗口创建函数中还有一个我们尚未讨论的额外信息,即 lpfnWndProc。在此参数中,我们需要分配一个指向将成为我们主窗口过程的函数的引用。此过程负责接收 Windows 发送给我们的应用程序的消息。系统(而不是您)将在您的应用程序收到任何消息(如按键、绘制消息、鼠标移动等)时调用此函数。这是我们 WndProc 函数的基本定义:

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM 
lParam)
{
   switch (message)
   {
   case WM_DESTROY:
      PostQuitMessage(0);
      return 0;

   } // switch

   return DefWindowProc(hWnd, message, wParam, lParam);
} //

好的,我们的 Windows 应用程序几乎准备好了,我们只差一段重要的代码:消息循环。为了让 Windows 向我们的程序发送消息,我们需要调用一个函数来检查我们的程序是否收到了任何消息。如果我们收到这些消息,我们需要调用一个函数,以便我们的 WndProc 可以处理消息。如果我们没有收到任何系统消息,我们可以利用应用程序的“空闲时间”进行一些后台处理,甚至进行一些 DirectX 操作。这个过程称为空闲处理。我们需要将消息循环插入到全局变量初始化之后。

while( TRUE )
{
    MSG msg;

    if( PeekMessage( &msg, NULL, 0, 0, PM_REMOVE ) )
    {
        // Check for a quit message
        if( msg.message == WM_QUIT )
            break;

        TranslateMessage( &msg );
        DispatchMessage( &msg );
    }
    else
    {
        ProcessIdle();
    }
}

在我们的消息循环中,我们首先检查消息队列中是否有发往我们应用程序的消息。这是通过调用 PeekMessage 函数实现的。如果函数返回 true,我们调用 TranslateMessageDispatchMessage,以便处理我们程序接收到的消息。如果我们没有消息,我们将调用另一个名为 ProcessIdle 的函数。该函数将在我们的程序中创建,我们将用它来更新屏幕的图形。以下是该函数的一个简单定义:

void ProcessIdle()
{

}

好的,我们的基本 Windows 应用程序已准备就绪。如果您编译并运行该应用程序,您将看到一个完全黑色的窗口,它覆盖了您的整个桌面。

初始化 DirectX 和 DirectDraw

现在我们将着手处理应用程序中 DirectDraw 的初始化。在您开始修改代码之前,我需要向您介绍一些概念(表面和页面翻转)。DirectDraw 创建的所有绘图都基于称为表面的结构。表面是包含可用于您应用程序的图形的内存区域。我们需要在屏幕上绘制的所有内容都需要先在表面上创建。假设我们正在创建一个太空侵略者游戏(就像我写的那样)。为此,您可能需要一个图形缓冲区来存储飞船、UFO 和子弹。

所有这些图形都将以这些我们称之为表面的结构存储在内存中。事实上,对于 DirectDraw 应用程序,显示我们在屏幕上看到的内容的区域也被视为表面,称为 FrontBuffer(前缓冲区)。与此 FrontBuffer 表面关联的是另一个称为 BackBuffer(后缓冲区)的表面。该表面存储将在我们应用程序的下一帧显示给用户的信息。假设用户当前在屏幕上的位置 (10,10) 看到一个 UFO,用户的飞船在位置 (100,100)。由于对象在移动,我们需要将 UFO 移动到位置 (12,10),将飞船移动到位置 (102,10)。如果我们直接将这些绘制到前缓冲区,可能会出现某种同步问题(例如,用户可能先看到 UFO 移动,然后是飞船,但它们需要同时移动)。为了解决这个问题,我们将需要显示给用户的所有内容绘制到后缓冲区,然后当绘制完成后,我们将后缓冲区中的所有信息移动到前缓冲区。这个过程称为页面翻转,与创建卡通的过程非常相似(我们使用大量的纸张来制作动画)。

实际上在后台发生的是,DirectDraw 交换了后缓冲区和前缓冲区的指针,这样下次显卡将视频数据发送到显示器时,它使用的是后缓冲区的内容,而不是旧的前缓冲区的内容。当我们执行页面翻转时,后缓冲区的内容变成了之前显示的前缓冲区的内容,而不是您可能认为的绘制的后缓冲区的内容。

现在您对 DirectDraw 的概念有了一些了解,我们将开始编写程序的 DirectX 部分。您需要做的第一件事是在您的主源文件中包含 DirectDraw 的 #include。只需在文件顶部插入以下行:

#include <ddraw.h>

您还需要通知库文件与 DirectDraw 相关。转到“项目”菜单,子菜单“设置”。选择“链接”选项卡,然后在“对象/库模块”中放入以下 lib 文件:

kernel32.lib user32.lib ddraw.lib dxguid.lib gdi32.lib

现在我们将在程序中创建一个新函数。该函数将称为 InitDirectDraw,它将用于启动主 DirectDraw 对象并创建我们将使用的主要表面(前缓冲区和后缓冲区表面)。

int InitDirectDraw()
{
   DDSURFACEDESC2 ddsd;
   DDSCAPS2       ddscaps;
   HRESULT          hRet;

   // Create the main DirectDraw object.
   hRet = DirectDrawCreateEx(NULL, (VOID**)&g_pDD, IID_IDirectDraw7, 
                             NULL);
   if( hRet != DD_OK )
       return -1;

   // Get exclusive mode.
   hRet = g_pDD->SetCooperativeLevel(g_hMainWnd, 
                                     DDSCL_EXCLUSIVE | DDSCL_FULLSCREEN);
   if( hRet != DD_OK )
       return -2;

   // Set the video mode to 640x480x16.
   hRet = g_pDD->SetDisplayMode(640, 480, 16, 0, 0);
   if( hRet != DD_OK )
       return -3;

   // Prepare to create the primary surface by initializing
   // the fields of a DDSURFACEDESC2 structure.
   ZeroMemory(&ddsd, sizeof(ddsd));
   ddsd.dwSize = sizeof(ddsd);
   ddsd.dwFlags = DDSD_CAPS | DDSD_BACKBUFFERCOUNT;
   ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE | DDSCAPS_FLIP |
      DDSCAPS_COMPLEX;
   ddsd.dwBackBufferCount = 1;

   // Create the primary surface.
   hRet = g_pDD->CreateSurface(&ddsd, &g_pDDSFront, NULL);
   if( hRet != DD_OK )
       return -1;

   // Get a pointer to the back buffer.
   ZeroMemory(&ddscaps, sizeof(ddscaps));
   ddscaps.dwCaps = DDSCAPS_BACKBUFFER;
   hRet = g_pDDSFront->GetAttachedSurface(&ddscaps, &g_pDDSBack);
   if( hRet != DD_OK )
       return -1;

   return 0;

} 

请注意,在此函数中,我们使用了一些带有“g_”(全局)前缀的其他变量。由于我们将在所有代码中使用后缓冲区和前缓冲区的引用,因此我们将这两个表面句柄存储在全局变量中。我们作为全局变量存储的另一个变量是主 DirectDraw 对象 (g_pDD)。该对象将用于创建所有 DirectDraw 相关对象。因此,在代码的顶部,添加以下全局变量:

LPDIRECTDRAW7        g_pDD = NULL;        // DirectDraw object
LPDIRECTDRAWSURFACE7 g_pDDSFront = NULL;  // DirectDraw fronbuffer surface
LPDIRECTDRAWSURFACE7 g_pDDSBack = NULL;   // DirectDraw backbuffer surface

现在让我们回到 InitDirectDraw 函数。我们在函数中所做的第一件事是创建 DirectDraw 对象。要创建此对象,我们使用 DirectDrawCreate 函数,该函数定义在 ddraw.h 头文件中。此函数中有两个重要参数,即第二个和第三个。第二个参数传递一个指向我们要存储 DirectDraw 对象变量的变量的引用(在我们的例子中是 g_pDD 变量)。在第三个参数中,我们需要传递我们要获取的 DirectDraw 对象版本。这允许您在使用新 SDK 版本时处理旧版本的 DirectDraw。在我的例子中,我使用的是 DirectX 7 的对象,但使用 DX SDK 8.1。

请注意,我正在测试函数结果是否为 DD_OK,这是所有 DirectDraw 函数的 OK 结果。测试 **每个** DirectDraw 函数调用的返回代码很重要。如果收到的值与 DD_OK 不同,我们将向函数返回一个负值。如果程序在此处出现错误,您可以假定用户可能没有安装正确版本的 DirectX,因此您可以向他显示一条友好的消息(稍后我们将看到这一点)。

第二个函数调用是 SetCooperativeLevel。此函数用于告知 DirectX 我们将如何与显示器协同工作,我们将使用全屏模式还是窗口模式以及其他一些选项。您可以在 DirectX 文档中查看可用选项。我们像第一个函数调用一样测试此函数的结果。

第三个调用的函数是 SetDisplayMode。此函数负责选择我们将与应用程序一起使用的分辨率。在这种情况下,我们创建了一个 640x480 的全屏。第三个参数表示我们使用的颜色深度。这将取决于您想在应用程序中使用多少种颜色。

启动显示后,我们需要创建两个表面,我们将用它们来在屏幕上绘制图形。首先,我们需要初始化前缓冲区(用户正在看到的那个)。当我们想使用 DirectDraw 创建一个表面时,我们需要初始化 DDSURFACEDESC2 结构,该结构包含一些表面创建的参数。重要的是先使用 ZeroMemorymemset 清理结构(否则在某些调用中可能会出现问题)。由于我们正在创建前缓冲区,因此我们需要在 dwflags 参数中填充 DDSD_BACKBUFFERCOUNT 值,以便创建函数识别出我们的前缓冲区将有一个关联的后缓冲区。在 ddsCaps.dwCaps 参数中,我们需要通过 DDSCAPS_PRIMARYSURFACE 参数告知我们正在创建的是前缓冲区表面(或主表面)。由于我们将处理页面翻转表面,因此我们需要填充 DDSCAPS_FLIPDDSCAPS_COMPLEX 参数。

设置好 DDSURFACEDESC2 结构后,我们需要调用 DirectDraw 全局对象的 CreateSurface 函数,并将表面描述结构和将保存 DirectDraw 前缓冲区表面的全局对象作为参数传递。

创建前缓冲区表面后,我们需要获取与该前缓冲区关联的后缓冲区。我们可以通过调用前缓冲区表面的 GetAttachedSurface 来实现。作为参数,我们需要传递一个 DDSCAPS2 结构,以便函数知道我们正在尝试获取后缓冲区。

现在我们的函数已经创建好了,我们需要从 main 函数中调用它。以下是我们的调用方式:

if(InitDirectDraw() < 0)
{
    CleanUp();
    MessageBox(g_hMainWnd, 
               "Could start DirectX engine in your computer." 
               "Make sure you have at least version 7 of "
               "DirectX installed.", 
           "Error", MB_OK | MB_ICONEXCLAMATION);
    return 0;
}

请注意,我们正在测试负返回值。如果我们收到负返回值,我们会告诉用户他可能没有安装正确版本的 DirectX。

这里有一个额外的函数调用,Cleanup 函数。Cleanup 函数将负责删除 DirectX 创建的所有对象。所有对象都通过调用每个实例的 Release 方法来销毁。以下是该函数定义:

void CleanUp()
{
    if(g_pDDSBack)
        g_pDDSBack->Release();

    if(g_pDDSFront)
        g_pDDSFront->Release();

    if(g_pDD)
        g_pDD->Release();
}

在再次编译运行代码之前,将以下代码插入到 WndProc 函数的消息处理 switch 语句中:

   case WM_KEYDOWN:
       if(wParam == VK_ESCAPE)
       {
            PostQuitMessage(0);
            return 0;
       }
       break;

有了这段代码,您就可以通过按下 ESCAPE 键退出应用程序了。现在,编译并运行应用程序,您会注意到您进入了 640x480 全屏模式。

绘制图形

现在我们将在后缓冲区中绘制一些内容,以便我们可以翻转表面并生成动画。我们将使用一张包含赛车图块的位图,这些图块会产生动画。要创建 DirectDraw 中的 Sprite,我们需要将此位图存储在另一个表面(我们将称之为 tile 或 offscreen surface)中,这样我们就可以将该表面 blit(打印)到后缓冲区并生成动画。我们将创建一个名为 cSurface 的类来帮助我们管理我们的 tile 表面。右键单击 Visual C++ 的 ClassView,然后选择“创建新类”选项。作为类类型,选择“Generic Class”,然后将名称用作 cSurface

让我们开始创建我们类的成员变量。主要变量将是 LPDIRECTDRAWSURFACE7 类型,并将保存与我们类关联的 DirectDraw Surface 对象的引用。我们还将存储表面的宽度和高度。我们还将有一个名为 m_ColorKey 的成员,我稍后会解释它。以下是我们成员变量的定义:

protected:
    COLORREF m_ColorKey;
    UINT m_Height;
    UINT m_Width;
    LPDIRECTDRAWSURFACE7 m_pSurface;

我们要插入到类中的第一个函数是 Create 函数。此函数将用于创建我们位图的 DirectX 表面对象。以下是 Create 函数代码:

BOOL cSurface::Create(LPDIRECTDRAW7 hDD, int nWidth, int nHeight, 
                      COLORREF dwColorKey)
{
    DDSURFACEDESC2    ddsd;
    HRESULT        hRet;
    DDCOLORKEY          ddck;


    ZeroMemory( &ddsd, sizeof( ddsd ) );

    ddsd.dwSize = sizeof( ddsd );
    ddsd.dwFlags = DDSD_CAPS | DDSD_WIDTH | DDSD_HEIGHT;
    ddsd.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN | DDSCAPS_VIDEOMEMORY;
    
    ddsd.dwWidth  = nWidth;
    ddsd.dwHeight = nHeight;

    hRet = hDD->CreateSurface(&ddsd, &m_pSurface, NULL );
    if( hRet != DD_OK )
    {
        
    if(hRet == DDERR_OUTOFVIDEOMEMORY)
    {
    ddsd.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN |
                  DDSCAPS_SYSTEMMEMORY;
    
    hRet = hDD->CreateSurface(&ddsd, &m_pSurface, NULL );
    }

    if( hRet != DD_OK )
    {
        return FALSE;
    }
    }


    if((int)dwColorKey != -1)
    {
    ddck.dwColorSpaceLowValue = dwColorKey;
    ddck.dwColorSpaceHighValue = 0;
    m_pSurface->SetColorKey(DDCKEY_SRCBLT, &ddck);
    }

    m_ColorKey = dwColorKey;
    m_Width  = nWidth;
    m_Height = nHeight;

    return TRUE;
}

请注意,用于创建 tile 表面的创建过程与创建前缓冲区表面的创建过程非常相似。不同之处在于 DDSURFACE2 结构中分配的信息。在 dwFlags 参数中,我们指明 dwCapsdwWidthdwHeight 将包含创建表面所需的信息。在 dwCaps 参数中,我们通过 DDSCAPS_OFFSCREENPLAIN 标志告知此表面是一个离屏表面(tile 表面)。我们将此值与 DDSCAPS_VIDEOMEMORY 值结合使用,该值告诉函数我们正在尝试在显存中创建此函数。

在错误检查中,我们检查函数返回值是否为 DDERR_OUTOFVIDEOMEMORY,以便如果用户有一个内存只有几 MB 的旧显卡,我们可以将 DDSURFACEDESC2 参数更改为 DDSCAPS_SYSTEMMEMORY,并尝试在 RAM 中而不是显存中创建表面。从 SYSTEM_MEMORYVIDEO_MEMORY 的表面 Blitting 过程比 VIDEO MEMVIDEO MEM 的过程慢得多,但如果用户内存不足,则需要这样做。

在函数的最后一部分,我们有 dwColoKey 参数测试。如果我们处理的是彩色键表面,则会使用此参数。彩色键表面是我们不希望显示特定颜色的表面。假设我想在一个星空背景上 blit 一艘宇宙飞船。当 blit 飞船时,我不想显示位图的黑色背景,只想显示飞船本身,因此我可以为飞船关联一个颜色键,只显示飞船图片而不是背景。您需要注意创建 tile 位图,并确保不在 Sprite 位图中使用的反锯齿背景(许多应用程序允许您删除反锯齿背景,以便您拥有高质量的 Sprite)。

现在我们将创建另一个函数,用于将位图文件加载到 DirectX 表面对象中。为此,我们将使用一些基本的 GDI 函数。由于我们将只加载一次,这可能不会对绘图过程的性能产生太大影响。以下是 LoadBitmap 函数:

BOOL cSurface::LoadBitmap(HINSTANCE hInst, UINT nRes, int nX, int nY, 
                          int nWidth, int nHeight)
{
    HDC                     hdcImage;
    HDC                     hdc;
    BITMAP                  bm;
    DDSURFACEDESC2          ddsd;
    HRESULT                 hr;

    HBITMAP    hbm;

    hbm = (HBITMAP) LoadImage(hInst, MAKEINTRESOURCE(nRes), 
                              IMAGE_BITMAP, nWidth, nHeight, 0L);

    if (hbm == NULL || m_pSurface == NULL)
        return FALSE;

    // Make sure this surface is restored.
    m_pSurface->Restore();

    // Select bitmap into a memoryDC so we can use it.
    hdcImage = CreateCompatibleDC(NULL);
    if (!hdcImage)
        return FALSE;

    SelectObject(hdcImage, hbm);

    // Get size of the bitmap
    GetObject(hbm, sizeof(bm), &bm);

    if(nWidth == 0)
    nWidth = bm.bmWidth;
    
    if(nHeight == 0)
    nHeight = bm.bmHeight;
    
    // Get size of surface.
    ddsd.dwSize = sizeof(ddsd);
    ddsd.dwFlags = DDSD_HEIGHT | DDSD_WIDTH;
    m_pSurface->GetSurfaceDesc(&ddsd);

    if ((hr = m_pSurface->GetDC(&hdc)) == DD_OK)
    {
        StretchBlt(hdc, 0, 0, ddsd.dwWidth, ddsd.dwHeight, hdcImage, 
                   nX, nY, nWidth, nHeight, SRCCOPY);
        m_pSurface->ReleaseDC(hdc);
    }
    DeleteDC(hdcImage);

    m_srcInfo.m_hInstance = hInst;
    m_srcInfo.m_nResource = nRes;
    m_srcInfo.m_iX          = nX;
    m_srcInfo.m_iY          = nY;
    m_srcInfo.m_iWidth    = nWidth;
    m_srcInfo.m_iHeight      = nHeight;
    
    return TRUE;
}

如果您对 GDI 编程有所了解,这个函数很容易理解,不过我将解释所有代码。我们需要做的第一件事是调用我们 m_Surface 内部变量的 restore 方法。这将恢复 DirectDraw 表面对象分配的内存,以防 DirectDraw 释放内存(如果发生这种情况,任何引用 m_Surface 对象的函数调用都将返回 DERR_SURFACELOST)。恢复内存后,我们创建一个 GDI dc 并从资源加载作为参数传递的位图。然后使用 StretchBlt 函数将位图选择到 DC 并 blit 到表面。请注意,我正在将位图信息保存在 m_srcInfo 结构中。当出现表面丢失问题时,我们将使用此结构,这样我们就可以使用原始数据恢复表面。

我们将在此处介绍的最后一个函数是 Draw 函数,它用于将表面的某个部分绘制到另一个表面上。在大多数情况下,您会将表面绘制到后缓冲区,但您可以使用此 Draw 方法处理任何其他类型的表面。

BOOL cSurface::Draw(LPDIRECTDRAWSURFACE7 lpDest, int iDestX, int iDestY, 
                    int iSrcX, int iSrcY, int nWidth, int nHeight)
{
    RECT    rcRect;
    HRESULT    hRet;

    if(nWidth == 0)
        nWidth = m_Width;

    if(nHeight == 0)
        nHeight = m_Height;

    rcRect.left   = iSrcX;
    rcRect.top    = iSrcY;
    rcRect.right  = nWidth  + iSrcX;
    rcRect.bottom = nHeight + iSrcY;

    while(1)
    {
        if((int)m_ColorKey < 0)
        {
            hRet = lpDest->BltFast(iDestX, iDestY, m_pSurface, 
                                   &rcRect,  DDBLTFAST_NOCOLORKEY);
        }
        else
        {
            hRet = lpDest->BltFast(iDestX, iDestY, m_pSurface, 
                                   &rcRect,  DDBLTFAST_SRCCOLORKEY);
        }

        if(hRet == DD_OK)
            break;

        if(hRet == DDERR_SURFACELOST)
        {
            Restore();
        }
        else
        {
            if(hRet != DDERR_WASSTILLDRAWING)
                return FALSE;
        }
    }

    return TRUE;

这个函数非常简单。我们首先要做的是创建一个 rect 变量,并用我们想要在目标表面上 blit 的源位图位置和大小填充它。之后,我们调用表面的 BltFast 方法将内容 blit 到目标表面。请注意,我们正在进行测试以查看表面是否有颜色键。没有颜色键的表面 Blitting 比有颜色键的表面快得多,所以只在需要时创建颜色键。您可以看到绘图代码位于一个无限循环中。这是因为绘图函数可能会返回一个表面丢失错误。如果返回此错误,我们需要恢复表面并再次尝试 blit,直到表面恢复为止。

另一个重要函数是 Destroy 函数,它负责释放与这些对象相关的 DirectDraw 资源。它基本上是调用 m_Surface 变量的 Release 方法。

void cSurface::Destroy()
{

    if(m_pSurface != NULL)
    {
        m_pSurface->Release();
        m_pSurface = NULL;
    }
}

在源代码中,您会在该类中找到一些其他方法,但基本上,对于本文,您只需要这四个。编译代码以查看是否没有错误。

使用 cSurface 类在后缓冲区中绘制

下一步是创建我们 cSurface 类的一个实例,以便我们可以将此信息 blit 到后缓冲区。为此,我们需要在包含 WinMain 函数的文件中插入一个 include 语句。

#include "csurface.h"

包含我们类的头文件后,创建一个新的全局变量来保存我们的实例。您可以在其他全局变量声明下方创建它。

cSurface    g_surfCar;

在继续编码之后,将位图资源添加到对象中,以便我们可以使用它来 blit 表面到后缓冲区。资源是名为 bmp_bigcar_green.bmp 的位图文件。这个位图在我新的游戏(RaceX)中被使用,该游戏很快将在 CP 上发布。您可以使用名为“IDB_GREENCAR”的资源 ID 来创建位图。

现在我们已经声明了表面类的实例,我们需要调用 create 和 loadbitmap 方法来创建类内部的 DirectX 对象。这段代码可以插入在 InitDirectDraw 调用之后。

g_surfCar.Create(g_pDD, 1500, 280);
g_surfCar.LoadBitmap(g_hInst, IDB_GREENCAR, 0, 0, 1500, 280);

在我们继续之前,请记住,如果您在代码执行期间创建了该对象,则需要销毁它。为此,您需要调用 Destroy 方法。您可以将其放在 CleanUp 函数中。

void CleanUp()
{
    g_surfCar.Destroy();

    if(g_pDDSBack)
        g_pDDSBack->Release();

    if(g_pDDSFront)
        g_pDDSFront->Release();

    if(g_pDD)
        g_pDD->Release();
}

现在我们已经创建、初始化并添加了表面类的销毁代码,我们只需要在 ProcessIdle 函数中绘制图片到后缓冲区并翻转表面。

void ProcessIdle()
{
    HRESULT hRet;

    g_surfCar.Draw(g_pDDSBack, 245, 170, 0, 0, 150, 140);

    while( 1 )
    {
        hRet = g_pDDSFront->Flip(NULL, 0 );
        if( hRet == DD_OK )
        {
            break;
        }
        if( hRet == DDERR_SURFACELOST )
        {
            g_pDDSFront->Restore();
        }
        if( hRet != DDERR_WASSTILLDRAWING )
        {
            break;
        }
    }
}

这段代码将汽车的第一张图片绘制在后缓冲区的中间,并在每次空闲处理调用时将后缓冲区与前一个翻转。让我们稍微更改一下代码,以便我们可以 blit 汽车的动画。

void ProcessIdle()
{
    HRESULT hRet;
    static int iX = 0, iY = 0;
    static iLastBlit;

    if(GetTickCount() - iLastBlit < 50)
    {
        return;
    }

    g_surfCar.Draw(g_pDDSBack, 245, 170, iX, iY, 150, 140);

    while( 1 )
    {
        hRet = g_pDDSFront->Flip(NULL, 0 );
        if( hRet == DD_OK )
        {
            break;
        }
        if( hRet == DDERR_SURFACELOST )
        {
            g_pDDSFront->Restore();
        }
        if( hRet != DDERR_WASSTILLDRAWING )
        {
            break;
        }
    }

    iX += 150;
    if(iX >= 1500)
    {
        iX = 0;
        iY += 140;
        if(iY >= 280)
        {
            iY = 0;
        }
    }

    iLastBlit = GetTickCount();
}

我们创建了 3 个静态变量。前两个将用于更改源位图的 blit 部分的位置。这样,我们就可以通过从第 1 帧到第 20 帧来创建汽车的动画。请注意,我们还有一个额外的变量称为 iLastBlit,它保存 GetTickCount 函数调用的结果。这用于允许每一帧至少在屏幕上停留 50 毫秒,这样动画就会非常流畅。您可以删除这段代码看看会发生什么(在我的机器上,汽车旋转得太快了)。

这是关于如何创建使用 DirectX DirectDraw 库的基本 C++ 程序的简短介绍。如果您有任何问题或意见,请随时发布!

© . All rights reserved.