|
|||||||||||||||||||||
|
|||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
IntroductionLots of people asked me to write an introductory article about DirectDraw programming and Spriting so that people can understand the basic concepts and start discovering the other things about DirectX from samples (MSDN and others available here). For all those that asked me the introductory article, here it is. WinMain and Message Loop - The Starting pointSince we are working with a DirectX application, there is no need to use the MFC library in our program. Not that the use of MFC in a DirectX application is prohibited, but MFC has a lot of code aimed to desktop apps and not graphic intensive ones, so its better to stick on plain Windows API and STL. We will start our basic DirectDraw program by selecting the "Windows Application" option in the Visual C++ interface. At the first screen we will select the option "Simple Win32 Application" to allow Visual C++ to create a #include "stdafx.h" int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { // TODO: Place code here. return 0; } Now that we have the main function of our program, we need to create a main window for the program so that we can allow Windows OS to send messages to our application. Even if you work with a full screen DirectX application you'll still need a main window in the background, so that your program can receive the messages that the system sends to it. We will put the window initialization routine in another function of our program, this function will be called 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; } The first thing that this function does is register a window class in windows environment (this is needed for the window creation process). In the window class we need to pass some information about the window to the HWND g_hMainWnd; HINSTANCE g_hInst; Don't forget that you need to fill the content of this variables at the very begging of your program so, at our g_hInst = hInstance;
g_hMainWnd = InitWindow(nCmdShow);
if(!g_hMainWnd)
return -1;
Notice that we are assigning the result of our 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);
} //
Ok, our windows application is almost set, we are only missing an important code: the message loop. In order to allow windows to send messages to our program, we need to call a function to check if our program has received any messages. If we receive this messages we need to call a function so that our 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(); } } In our message loop, the first thing we do is check the message queue for messages to our application. This is accomplished by calling the void ProcessIdle()
{
}
Ok, our basic windows application is set. If you compile and run the application you will see an entirely black window that covers all your desktop. Initializing DirectX and DirectDrawNow we are going to work on the initialization of the DirectDraw in our application. Before your start to modify the code, I need to present you some concepts (surfaces and page flipping). All the drawing created by DirectDraw are based on structures called surfaces. Surfaces are memory regions that contains graphics that can be used in your application. Everything we need to drawn on the screen needs to be created on a surface first. Let's assume that we are creating a space invaders game ( like the one I wrote). For this you'll probably need a graphic buffer that will hold the space ships, the UFOs, the shots. All this graphics will be stored in memory in this structures that we'll call surfaces. In fact, for DirectDraw applications, the area that displays what we are seeing on the screen is considered a surface too, and it's called the FrontBuffer. Attached to this FrontBuffer surface, we have another surface called the BackBuffer. This surface stores the information of what will be showed to the user in the next frame of our application. Lets say that the user is currently seeing an UFO on the screen at position (10,10) and the user's ship is at position (100,100). Since the objects are moving, we need to move our UFO to the position (12,10) and our ship to position (102, 10). If we draw this to the front buffer directly we can have some kind of synchronization problems (ie. the user can see the UFO move first and them the ship, but they need to move both at the same time). To solve this we draw everything we need to show to the user in the next frame in the backbuffer. When we finish it, we move all the information contained in the backbuffer to the frontbuffer. This process is called page flipping and is very similar to the process of creating cartoons (where we use lots of paper sheets to animated a drawing). What really happens in the background is that DirectDraw changes the pointer of backbuffer with the pointer of frontbuffer, so that next time the video card send the video data to the monitor it uses the backbuffered content and not the old frontbuffer. When we do a page flip, the content of the backbuffer becomes the content of the previously showed frontbuffer, and not the same content of the drawn backbuffer as you might think. Now that you have some idea of the concepts of DirectDraw, we will start coding the DirectX part of the program. The first thing you need to do is insert the #include <ddraw.h> You need to inform the library files related to DirectDraw too. Go to the Project Menu, submenu Settings. Select the link tab and put the following lib files in the "Object/Library Modules" kernel32.lib user32.lib ddraw.lib dxguid.lib gdi32.lib Now we are going to create a new function in our program. This function will be called InitDirectDraw and it will be used to start the main DirectDraw object and create the main surfaces that we are going to use (the front and back buffer surfaces). 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; } Notice that in this function we are using some other variables with the " LPDIRECTDRAW7 g_pDD = NULL; // DirectDraw object LPDIRECTDRAWSURFACE7 g_pDDSFront = NULL; // DirectDraw fronbuffer surface LPDIRECTDRAWSURFACE7 g_pDDSBack = NULL; // DirectDraw backbuffer surface Now lets get back to our Notice that I´m testing the result of the function for The second function call is the The third function called is the After starting the display, we need to create the two surfaces that we will use to draw our graphics on the screen. First we need to initialize the front buffer (the one that the user is seeing). When we want to create a surface with DirectDraw we need to initialize the After setting up the DDSURFACEDESC2 structure we need to call the After creating the frontbuffer surface we need to get the backbuffer associated with this frontbuffer. We can do that by calling the Now that our function is created, we need to call it from the main function. Here is how we are going to call it: 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; } Notice that we are testing for a negative result. If we receive a negative result we tell the user that he probably didn´t installed the correct version of DirectX. We have an extra function call here, the Cleanup function. The Cleanup function will be responsible for deleting all the objects created by DirectX. All the objects are destroyed by calling the void CleanUp() { if(g_pDDSBack) g_pDDSBack->Release(); if(g_pDDSFront) g_pDDSFront->Release(); if(g_pDD) g_pDD->Release(); } Before we compile and run the code again, insert the following code to the case WM_KEYDOWN: if(wParam == VK_ESCAPE) { PostQuitMessage(0); return 0; } break; With this code you'll be able to get out of the application by pressing the ESCAPE key. Now, compile and run the application and notice that you'll enter in the 640x480 fullscreen mode. Blitting GraphicsNow we are going to draw some things in our backbuffer so that we can flip the surfaces and produce some animation. We are going to use a bitmap with some tiles of a race car that produce an animation. To create an sprite in DirectDraw we need to store this bitmap in another surface (that we will call tile or offscreen surface) so that we can blit (print) this surface in the backbuffer and produce the animation. We are going to create a class called cSurface to help us to manage our tile surfaces. Right click in the ClassView of Visual C++ and select the Create New Class option. As class type, select Generic Class and for the name use Let´s start by creating the member variables of our class. The main variable will be of protected:
COLORREF m_ColorKey;
UINT m_Height;
UINT m_Width;
LPDIRECTDRAWSURFACE7 m_pSurface;
The first function we are going to insert in our class is the 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; } Notice that the creation process used to create the tile surface is very similar to the creation process of the front buffer surface. The different is at the information assigned to the DDSURFACE2 structure. At the At the error test we are testing if the return value of the function is At the last portion of the function we have the Now we will create another function to load a bitmap file into the DirectX surface object. For this we are going to use some basic GDI functions. Since we are going to load this just once, this will probably not impact much on the performance of the drawing process. Here is the 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; } This function is very easy to understand if you know a little bit of GDI programming, anyway I'll explain all the code. The first thing we need to do is to call the restore method of our The last function we are going to present here is the Draw function that is used to Draw a portion of the surface in to another surface. In most of the cases you´ll draw the surface in the backbuffer but you can use this Draw method with any other kind of surface. 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; This function is extremely simple. The first thing we do is create a rect variable and fill it with the source bitmap position and size that we want to blit in the destination surface. After that, we call the Another important function is the Destroy function that is responsible for releasing the DirectDraw resources related to this objects. Its basically a call to the Release method of the void cSurface::Destroy() { if(m_pSurface != NULL) { m_pSurface->Release(); m_pSurface = NULL; } } In the source code you´ll find some other methods in this class but basically, for this article, you'll only need this four. Compile the code to see if you have no errors. Drawing in the BackBuffer using the cSurface ClassThe next step is the creation of an instance of our #include "csurface.h" After including the header of our class, create a new global variable that will hold our instance. You can create it below the declaration of the other global variables. cSurface g_surfCar; After proceeding with the coding, add the bitmap resource to the object so that we can use it to blit the surface in the backbuffer. The resource is a bitmap file called bmp_bigcar_green.bmp . This bitmap is used in my new game (RaceX) that will be posted here in CP pretty soon. You can create a resource ID for the bitmap with the " Now that we have the surface class instance declared, we need to call the create and loadbitmap method to create the DirectXobject inside the class. This code can be inserted after the call of g_surfCar.Create(g_pDD, 1500, 280); g_surfCar.LoadBitmap(g_hInst, IDB_GREENCAR, 0, 0, 1500, 280); Before we proced, remember that you need to destroy this object in the case you create it during the code execution. For this you need a call to the Destroy method. You can put this in the void CleanUp() { g_surfCar.Destroy(); if(g_pDDSBack) g_pDDSBack->Release(); if(g_pDDSFront) g_pDDSFront->Release(); if(g_pDD) g_pDD->Release(); } Now that we have created, initialized and added the destruction code of our surface class we just need to draw the picture in the backbuffer and flip the surface in the 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; } } } This code draws the first picture of the car in the middle of the backbuffer and flips the backbuffer with the front one every time we have an idle processing call. Let´s change the code a little bit so that we can blit the animation of the car. 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(); } We create 3 static variables. The first 2 will be used to change the position of the blitted portion of the source bitmap. This way we can create the animation of the car by going from frame 1 to frame 20. Notice that we have an extra variable called This was a brief introduction on how to create a basic C++ program that uses the DirectX DirectDraw library. If you have any questions or comments just post them in! | ||||||||||||||||||||