This tutorial will include a lot of code and even more text to consume. The good news is that you'll be able to understand DirectDraw surfaces, loading 16-bit graphics into surfaces, general DirectDraw animation technique and rendering & moving sprites on the screen after you're done reading it.
It will get complicated at some points but we all put a lot of effort into something we really want to learn. And nobody said that programming was easy in the first place. Most tutorials I've seen on the internet don't explain a damn thing in details. I used to get lost a lot trying to figure out why certain things didn't work. That was one of the reasons I wrote these tutorials.
Hopefully, you will get lost in the details of my tutorials. I tried not to miss anything important and let you figure some of the stuff on your own - you can never learn programming by just reading a book on programming or a bunch of tuturials. A lot comes with actual experience and practice practice practice.
A DirectDraw surface is identified by a pointer to the memory location it is stored in. It can be represented by linear memory. Although surfaces can(and should) be stored in video memory you can still store them in the system memory.
Note that when a surface is stored in system memory and further rendered onto the screen the rendering process will take more time, and therefore storing surfaces in the system memory is not efficient.
However, sometimes you will want to create an effect such as blur or water, in that case you will need to do all your byte reads and writes in SYSTEM memory and only then blit the whole surface onto the primary surface which might(and might not be, according to your program) reside in the video memory, but that's not the topic of this tutorial. Anyway, in DirectDraw interface 7, surfaces are declared like this:
LPDIRECTDRAWSURFACE7 surface = NULL;
Another way of comprehending what a DirectDraw surface is, is to imagine a square or a rectangle. The surface's size is determined by multiplying it's width by its height by its color depth. Meet Mr. Smiley...
This is an 8 by 8 pixels surface. Each pixel has it's own color. The color data is usually loaded from graphics files such as Bitmaps(.bmp) or Targas(.tga) or by obvious means you can create your own file format. Before the data is loaded onto a surface you need to create the surface first. Using the IDirectDraw7's CreateSurface method, you can create surfaces. Here's the function's prototype.
HRESULT CreateSurface(LPDDSURFACEDESC2 ddsd,
LPDIRECTDRAWSURFACE *surface
IUnknown *pUnkOuter);
Parameters:
ddsd - is the DDSURFACEDESC2 structure you will fill with information about the surface(width, height, video memory/system memory?) surface - pointer to the surface you're creating. pUnkOuter - doubt it will ever be used, always set it to NULL. The ddsd parameter needs a little more explanation. You will need to declare it globally because you will need it in many places. Here, you fill it in with information about the surface. Specifically you will need to set the surface's width, height and memory flags. This is how you would prepare the mrsmiley's surface before you load any graphics on it presuming you've initialized DirectDraw7 already.
LPDIRECTDRAWSURFACE7 mrsmiley = NULL;
DDSURFACEDESC2 ddsd;
// Clear the ddsd memory
memset(&ddsd, 0, sizeof(DDSURFACEDESC2));
ddsd.dwSize = sizeof(DDSURFACEDESC2);
// Fill in the surface info
ddsd.dwFlags = DDSD_CAPS|DDSD_WIDTH|DDSD_HEIGHT;
ddsd.dwWidth = 8;
ddsd.dwHeight = 8;
ddsd.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN|DDSCAPS_VIDEOMEMORY;
// Create the surface
long rval = lpdd7->CreateSurface(&ddsd, &mrsmiley, NULL);
if(rval != DD_OK)
MessageBox(MessageBox(NULL, "Failed to create surface", "Surface Error", MB_OK));
ddsd.dwFlags indicates that ddsCaps.dwCaps, dwWidth and dwHeight members of ddsd are valid. dwWidth and dwHeight should be set to the width and height of the surface you are creating which is 8 and 8 in mrsmiley's case. ddsCaps.dwCaps takes the surface property flags.
DDSCAPS_OFFSCREENPLAIN - request an offscreen surface(more about types of surfaces later in this tutorial)
DDSCAPS_VIDEOMEMORY - create surface in video memory(to create a surface in system memory use DDSCAPS_SYSTEMMEMORY flag) In the source code I will seclude the above code into a function and name it CreateSurface(int width, int height). It will only take two parameters the height and the width of surface. And the result could be assigned to a surface like this:
LPDIRECTDRAWSURFACE mysurface = CreateSurface(100, 100); In the above example mysurface will be allocated 100 by 100 by "whatever the pixel depth is set to" bytes in video memory. For sake of simplicity this function will always create surfaces in video memory. There are two types of surfaces you can create using the above code - transparent and just plain surfaces. I will modify the CreateSurface function so it will be able to create both transparent and non-transparent surfaces. I'm not going to talk about transparent surfaces in this tuturoial because the following tutorial covers them but to create a transparent surface you would call the same function with a true flag as the third parameter like this:
LPDIRECTDRAWSURFACE mysurface = CreateSurface(100, 100, true); Althought I've mentioned this in one of the previous tutorials I want to remind you about it because this is a perfect place for a small DirectDraw utility function. You will need to use and clear the memory of the DDSURFACEDESC2 structure in many places of your DirectDraw program. So it would be easier if you had a function to do that for you. Here is the function that will clear up the global DDSURFACEDESC2 structure's memory.
// This is assuming ddsd is declared globally
void EraseDesc()
{
memset(&ddsd, 0, sizeof(DDSURFACEDESC2));
ddsd.dwSize = sizeof(DDSURFACEDESC2);
}
After the surface is created you need to load some graphics on it. I'm gonna throw a 16-bit Bitmap file loader function at you in a sec. But before I do so you will need to know about additional global variables listed below. I have to add something here.
The bitmap we're going to load is going to be a 32-bit bitmap. We will then convert it to 16-bit inside the CopyFromBitmap function (that I will mention in just a second). Why not just load straight 16-bit bitmaps you say? The thing is, Photoshop(what I use to edit graphics) saves 16-bit bitmaps in 555 mode, but other graphics editors might not.
I don't want to get into the whole 555/565 conversions in this tutorial to make things easier. The 32-bit format is a lot easier to convert to the 16-bit modes and every graphics program saves them the same way, so to avoid confusion I decided to use the 32-bit format. So just keep in mind that the code always assumes a 32-bit bitmap.
// Global data
int bitmap_width;
int bitmap_height;
unsigned long *bitmap_data = NULL;
LPDIRECTDRAWSURFACE7 extractor = NULL;
bitmap_width - global width of the current bitmap bitmap_height - global height of the current bitmap bitmap_data - used to store the image bits extractor - the surface the bitmap image is copied to.
If you have a number of smaller sprites on the image, it will be more efficient to copy them to surfaces from another surface rather than just the bitmap data. And this is that surface. In other words, the data is first copied to this surface and then you can "extract" sprites from this surface(and not the raw bitmap data).
I would also assume that the surface to surface data transfer would be faster than a bitmap data to surface transfer(I have no other evidence than the fact that surface to surface transfers are done in hardware, so it is reasonable to believe it would be faster).
As I promised, here's the Bitmap loader function. It opens a bitmap and saves the data into the exctractor surface. You will find two more Bitmap utility functions inside this function that I will talk about after this. Just for your convenience those functions are FlipBitmap( ) and CopyFromBitmap( ).
char *LoadBitmap (char *filename)
{
FILE *file;
long offset, width, height, size, bitsize;
short type, bitcount;
if (extractor)
{
extractor->Release();
extractor = NULL;
}
if (bitmap_data)
{
free(bitmap_data);
bitmap_data = NULL;
}
file = fopen(filename, "rb");
if (!file)
{
fclose(file);
return "couldn't open the bitmap file";
}
fread(&type, sizeof(short), 1, file);
// make sure it is a bitmap file
if (type != 0x4D42)
{
fclose(file);
return "bitmap=!0x4d42";
}
// read some useful info
fseek(file, 10, SEEK_SET);
fread(&offset, sizeof(long), 1, file);
fseek(file, 4, SEEK_CUR);
fread(&width, sizeof(long), 1, file);
fread(&height, sizeof(long), 1, file);
fseek(file, 2, SEEK_CUR);
fread(&bitcount, sizeof(short), 1, file);
if (bitcount != 32)
{
fclose(file);
return "bitmap_not32bit";
}
// read the size of data in bytes
fseek(file, 4, SEEK_CUR);
fread(&size, sizeof(long), 1, file);
// skip the rest of header and read the bit data
fseek(file, 16, SEEK_CUR);
size = width * height * 4;
bitmap_data = (unsigned long*)malloc(size);
if (bitmap_data == NULL)
{
fclose(file);
return "bitmap_nullmemory";
}
bitsize = fread(bitmap_data, 1, size, file);
if (bitsize != size || !bitsize)
{
fclose(file);
return "bitmap_badmemory";
}
bitmap_width = width;
bitmap_height = height;
FlipBitmap(width, height);
extractor = CreateSurface(width, height);
CopyFromBitmap(extractor);
fclose(file);
return "bitmap_ok";
}
Now I want to talk about the previously mentioned Bitmap utility functions. First on the list is the FlipBitmap(width, height) function. Here's how it goes.
void FlipBitmap(int width, int height)
{
unsigned long *imgcopy = NULL;
int byte_width = width * 4;
int row = 0;
int bpl = byte_width;
int size = bpl * height;
if(!(imgcopy = (unsigned long*)malloc(size)))
MessageBox(hwnd, "Out of memory", "Memory Error", MB_OK);
// flip
do{ int src_addr = row * width;
int dst_addr = width * height - row * width - width;
memcpy(&imgcopy[dst_addr], &bitmap_data[src_addr], byte_width);
row++;
} while (row < height);
memcpy(bitmap_data, imgcopy, size);
if (imgcopy)
free(imgcopy);
}
Bitmaps are stored upside down in memory. That means that we have to "flip" the data after it is loaded. That's what FlipBitmap does. The second function CopyFromBitmap(LPDIRECTDRAWSURFACE7 surface) copies the data from bitmap to the extractor surface. You need to pass it the surface you want to copy the data to. In this case we want to fill the surface extractor with bitmap data.
void CopyFromBitmap(LPDIRECTDRAWSURFACE7 surface)
{
int size = bitmap_width * bitmap_height;
int widthc = 0;
int offset = 0;
unsigned long *ptrBitmap = (unsigned long*)bitmap_data;
EraseDesc();
surface->Lock(NULL, &ddsd, DDLOCK_WAIT, NULL);
lpitch = (int)ddsd.lPitch >> 1;
lpscreen = (ushort*)ddsd.lpSurface;
int i = 0;
int x, y;
for(y = 0; y < bitmap_height; y++)
{
for(x = 0; x < bitmap_width; x++, i++)
{
// convert from 32 to 16 bit
int r = (ptrBitmap[i] >> 16) & 0xff;
int g = (ptrBitmap[i] >> 8) & 0xff;
int b = ptrBitmap[i] & 0xff;
lpscreen[x + y * lpitch] = _16BIT(r, g, b);
}
}
surface->Unlock(NULL);
}
Before working with the data of any surface you need to "lock" its memory so nothing else can access it. When you're done working with the surface's data you have to unlock it. There are two functions that do that in DirectDraw. They are both members of the surface object.
Lock - You have to pass the DDLOCK_WAIT flag to it so if the surface memory is busy or something else goes wrong the lock function waits until the surface is available. Pass it the ddsd structure which will hold information about the surface when it is locked.
Unlock - Unlocks the surface memory. After the surface is locked, you need to get the surface's lPitch(the real width of the linear surface memory. It can be different on different cards depending on the memory allocated for the "offset" area which is there to prevent writing to wrong memory.
lPitch takes care of this problem) and lpSurface. lpSurface is the pointer to the first byte of the surface data. Afterwards the function goes through the bitmap data, assigning the right rgb pixel values to each byte of the surface and coming up with the final color value which is a ushort.
You have probably noticed that I assign the color variable with the _16BIT macro and I havent exposed it yet but I will after I explain how pixels are written to a surface. You will need a little formula for doing so. Let's take another look at the Mr. Smiley from a little different stand point.
As we all know the surface is really an array of bytes; it is not actually a rectangle, we pack it into a rectangle so we could see the picture on it and otherwise it would be just a straight line. So how do we access that linear memory in a rectangle fasion? On the picture you can see that each pixel is numbered 0 through 63(width*height-1). Let's say you wanted to get location of the pixel with number 30. It is located at x = 6 and y = 3. The formula for locating the right pixel in the ddsd.lpSurface(same as lpscreen in code) array based on x and y position is this:
lpscreen[x+y*width] = color;
This should make sense: go down y rows and add the x position to progress on the current scan line and assign the variable color to that pixel. Replace the x, y and the width value with numbers and you'll see that it actually works.
[6 + 3 * 8] = 30
In the code the width variable is changed to lPitch which is shifted right because lPitch is the real width (width + offset if present) of the surface and it is stored in bytes and we're dealing with 16-bit memory here, where each pixel is identified by 2 bytes, hence the division by 2 (shifting right by 1 is the same as divide by 2).
Note that by shifting lPitch you assume that all widths of surfaces you will be creating will be powers of 2 (e.g. you are not allowed to make a 27 by 35 pixels surface, only powers of two such as 4, 8, 16, 32, 64, ... are allowed). I want to stop here right now and say a few things about the lPitch value and what it actually is. The lPitch value is the actual width of the surface.
But there's a trick. Let's say your surface is 8 by 8 pixels. lPitch will not necessary be 8. lPitch is the width of the surface PLUS an offset that the video card allocates for safety purposes, so you wouldn't overwrite wrong memory. HOWEVER, the video card might not allocate such an offset of special memory and as a result lPitch will be equal to the surface's width; this is very important to note.
Always remember lPitch equals AT LEAST the width of the surface but not necessary exactly the width. Let's take a look at the memory configuration of the actual data in a surface along with lPitch (when one is present, e.g. greater than the surface width). The offset memory is indicated in grey, and those pixels are valid pixels of the surface. When drawing a surface, the offset memory is not shown and is only there to save you from wrong memory writes:
Now that we got lPitch definition out of the way, the general formula for getting a pixel at x and y position on a surface given that ptrData points to the address of the first pixel in the surface and color is a valid color word, is
ptrData[x+y*(lpitch >> 1)] = color
Okay, it's time to talk about the color and how it is generated. And it is generated with one of these macros, depending on your video card.
#define _15BIT(r,g,b) (((r&248)<<7) + ((g&248)<<2) + (b>>3))
#define _16BIT(r,g,b) (((r&248)<<8) + ((g&252)<<3) + (b>>3))
Generally the color is stored in rgb format for red, green and blue. From my own experience 90% of video cards today are made to support the real(565) 16 bit mode. There are still those 10% of cards that support so-called 15 bit(555) format. The differences between these modes are
16 bit format (or 565) is stored using 5 bits for red, 6 bits for green and 5 bits for blue. 15 bit format (or 555) is stored using 5 bits for red, 5 bits for green and 5 bits for blue. My card is a 16 bit card and so my tutorials are based on the 16 bit macro(_16BIT). If after compiling and running the source code the colors are distorted, the truth is - you've got a 15 bit card and to fix that problem you will need to replace the _16BIT macro with the _15BIT one everywhere in the code.
Now that you know how to load Bitmap files and copy them to DirectDraw surfaces I want to talk about creating sprites and loading graphics on them from the extractor surface that's been loaded with bitmap data. I'll write a small sprite structure. It's so straight forward that it doesn't require much explanation. To make things easier you should create a new header file to store this structure. I'll call it sprite.h
// sprite.h
#define MAXFRAMES 16
struct SPRITE
{
int x, y, width, height;
bool transparent;
LPDIRECTDRAWSURFACE7 surface[MAXFRAMES];
};
The transparency flag transparent will indicate whether the sprite has any "transparent" pixels. Transparent pixels will not get rendered to the screen. This makes possible for creating sprites of any shape. The sprite structure also has an array of surfaces which can be used for animation. Generally if you only had one unanimated sprite you would store it in the first surface as in surface[0]. You can come up with your own structure here to support whatever you like. Maybe make only one frame per sprite or something like that, if your game will have a local animation engine.
Time to show you the function that copies a rectangle from the extractor surface and fills the sprite surface with it.
Lets assume that the extractor surface has been loaded with a font bitmap and that you have a SPRITE object named font. Lets just say that we want to load font.surface[0] with the letter R from the extractor surface and that the font.surface[0]'s width and height are both 16 pixels. For this tutorial I wrote a function that does just that and it's called CreateSprite( ).
Here's how you would use it to load the letter R assuming its location is [x=94, y=23] on the extractor surface.
// This function loads data from the extractor surface to a SPRITE object's surface[0]
// member at location 94 23 with width 12 and height 12 with transparency flag set to
// false to indicate that the sprite will not be transparent.
CreateSprite(&font, 0, 12,12, 94, 23, false);
Here's what the function looks like on the inside.
void CreateSprite(SPRITE *sprite, int num, int width, int height,
int x, int y, bool transparent)
{
sprite->width = width;
sprite->height = height;
if(transparent)
sprite->transparent = true;
sprite->surface[num] = CreateSurface(width, height, transparent);
int rectx = x;
int recty = y;
int rectw = x+width;
int recth = y+height;
RECT srcrect = {rectx, recty, rectw, recth};
sprite->surface[num]->Blt(NULL, extractor, &srcrect, DDBLT_WAIT, NULL);
}
This needs quite a bit of explanation. First the SPRITE object's parameters are set, that's self-explanatory. Then you create the sprite's surface and if the transparency flag is present and is true the function CreateSurface creates a transparent surface for the SPRITE object. Otherwise it creates a normal surface with no transparent pixels. After the sprite is created it's time to copy the data onto its surface from the extractor surface that's been loaded with data previously.
DirectDraw provides you with a function that copies data from one surface to another. That function is Blt( ) and it should be called from the surface you're blitting to.
The first parameter is the destination RECT(the area of the surface to blit the data to), it is set to NULL to indicate that the whole SPRITE's surface is used and that the surface's area is in fact the destination RECT.
The second parameter is the surface the data will be taken from, it's the source surface. The LoadBitmap function stores the bitmap data in the extractor surface so the extractor surface would be the source surface.
The third parameter is the source RECT. It is the area of the surface data to be copied from. The fourth parameter is a flag and is set to DDBLT_WAIT. DDBLT_WAIT tells Blt to wait if the blitter is busy and returns as soon as the blit can be set up or another error occurs. The last parameter is expected to be a pointer to the DDBLTFX struct. It will be used for rendering transparent sprites on the screen later and is not used in here.
So far I've explained enough to load a bitmap from disk and store it in a sprite. There is a reason I'm didn't explain how to actually display this sprite on the screen because there are a few more things you should know about DirectDraw and specifically it's double-buffer system and animation. Here's an example of what I covered so far, and after this I'll write something about the DirectDraw double-buffer system.
SPRITE sprite;
LoadBitmap("bitmap.bmp");
CreateSprite(&sprite, 0, 64, 64, 0, 0, false);
That code will open a bitmap(bitmap.bmp) and create a non-transparent sprite out of the data stored in the bitmap at [x=0, y=0] with 64x64 dimension. The bitmap's width and height are expected to be bigger or equal to 64 otherwise the program would exit or crash.
What if the width and height are arbitrary numbers? We would simply change the 64's by bitmap_width and bitmap_height because these values are global(remember?) and are filled with the right stuff inside LoadBitmap(). Now that you know how to load sprites you need to find out how to put them on the screen. Let's see how this is done in DirectDraw.
This is the general description of how animation in DirectDraw works.
Clear the Back Buffer surface - this is done each frame of the animation. You need to clear the Back Buffer to erase the previous frame of animation.
Render sprites to the Back Buffer - first you need to render the graphics on the Back Buffer surface.
Render the Back Buffer to the Primary surface - finally display the contents of the Back Buffer on the screen.
There are two new things you should know about. The Back Buffer surface and the Primary surface. The Back Buffer surface is where you'll render, or "blit" all your graphics to.
When all the graphics are on the Back Buffer you're safe to translate(or "flip") it to the Primary surface. The Primary surface is always shown on the screen at all times in your program. The Back Buffer is "behind" the Primary surface hence the name.
The technique of flipping graphics from Back Buffer to the Primary surface is called double-buffering. It is done to avoid the flicker. If you try to just clear the Primary surface and keep rendering sprites to it you will encounter annoying flickering of the screen.
There is a special function in DirectDraw which flips the Back Buffer surface to the Primary surface. It is called Flip( ) and you will see it in action in a while. A thing to note is that when you Flip the Back Buffer it doesn't become the Primary surface, only the data is copied. DirectDraw does this in hardware so this is pretty fast.
You will need to create the Back Buffer surface and the Primary surface before the program enters the main loop and then you just keep flipping the Back Buffer to the Primary surface inside the loop throughout the program. You'll also have to attach the Back Buffer to the Primary surface to create a so-called flipping structure.
Follow this code below to create both surfaces and attach the Back Buffer to the Primary surface. It should be added to the end of the initialization code of the previous tutorial.
// This chunk of code assumes that the Primary and the Back Buffer surfaces;
// and the surface description object were previously declared as:
//
// DDSURFACEDESC2 ddsd;
// LPDIRECTDRAWSURFACE7 primary = NULL;
// LPDIRECTDRAWSURFACE7 back = NULL;
EraseDesc();
ddsd.dwFlags = DDSD_CAPS|DDSD_BACKBUFFERCOUNT;
ddsd.dwBackBufferCount = 1;
ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE|DDSCAPS_COMPLEX|DDSCAPS_FLIP;
rval = lpdd7->CreateSurface(&ddsd, &primary, NULL);
if(rval != DD_OK)
exit(1); // Falied to create the Primary surface
ddsd.ddsCaps.dwCaps = DDSCAPS_BACKBUFFER;
rval = primary->GetAttachedSurface(&ddsd.ddsCaps, &back);
if(rval != DD_OK)
exit(1); // Failed to create and attach the Back Buffer
Let's see what's going on here. You fill the surface description object(ddsd) with information about the surface. The whole process of creating the Primary surface is the same as if you're creating a normal(offscreen) surface.
First I'd like to say that in DirectDraw there are only 3 types of surfaces you can create. The Back Buffer surface, the Primary surface or an offscreen surface(which is what a normal surface is, Mr.Smile is an example of an offscreen surface).
Right now we're concerned about creating the Primary surface and the Back Buffer surface. Okay, back to the code. Here's a brief description of what the flags you have to fill the ddsd struct with stand for
DDSD_CAPS - Indicates the ddsd.ddsCaps.dwCaps field is valid
DDSD_BACKBUFFERCOUNT - Indicates the ddsd.dwBackBufferCount field is valid
The ddsd.dwBackBufferCount flag is set to 1 because we only have one Back Buffer (you can possibly create 2 Back Buffers and the system would be called triple-buffering, but right now we're not interested in that)
The ddsCaps.dwCaps member of ddsd should be set to following flags
DDSCAPS_PRIMARYSURFACE - Indicates the surface is the Primary surface
DDSCAPS_COMPLEX - Indicates a complex surface is being described. A complex surface results in the creation of more than one surface. Usually used to describe the Primary surface which will have a Back Buffer attached to it
DDSCAPS_FLIP - Indicates that this surface is a part of a surface flipping structure and that you can call the Flip function from it. The primary surface width, height and pixel depth are set to the screen resolution and the pixel depth you provided when you created the DirectDraw interface.
Afterwards you create the Primary surface, reset the ddsCaps.dwCaps member of ddsd to DDSCAPS_BACKBUFFER and create and attach(at the same time) the Back Buffer to the Primary surface by calling the GetAttachedSurface function which takes the ddsd.ddsCaps member and the pointer to the surface which will become the Back Buffer surface. This Back Buffer surface will be created as an identical surface to the Primary surface with the same dimensions as the Primary surface
This is a good time to introduce you to a new DirectDraw utility function that will erase the surface you pass to it with a specific color. Here's ClearSurface( )
void ClearSurface(LPDIRECTDRAWSURFACE7 surface, int r, int g, int b)
{
DDBLTFX fx;
memset(&fx, 0, sizeof(fx));
fx.dwSize = sizeof(fx);
fx.dwFillColor = _16BIT(r, g, b);
surface->Blt(NULL, NULL, NULL, DDBLT_COLORFILL|DDBLT_WAIT, &fx);
}
You already saw the Blt function. Here it requires a pointer to a DDBLTFX struct filled with the color of your choice passed to it. You already know what should be in the place of the 3 NULLs from the previous time I used this function in this tutorial.
The first flag is something new though. Here's what it stands for. The DDBLT_COLORFILL flag tells the Blt function that it should fill the surface with plain color and not graphics. You are already familiar with the DDBLT_WAIT flag.
Let's see the flipping structure in action. I will create a function that will be called from the message loop each frame and call it HostFrame( ). It will take no parameters and will be responsible for drawing stuff on the screen.
void HostFrame()
{
ClearSurface(back, 0, 0, 0);
// Render sprites to the Back Buffer surface here
primary->Flip(back, DDFLIP_WAIT);
}
I'm getting closer to the concluding point of this tutorial. At this point I've explained how to create sprite surfaces, load graphics on them and I went over the double-buffer system for smooth and fast animation.
The one thing is missing though and yes it is actually displaying the sprite on the screen(aka the Primary surface). Once again I'll show you how its done. As you might have figured out by now in DirectDraw rendering from one surface to another doesn't take a rocket scientist.
To display a sprite on the Primary surface all you'll have to do is copy the sprite's surface onto the Back Buffer surface which will be later flipped onto the Primary surface by calling a DirectDraw function named Flip. It's a good idea to write a function that does that. Here's that mighty function that wraps this tutorial up:
void BltSprite(SPRITE *sprite, int num, int x, int y)
{
RECT dstrect = {x, y, x+sprite->width, y+sprite->height};
back->Blt(&dstrect, sprite->surface[num], NULL, DDBLT_WAIT, NULL);
}
Lets see how easy it would be to create a sprite and display it on the screen with all the functions from this tutorial.
// ==============================================================================
// Somewhere after the DirectDraw initialization and before the program loop...
// ==============================================================================
SPRITE monster;
LoadBitmap("monster.bmp");
CreateSprite(&monster, 0, 64, 64, 0, 0, false);
// ================================
// Inside the program loop...
// ================================
ClearSurface(back, 0, 0, 0);
BltSprite(&monster, 0, 100, 100);
primary->Flip(back, DDFLIP_WAIT);
And the monster sprite will be rendered at x=100, y=100 position on the screen. Seems like I covered everything I wanted to cover for this tutorial. But there is still one more thing I didn't say anything about. When you render sprites to the screen and you want to move them around the sprites will at some point of your program will go behind the bounds of the screen.
As soon as a sprite goes over any of the 4 sides of the screen it needs to be clipped because if you don't clip it, the data that goes behind the bounds will be written to a region of memory outside the screen array causing bad things such as program crashing or computer freezing - stuff like that. DirectDraw provides you with a "clipper". You simply pass the bounds of the screen to it and the rest will be taken care of for you.
I wrote a function that does that. This function should be called in the very end of the DirectDraw initialization process after you created and attached the Back Buffer to the Primary surface. By the way the clipper is being attached to the Back Buffer surface because that's the surface you're "blitting" all of your graphics to.
void ClipSurface(LPDIRECTDRAWSURFACE7 surface, int x, int y, int width, int height)
{
LPRGNDATA rd;
RECT cliprect = {x, y, width, height};
rval = lpdd7->CreateClipper(0, &clipper, NULL);
exit(1); // Failed to create clipper
rd = (LPRGNDATA)malloc(sizeof(RGNDATAHEADER) + sizeof(RECT));
memcpy(rd->Buffer, &cliprect, sizeof(RECT));
rd->rdh.dwSize = sizeof(RGNDATAHEADER);
rd->rdh.iType = RDH_RECTANGLES;
rd->rdh.nCount = 1;
rd->rdh.nRgnSize = sizeof(RECT);
rd->rdh.rcBound.left = x;
rd->rdh.rcBound.right = x+width;
rd->rdh.rcBound.top = y;
rd->rdh.rcBound.bottom = y+height;
rval = clipper->SetClipList(rd, 0);
if(rval != DD_OK)
exit(1); // Failed to SetClipList
rval = surface->SetClipper(clipper);
if(rval != DD_OK)
exit(1); // Failed to SetClipper
free(rd);
}
You have to pass the surface that will have the clipper attached to it and the clipper object(declared as LPDIRECTDRAWCLIPPER clipper) is assumed to be global. You know what? I'll leave the contents of this function for you to eat. It's nothing complicated.
On the program's exit you have to release all the DirectDraw surfaces, the clipper, and the DirectDraw object itself. Somewhere in one of the previous tutorials I created a general function that takes care of DirectDraw un-initialization process. The function's name is DirectDrawRelease( ).
Let's revisit it here. In it I'll just release all the memory we've allocated including the SPRITE monster as an example of releasing a sprite's memory.
void DirectDrawRelease()
{
// Release sprites' memory first
// Assuming the previous sprite example
for(int i = 0; i < MAXFRAMES; i++)
{
monster.surface[i].Release();
monster.surface[i] = NULL;
}
// Release memory allocated for DirectDraw
if(clipper)
{
clipper->Release();
clipper = NULL;
}
if(back)
{
back->Release();
back = NULL;
}
if(primary)
{
primary->Release();
primary = NULL;
}
if(lpdd7)
{
lpdd7->Release();
lpdd7 = NULL;
}
}
The source code contains an example of a program that loads up a Bitmap pic and shows it on the screen using techniques described in this tutorial. For your convinience, all instances of the two variables named "primary"(the primary surface) and "back"(the back buffer surface) had been changed to lpPrimary and lpBack in the source code, I just have no time replacing them on this page because this tutorial is huge. Just keep that in mind. The next tutorial explains how transparent sprites are created and displayed.
© 2016 OpenGL Tutorials.
Built with Tornado PHP Framework with template | Web design by Web Design by Greg.