Starting With the Basics
So far, we’ve looked at our game template code and how it uses DirectDraw to get us into a full-screen mode with our desired resolution and color depth. We then created DirectDrawSurface objects for the primary display (video card), and a backbuffer for our actual drawing.
You know, I really like the overhead projector analogy with transparencies (clear plastic sheets). Our primary surface is the transparency currently on the overhead projector, and our backbuffer is an extra sheet on our desk. We’ll draw whatever we want onto this backbuffer, and quickly trade transparencies so that the backbuffer becomes the active display, and we start drawing all over again on the new backbuffer.
We’re going to start drawing things on surfaces, and we’re going to start from scratch. What that means is that you take the contents of
Game_Main() and clean it out so that it looks like this:
void Game_Main()
{
}
You can go ahead and run this program, and as you’d expect, nothing will be displayed. Thankfully the ESC key press is intercepted by Windows and passed to our template code via
WindowProc() so that the program can terminate properly.
Remember now, this function gets called over and over so long as the program is running. We know that there’s a primary DirectDrawSurface object for the active display and a secondary DirectDrawSurface object that’s our backbuffer. We also know that the plan is to draw onto the backbuffer and flip the two DirectDraw surfaces so that the backbuffer surface becomes the active display surface.
Okay, so let’s paint some pixels!
Pixel Painting
Our first objective is to get our hands on the memory held by the backbuffer DirectDrawSurface object. The online SDK docs tell us that there are two methods for this purpose,
Lock() and
Unlock() . Okay, so let’s take a look at
Lock() :
HRESULT Lock (
LPRECT lpDestRect;
LPDDSURFACEDESC2 lpDDSurfaceDesc;
DWORD dwFlags,
HANDLE hEvent );
You really need to have the online SDK docs open in order to be able to fill out method calls like this correctly – especially when flags are involved. We’ll fill in the parameters together, assuming that you are reading along:
- We can ask for a particular region of surface memory to lock, but we really want the entire thing, so we use NULL (as per the doc directions).
- This method requires a pointer to a valid DDSURFACEDESC2 structure. Lock() will fill in certain fields in this structure for us to use. So, we create a DDSURFACEDESC2 variable on the stack (‘on the stack’ means a local variable) and pass a pointer to it to the Lock() method as our second parameter.
- Taking a look at the possible flags in the docs, we see that DDLOCK_SURFACEMEMORYPTR is a must-have. Later in the tutorial we’ll explore the use of other flags, but for now this is all we need.
- We are told to use NULL here. No problem.
Here’s the finished product:
DDSURFACEDESC2 ddsd;
// Lock the backbuffer surface
ZeroMemory(&ddsd, sizeof(ddsd));
ddsd.dwSize = sizeof(ddsd);
G.lpDDSBack->Lock(NULL, &ddsd,
DDLOCK_SURFACEMEMORYPTR | DDLOCK_WAIT, NULL);
First, I’ve cleared the DDSURFACEDESC2 structure using
ZeroMemory() , which is the same thing as calling
memset() – we’re just setting the entire structure to zeros. Then, we’re placing the size of the structure into the structure itself in the dwSize field. This is important, as many DirectX methods use structures as parameters and require us to supply the size in this field. Although it’s not important to us why, it’s worth mentioning that this is the only way a DirectX method can tell what structure we’re giving it is to provide the structure’s size. If you consider the fact that there’s a DDSURFACEDESC structure and a DDSURFACEDESC2 structure available, you can see why telling the difference might be important.
And then, we call
Lock() with our parameters. All DirectX methods returns values of type HRESULT, but for simplicity’s sake I’m not taking the return values into account for now. Rest assured however that we’ll need to, because so many things can go wrong and if you’re not checking the results of method calls, you get what you deserve in terms of headaches when things don’t work right (or worse yet, crash).
There are two fields inside of the DDSURFACEDESC2 structure that we’re interested in,
lpSurface and
lPitch . With these two fields, we have everything we need to draw onto the backbuffer. Before I get to their use however, I’d like to get the rest of the DirectDraw methods out of the way.
Okay, so assuming we’ve done our drawing, we are then supposed to unlock the backbuffer. This is done with
Unlock() , which looks like this:
HRESULT Unlock (
LPRECT lpRect );
This one’s easy – if you used a RECT in the call to
Lock() , give it here as well. For us, we’ve locked the entire buffer, so we simply use NULL here as well:
G.lpDDSBack->Unlock(NULL);
The last thing we need to do is to actually get the backbuffer onto the active display. The method is called
Flip() , and its prototype looks like this:
HRESULT Flip (
LPDIRECTDRAWSURFACE7 lpDDSurfaceTargetOverride,
DWORD dwFlags );
If you read the paragraph in the SDK docs describing the first parameter, you quickly realize that it’s an advanced feature we don’t need. For us, the default action is to flip the primary and secondary buffer, so we use NULL here. As for the flags, we don’t need any of them, so we’ll use 0.
That’s it for DirectDraw calls! If you were to run the program now, you’d still see nothing (maybe some memory garbage), but the template for pixel manipulation is in place. Our last task in
Game_Main() now is to actually alter the backbuffer memory using the surface pointer provided by
Lock() .
Accessing Surface Memory
How large do you think the area of memory is that we now have access to? Well, what’s involved in figuring it out? Here’s the criteria:
- screen width in pixels
- screen height in pixels
- color depth (in bits)
The logic is as follows: If I have 10 pixels across and 10 pixels down, I therefore have 100 pixels on the display. If each pixel needs 16 bits (= 2 bytes) of memory, then I’d need 100 pixels * 2 bytes = 200 bytes of memory. In our particular case, we’d like to use a 640x480 screen with 16-bit color, which would make our memory needs 614,400 bytes.
For this article, I’m going to assume that your video card can handle 16-bit color. I’m also going to assume that it maps 5 bits for red and blue, and 6 bits for green (the most popular combination). Some video cards prefer to use 5 bits for each color component and use the remaining bit as an alpha component, and there’s a way to check in your code for what type of video card you have, but it’s really outside of the scope of this article.
Now, it’s a fact that pointers to areas of memory can also be used as arrays. As a matter of fact, an array name is equivalent to a pointer. What we’d like to do is take our surface memory pointer and use it like an array, with each array element corresponding to a single pixel. This way, pixels would be accessed like this:
buffer[0] = color; // First pixel (0,0)
buffer[10] = color; // (10,0)
So, the first row of pixels in a 640x480 display would be numbered 0-639. Therefore, the first pixel in the second row would be number 640:
buffer[640] = color; // (0,1)
…and the first pixel in the third row would be 640 + 640 = 1280. Generalizing, we can use the following:
int x, y;
buffer[y * SCREEN_WIDTH + x] = color; // (x,y)
Make sure that the above line makes perfect sense to you. Every time we go SCREEN_WIDTH pixels across, we end up directly underneath ourselves, so we move an additional x pixels over to get to any location on the display.
There are two outstanding issues at this point: the surface memory pointer and the actual color value composition. We’re almost there…
Typecasting the Buffer Pointer
The surface memory pointer given to us by
Lock() is of type LPVOID. In other words, it can’t be used until it is ‘cast’ to a proper data type. Just like with an array, the pointer needs a type so that when we do something like this:
buffer[3] = color;
…the compiler knows how far over in memory the fourth element is (0,1,2,3 = 4th). Is it a byte? Ten bytes? Forty-three? When we declare an array like this:
int Array[20];
…the compiler knows that each element is four bytes wide because that’s how large an
int is. The answer here is to figure out how big a pixel is, and typecast our surface pointer to some data type that matches. Since we’re playing with 16-bit color, any 16-bit unsigned data type will do (there are no negative color values).
Now, I realize that some people would like assistance with all of these data types, so check in 03.03 – Selected C Topics for an overview. I can tell you that either USHORT* or WORD* would work fine, so let’s use WORD. It’s now time to take possession of the surface pointer hidden in that DDSURFACEDESC2 structure we got back from
Lock() :
WORD *pwBuffer;
pwBuffer = (WORD *)ddsd.lpSurface;
That’s it! The (WORD *) is the typecast that changes the pointer from LPVOID (VOID*) to WORD*. Now, every time we access pwBuffer elements, we’ll be moving 16-bits at a time – exactly the size of a pixel in 16-bit color mode.
Using the Memory Pitch
In our illustrations, screen memory (in bytes) is exactly 640x480 multiplied by the number of bytes per pixel. In reality however, there’s a notable difference – the video adapter may reserve some extra memory for itself. If you picture the surface memory as being a rectangle (just like the monitor screen), then picture the video card’s private memory as being a rectangular ‘growth’ coming out of the right-hand side of the monitor. In other words, the number of bytes per horizontal line is equal to the number of pixels per line times the number of bytes per pixel, PLUS the video card’s reserved memory.
This is where the
lPitch field comes in. This value represents the
total number of bytes used by the video card of each horizontal line of memory for the display (which equals the visible display memory plus the reserved memory). Don’t let it bake your brain too much; just read this next line a few times over:
buffer[ y * lPitch + x] = color;
Of course, this assumes that the pitch is in pixels. DirectDraw gives it to us in bytes, so if you’re not using 8-bit color you’d better fix that value like so:
long lPitch;
// Get the pitch for 16-bit pixels
lPitch = ddsd.lPitch / 2;
The reasoning is like this: If there are x bytes in each row, then there are x / 2 WORDs in each row. It’s like in math – if your unit of measurement is metres, make sure you convert any miles and inches before evaluating anything. For us, we’ve decided that a pixel is one WORD wide, so we need our pitch in WORDs, not bytes. Put another way, think: “If the video card uses x bytes per row, how many pixels per row is that?”.
Color Composition
To some extent, I’ve already covered this subject in the Learning Ladder series, but I’ll reiterate some of it here for clarity.
Our pixels are 16-bits wide, and out buffer has been typecast to 16-bit elements. This means that whatever we assign to an array element will be 16 bits wide. Recall that in 5,6,5 16-bit color mode we have 5 bits available for red (intensity (brightness) from 0-31), 6 bits for green (0-63), and 5 bits for blue (0-31). Once you’ve decided on your values for these three, you need to combine them into a 16-bit number. Since I’ve already discussed how this is done, I’ll just show you a macro that’s included in GAMEMAIN.CPP to make life a little easier:
// This converts an R,G,B set to a 16-bit color (565)
#define RGB16(r,g,b) ((r << 11) + (g << 5) + b)
Here’s how you would use it with a buffer:
buffer[0] = RGB16(0, 0, 0); // Black
buffer[1] = RGB16(31, 63, 31); // White
buffer[2] = RGB16(31, 0, 0); // Red
buffer[3] = RGB16(031, 0, 31); // Purple (Red + Blue)
Just think of each color component as a brightness knob that you adjust from 0% to 100%. The color component’s highest allowed value is 100% bright, and 0 is always 0%. It may seem odd that green has an extra bit to it, and therefore can produce a higher maximum value, but don’t forget that 0 is 0, 63 is 100% brightness, and 50% brightness is ~32, just like ~16 is 50% bright for red or blue.
Putting it all Together
Let’s bring out the complete
Game_Main() as it stands:
void Game_Main()
{
DDSURFACEDESC2 ddsd;
WORD *buffer;
long lPitch;
// Prepare a DDSURFACEDESC2 structure for Lock()
ZeroMemory(&ddsd, sizeof(ddsd));
ddsd.dwSize = sizeof(ddsd);
// Lock the backbuffer surface
G.lpDDSBack->Lock(NULL, &ddsd,
DDLOCK_SURFACEMEMORYPTR | DDLOCK_WAIT, NULL);
// Get the surface pointer and the memory pitch (in WORDs)
buffer = (WORD *)ddsd.lpSurface;
lPitch = ddsd.lPitch / 2; // (bytes / 2 = WORDs)
// Draw onto the display
// Fill the entire screen with white pixels
int x, y;
for (y = 0; y < SCREEN_HEIGHT; y++)
{
for (x = 0; x < SCREEN_WIDTH; x++)
{
buffer[y * lPitch + x] = RGB16(31, 63, 31);
}
}
// Unlock the surface
G.lpDDSBack->Unlock(NULL);
// Flip the surfaces
G.lpDDSPrimary->Flip(NULL, 0);
}
Pop that into your GAMEMAIN.CPP, and take a quick look at GLOBALS.H to make sure that you’re in 16-bit color mode (i.e. SCREEN_BITDEPTH is 16). As it says, this function will color every pixel on the display white.
Go Play With Yourself
It’s absolutely vital that you take the time (the more the better) to really play with this function – especially the actual pixel coloring. I can think of dozens of cool effects to try and create right off the top of my head, so there’s plenty that can be done here with a few simple variables and some creative thinking.
Here’s some little tidbits that might come in handy in your adventures:
The
rand() function returns a pseudo-random number between 0 and 32,767. When used with the modulo operator (%), you can get a number in any range. Here’s an example:
buffer[0] = RGB16(rand()%32, rand()%64, rand()%32);
Since % gives you a remainder, something like
rand()%32 will give you a number between 0 and 31, which is the exact range for a 5-bit color component.
You might be interested in 'seeding' the random-number generator, which basically gives you fresh, unique streams of random numbers -- look in your online help for more information.
The
static keyword, when used with a local variable (i.e. a variable declared inside of a function) will retain its value between function calls. A function like this:
void SomeFunction()
{
static int i = 0;
printf(“Number = %d”, i);
i++;
}
…will output increasing values for
i with every call, instead of always printing zeros if the variable wasn’t static. With variables of this type, you can do some interesting things with
Game_Main() .
SCREEN_WIDTH and SCREEN_HEIGHT are your friends. Never use hard-coded values in
Game_Main() . Also, many interesting effects can be generated by using these definitions in your drawing code.
Some Things to Try
If you’re feeling like a challenge, try the following:
- Create solid diagonal lines of varying color on the display
- Create a sequence of boxes around the screen, steadily decreasing in size down to the middle
- Emulate gray-scale “off-air” television noise (think Poltergeist)
- Have single pixels or lines race from the top-left to the bottom right of the display
- Scatter pixels on the display that pulsate in varying colors
We don’t want to get too complicated here, but I’d love to have anyone who comes up with a cool creation using simple variables, loops and math functions to submit it for everyone to see and try.
Questions? Comments? Do you have a wicked demo to show off? Please reply to this topic!
Edited by - teej on May 10, 2001 10:59:32 AM