DirectDraw 和 Surface Blitting 入门






4.80/5 (68投票s)
2002 年 5 月 31 日
18分钟阅读

550519

12949
这是一个入门示例,演示如何使用 DirectDraw 库创建一个简单的应用程序,在全屏模式下生成一些动画。运行它需要 DirectX SDK 7 或更高版本。
引言
很多人要求我撰写一篇关于 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,我们调用 TranslateMessage
和 DispatchMessage
,以便处理我们程序接收到的消息。如果我们没有消息,我们将调用另一个名为 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
结构,该结构包含一些表面创建的参数。重要的是先使用 ZeroMemory
或 memset
清理结构(否则在某些调用中可能会出现问题)。由于我们正在创建前缓冲区,因此我们需要在 dwflags
参数中填充 DDSD_BACKBUFFERCOUNT
值,以便创建函数识别出我们的前缓冲区将有一个关联的后缓冲区。在 ddsCaps.dwCaps 参数中,我们需要通过 DDSCAPS_PRIMARYSURFACE
参数告知我们正在创建的是前缓冲区表面(或主表面)。由于我们将处理页面翻转表面,因此我们需要填充 DDSCAPS_FLIP
和 DDSCAPS_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
参数中,我们指明 dwCaps
、dwWidth
和 dwHeight
将包含创建表面所需的信息。在 dwCaps
参数中,我们通过 DDSCAPS_OFFSCREENPLAIN
标志告知此表面是一个离屏表面(tile 表面)。我们将此值与 DDSCAPS_VIDEOMEMORY
值结合使用,该值告诉函数我们正在尝试在显存中创建此函数。
在错误检查中,我们检查函数返回值是否为 DDERR_OUTOFVIDEOMEMORY
,以便如果用户有一个内存只有几 MB 的旧显卡,我们可以将 DDSURFACEDESC2
参数更改为 DDSCAPS_SYSTEMMEMORY
,并尝试在 RAM 中而不是显存中创建表面。从 SYSTEM_MEMORY
到 VIDEO_MEMORY
的表面 Blitting 过程比 VIDEO MEM
到 VIDEO 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++ 程序的简短介绍。如果您有任何问题或意见,请随时发布!