DirectX 2D Tilebased games part III

Author: James McCue
Author's e-mail: aloiterer@juno.com
Author's homepage: Jim the loiterer's PC games, programming, & stuff

Contents:
This one was a pain...

Things were going so smoothly, but I just had to bring over to directx some polygon graphics stuff I've done for DOS...

I figured, that if I texturemapped this space ship thing onto a polygon, I could explain some things I never did in my djgpp 3d stuff - like - for example - basic 2d rotation. (at least look at some code that does it anyhow)

... so I take the plotpixel function I got initially from gpmega, and use it to plot pixels in my triangle drawing functions - and it worked! (well - the colors were all wrong... because of the way I was trying to get the colors)

After trying to figure out how, after loading my spaceship bitmap into an HBITMAP structure - and extracting 8 bits each for red, green, and blue - and then compressing those 24 bits into a 16 bit rgb16, I realized (with a little help) that I could skip all that work. I realized that I could copy the HBITMAP with DDCopyBitmap() to an offscreen directdraw surface, LOCK that surface, read the 16-bit (WORD) rgb values into a WORD buffer SpriteRGB16, UNLOCK the surface -> and I'd be all set! I didn't even need to call GetRGB16(), because the info pulled from the surface was in the correct RGB format already.

At that point, I had my tile engine, I had my clouds - yes, they are clouds and not marshmallows! - and I now had my ship pretty much freely rotating to any angle.

IT WORKED PERFECTLY - and with the ship so small and all - it was still really fast! Thanks to Inedag, I even added a dialog box that allows you to choose resolutions for the demo to run in. Several people checked it out - like my buddy Lava...

Then it crashed

Dhonn (pronounced DEE-HAN <-as in SOLO) ; ) sent me the first in a long series of instant messages™ about this project. He said, "it crashed".

But this is windows programming I thought - if it runs on mine - it runs on yours right? My tiles worked, the only thing new was the texturing (well, everybody made it past the new dialog box) - and I was using clipping versions of my old functions even though I didn't need to at the time.

This thing was exploding on startup for him, and at the time, nobody else. I was getting mad at big D, thinking he found a way to make it crash, and was throwing it in my face (I'm a maniac like that - lest you thought I was a real coder)

Later that night, after working on the program, and the problem, for a few hours, I popped onto ICQ, and hooked up a certain Jered I know, and a certain hpawn and a certain JH, with my program.

Quoth Jered: "This program really didn't like me"

Quoth hpawn: "It set the video mode... and then exited quitely"

Then Dhonn sent me another instant message™, (we'd actually gotten into a fight over this demo's refusal to perform as I asked on his machine)...

"The screen turned blue", "is the text on there white or offwhite?"

I was furious, not only because I wanted to get this article out, with a demo for you to kick around (it's here) by Saturday morning...

- but I intend to use this code I'm posting in a smallish game project - so it HAS to work!

I finally said to dhonn, "can you help me make this more portable?"

Sure

I know I only glossed over how I finally managed to extract the correct 16bit RGB values from my bitmaps, but I'll get to that.

I thought my crashing problem must have something to do with LINEAR PITCH, but whenever I substituted ddsd->lPitch for ddsd->dwWidth in the following BAD routine, then the program didn't work on MY screen: (NOTE: This was modified from the original code I learned from, because I didn't need to shift around r,g, and b values to make my 16-bit colors)
void PlotPixel(int x, int y, WORD pixel, DDSURFACEDESC *ddsd)
{
 WORD *pixels = (WORD *)ddsd->lpSurface;
 DWORD pitch = ddsd->dwWidth; 
 pixels[y*pitch + x] = pixel; 
}
I couldn't for the life of me figure out how to fix this, but I was sure that, when I did, my program would run on all computers that had the DirectX 5 runtime files installed on them.

Here's the modifications Dhonn made to my hacked up pixel plotter code:
void PlotPixel (int x, int y, WORD pixel)
{
	*((WORD *)(double_buffer + y * lpitch + (x<<1))) = pixel;
}
I use a global double_buffer (which is actually declared as an unsigned char pointer believe it or not!) as the pointer to my LOCKED lpddsback buffer, and in an attempt to keep the number of LOCKS and UNLOCKS down per frame, I lock outside of all the lower level drawing routines, in Game_Main()

What's this unsigned char business? If it looks strange to you - it looks doubly strange to me, but it's code that makes the final cut. It's to keep me from doubling the offset all the time or something, it's dhonn's code - ask him please. I was drawing - in spite of the fact I was clipping - outside of my backbuffer's surface area. I felt for sure I had the problem that plagued my program nailed after I implemented these changes!

But the program continued to explode...

Because - there was another more pernicious problem...

I've always tried to look on the bright side of having the slowest computer in town. I figured "hey - if it works decent on mine - it'll work mint on other people's computers".

What was I up against... Dhonn and I tore my code apart, all day, figuring my clipping was the culprit. Other people contributed sources that used DirectDrawClippers, which I could not make heads or tails of.

This problem caused me to tear my code APART, falling back on the old "I'll just throw EVERYTHING at this problem, maybe it'll just die!" - but then I realized - I wasn't checking the return values from lpddsback->Lock() or lpddsback->Unlock!!!

Always, Always, Always check the return values from DDraw methods!!

I'll show you my new game loop later, but the idea is this: My computer couldn't go fast enough to recreate the errors Dhonn and several others were experiencing, and that made it impossible to debug on my own...

How I got my RGB values from bitmaps

Getting the sprite info was a walk on the wild side too... using the straightforward approach to it: Loading a bitmap, copying it to an offscreen surface, and then reading the RGB's into an array of type WORD - worked excellently on my machine. But alas, not on others, or it worked in 640x480 mode but not 800x600, the code was a mess and I was worse.

    sprite_data = (HBITMAP)LoadImage(NULL, szPlayer1, IMAGE_BITMAP, SHIP_WIDTH, SHIP_HEIGHT, LR_LOADFROMFILE);
    smelly = DDCopyBitmap(lpddsplayer, sprite_data, 0, 0,   SHIP_WIDTH, SHIP_HEIGHT);

    if (smelly!=DD_OK)
    {
        MessageBox(main_window_handle,"couldn't load bitmap PLAY1.BMP, please check the directory tiled3.exe is in, if the file isn't there, download tiled3.zip again, if it is, contact aloiterer@juno.com, thank you!","Bitmap loading error",MB_OK);
        PostQuitMessage(0);

    }

    // lock the lpddsplayer surface, and use double_buffer as a word
    // pointer, for ease of access

    memset(&ddsd,0,sizeof(ddsd));
    ddsd.dwSize = sizeof(ddsd);
    lpddsplayer->Lock(NULL,&ddsd,DDLOCK_SURFACEMEMORYPTR,NULL);
    double_buffer = (unsigned char *)ddsd.lpSurface;

    // here's the pitch acquisition

    lpitch = ddsd.lPitch;

    // loop across surface, storing values I'll use to draw with
    // in a WORD buffer called SpriteRGB16

    for (y_index=0; y_indexUnlock(NULL);

    // Don't need this hbitmap anymore

    DeleteObject(sprite_data);
You see that SpriteRGB16 assignment - that's the "real" sprite info, and you see that weird weird stuff on the right hand side.

I needed lPitch for reading as much as I need it for writing, to handle cases where somebodies card in a video mode might not be linear or whatever, and using that unsigned char pointer double_buffer is just something dhonn gave me that works.

Ask him how it works, because I've been up for 36 hours as I write this...

Rotation in software

Although I've been told by hpawn and others that DDraw's Blt() method can handle my rotation needs - if I'm running on cards that have the hardware support for it - I take care of it in software here.

It all begins by defining a polygon, onto which I map 16bit RGB colors, here's the polygon structure:
typedef struct poly_typ
{
	float x,y;
	vertex vertices_local_original[4];
	vertex vertices_local_rotated[4];
} polygon, *polygon_ptr;
I have two (2) sets of "local vertices" because I always like to keep the original, unrotated polygon vertices handy in memory... it's just the way I like to do things.

Note that the x and y variables are floats - there's a reason for this I'll get to down the page - and that unlike normal sprite stuff, where I'd have an x and y for the upper left hand corner of the sprite -> I "handle" my polygons from the center.

What I do when I animate my sprite is rotate the original points to match the heading of my ship, and place those new vertex positions in vertices_local_rotated structure member. Then I map a texture onto it. Here's the routine that rotates the points... it uses a little trigonometry:
void Rotate_Ship(polygon_ptr poly, int angle)
{
	int index;

	float si,cs,
		  rx,ry;

	// there's a little trigonometry going on here
	// your new x coordinate for a vertex is:
	//  oldx * cos(angle in radians) + oldy * sin(angle in radians)
	// and your new y coordinate for a vertex is:
	//  oldy * cos(angle in radians) + oldx * sin(angle in radians)

	si = sin_look[angle];
	cs = cos_look[angle];

	// loop through all 4 polygon vertices

	for (index=0; index<4; index++)
	{
		rx = poly->vertices_local_original[index].x *
			cs - poly->vertices_local_original[index].y * si;

		ry = poly->vertices_local_original[index].y *
			cs + poly->vertices_local_original[index].x * si;

		// put the info into vertices_local_rotated

		poly->vertices_local_rotated[index].x = rx;
		poly->vertices_local_rotated[index].y = ry;
	} // end for index
} // end Rotate_Ship
Having rotated the points, I call my old reliable Draw_Triangle() function, and since I've talked a lot about it in my djgpp stuff, I'll refer you to 3dtut2 and 3dtut3 (it's better in 3) because, well, I'm beat...

Check out the pixel plotting, polygon rotating, and poly texturing code in polydraw.cpp, included w/ the rest of the source and demo in the zip you can get at the bottom of this page.

Dhonn's superfast timer

The timing paradigm embraced by Dhonn Lushine is one that is and has been used in many top-selling games. A couple of notable companies that have used it are id Software and Lucas Arts.

A lot of people are confused by it, including myself at times, but it works with the relationship of apparent motion to the current framerate you're getting on your computer, when you're running a piece of software.

blah right? Well, don't tune out - give me a chance here - this is cool.

Picture it like this, you have a normal game loop, where you're erasing objects, moving them, and redrawing them. Let's say you move an object 1 pixel to the right each time you reach the part of the gameloop where you move it.

Let's say that object is only one pixel high, one pixel wide, and it starts at (0,50) ok?

Moving one pixel per frame, on a machine that's averaging you 10 frames per second, it would take approximately 64 seconds for the pixel to cross the screen. (unless I've done something terribly wrong in that calculation)

On a machine averaging 50 frames per second, the journey takes just about 13 seconds. There's clearly big difference there.

Now let's say you did it like this... take that 13 seconds, and figure out how many pixels PER SECOND have been traveled.

Well, we already know that that's 50 PIXELS PER SECOND. Now if we knew how long it took to make our frames full of graphics, we'd know how much of that 50 pixels we'd need to travel on machines that are getting 20, 30, or 150 frames per second. Here's a function that'll get that for us:
double ftime(void) 
{
    // by dividing by its rate you get accurate seconds

    __int64 endclock;

    QueryPerformanceCounter((LARGE_INTEGER*)&endclock);

    return (double)(endclock-startclock)*rate_inv;

    // note: I recommend that you multiply but the inverse of a constant.
    // (speed reasons)
}
Using something like this, (which has to be initialized first) and finding out that the time it took to draw the last frame was 1/10th of a second, and using that number to figure out that the object you're moving should move 5 pixels, has some interesting consequences.

For one thing, it means that if you had 2 computers, one running your program 80 frames per second, and one running it at 10, that started running it at the same time, would have the same object at the same position 2 seconds in, 5 seconds in, and 12 seconds in...

Another thing, which some folks have not liked about this timing concept, is that on very slow machines - it makes for choppy play.

Since Dhonn often tests his programs on my crappy slow machine, he hasn't lost touch of the fact that there are people (like me) among us with machines he wouldn't even pick up off the curb... So he's offered this solution to the choppy play deal:

A super high-resolution timer.

If the program itself is capable of even fairly decent motion on someone's computer, this routine will program the timer to such high resolutions, that it can "bridge the gap" and help the program achieve good performance on machines that run several frames per second to those that run several hundred frames per second.

BOOL initftime(void) 
{
    __int64 rate;

    // we need the accuracy
    if(!QueryPerformanceFrequency((LARGE_INTEGER*)&rate)) 
	{
        return FALSE; // win errors
    }

    // usually the rate will be 1193180
    if(!rate) 
	{
        return FALSE;
    }

    rate_inv=1.0/(double)rate;

    if(!QueryPerformanceCounter((LARGE_INTEGER*)&startclock)) 
	{
        return FALSE; // win errors
    }

    return TRUE; // there is a clock
}
With this timer, and it's correct usage, you can handle times when the framerate takes a sharp nosedive - or spikes... it's both a handy thing and a way of thinking about timing your games that's very cool.

It beats frame-rate "capping" hands down.

To check out how I used it in my demo, look in Game_Main(), Wind_Moves_all(), and Translate_Other_Objects() function in tiled3.cpp

NOTE: I've made many mistakes along the way with using this, my programs use page flipping - and therefore are capped at the refresh rates of people's monitors, but even so - keep anything involved in moving anything on the screen as a float, only get down to ints when you're ready to plot pixels and such...

My New Game_Main()

It's overkill to put this whole module on display, and the indentation looks completely messed up on the page because I use TAB to do my indenting... (I'm not a 2 or 3 spacer ; )

The main things to notice in the routine are whenever I use somesurface->GetDC(), somesurface->ReleaseDC(), somesurface->Lock(), somesurface->Unlock(), and especially my use of Dhonn's timer, which can be pretty confusing when you've just been briefly introduced to the concept.

I don't simply call those methods - I keep trying until I get those words I so long to hear lately - DD_OK. (alright it's not a word, it's an HRESULT - so what!)

    do 
	{
        // get the time till its not equal to start frame time, it should,
        // never loop at the rate of this timer, but if it does then
        // it will cap to the frame rate to the rate of this timer which
        // is 1193180 hz
	    start_time = ftime();

    } while(start_time==end_time);

	// the total time it took to put together the last frame and
	// get back here...

	frametime = start_time-end_time;
    end_time = start_time;

	// slow-poke rules are in effect... if your machine is running my
	// demo at less than 5 frames per second - aside from telling you
	// that I stink, this will keep my program from becoming unbelievably
	// choppy. ACTUALLY - I should set this value to 0.1 (representing
	// 10 fps)

	if (frametime>(double)0.2)
	    frametime = 0.2;

	// store the framerate for the last frame in the char buffer, which
	// I'll print below

	sprintf(buffer,"last fps rate: %4d ",(int)((double)1/frametime));
Now, we use a couple of Game_Main()'s static floats to compute the changes in position and angle possible - based on the time it took to create the last frame.

        // here is where we use frametime to get the amount of
		// of translation/rotation we can move this frame

// NOTE: you want to keep floating point precision here, for computers that will run this
//   at the high end of possible framerate (pageflipping caps us at the refresh rates
//   of people's monitors)

        movementspeed = ((double)magnitude_per_second*frametime);
        anglespeed = ((double)angles_per_second*frametime);

		// which way is player moving and/or rotating

		if (KEY_DOWN(VK_RIGHT))
		{
			angle -= anglespeed;
			if (angle < 0.0f) 
				angle+=360.0f;

			Rotate_Ship((polygon_ptr)&ship, (int)angle);
		}

		if (KEY_DOWN(VK_LEFT))
		{

			angle += anglespeed;
			if (angle >= 360.0f) 
				angle -= 360.0f;

			Rotate_Ship((polygon_ptr)&ship, (int)angle);
		}

		Translate_Other_Objects();

		Wind_Effects_All();

		// also move ship by our own power

		ship.x += (float)cos(angle*3.14159/180)*movementspeed;
		ship.y += (float)sin(angle*3.14159/180)*movementspeed;
The two routines called at the bottom of that last snippet: Translate_Other_Objects() and Wind_Effects_All() use the value in frametime in a similar way frametime is used to move/turn the ship.

Now it's time to take a look at the code that WAS exploding...

Notice I don't simply do lpddsback->Lock() now, I use a while loop, because you folks with fast video cards may not be able to lock the backbuffer's surface (or any other surface) whenever you get around to that call.

lpddswhatever->GetDC works the same way - it also locks a surface, but it does it internally, as part of the process of getting a Display Context - which is what you need to write text on a surface.

        // FINALLY - draw the polygon ship!
		// LOCK the backbuffer surface so we can 
		// draw directly to it

		memset(&ddsd,0,sizeof(ddsd));
		ddsd.dwSize = sizeof(ddsd);
		
		while((lpddsback->Lock(NULL,&ddsd,DDLOCK_SURFACEMEMORYPTR,NULL)) != DD_OK);

		double_buffer = (unsigned char *)ddsd.lpSurface;

		// here's the pitch acquisition, I think I redundantly get this value
		// in PlotPixel

		lpitch = ddsd.lPitch;
    

		Draw_Ship((polygon_ptr)&ship,sprite_mid_x,sprite_mid_y);

		// don't forget to unlock the surface!

	    while((lpddsback->Unlock(NULL))!=DD_OK);

		// print a message on the backbuffer

		while ((lpddsback->GetDC(&hdc)) != DD_OK);

		SetBkColor( hdc, RGB( 0, 0, 255 ) );
		SetTextColor( hdc, RGB( 255, 255, 0 ) );
		TextOut( hdc, 0, 0, "Keys: LEFT, RIGHT, UP, DOWN, .. and ESC... OH - and M Music (on), N (off).", 74 );
        TextOut( hdc, 0, iScreenHeight-25, buffer, 20 ); 
		
		while ((lpddsback->ReleaseDC(hdc)) != DD_OK);

		// flip back buffer to primary buffer
		lpddsprimary->Flip(NULL,DDFLIP_WAIT);

	// return and let windows have some time - NO WAY, since I'm not checking
	// for "lost" surfaces, I don't let you switch to other applications while
	// this is running, we'll cros that bridge when we come to it...
    } // end if NOT paused

} // end Game_Main
Alt + Tab

Here's code that enables me (in part) to Alt+Tab without crashing this stupid demo:
		case WM_ACTIVATEAPP:
		{
			fActive = (BOOL)wparam;           // activation flag 

			if (fActive)
			{
				if (fActive!=lastwparam)
				{
					Restore_Surfaces();
					Reload_Bitmaps();
					game_is_running = TRUE;
				}
			}
			else
			{
				Stop_Music();
				game_is_running = FALSE;
				paused=TRUE;
            }
			lastwparam = fActive;
		}
		break;
Note that when I return to my ddraw application, I need to restore all the surfaces, and since that only get's us the memory back, I have to reload any bitmaps I need to have loaded into directdraw surfaces again... so it takes a moment getting back is all.

I don't think I'm going to win any awards for this html file you're looking at right here, but I don't know... maybe I'll update this badboy soon.

Running the demo

Running this demo has not been tried on a one meg card yet, and the standard disclaimer applies to everyone regardless of hardware. I'm not responsible for damages that may result due to the use or abuse of the included demo program, or any of it's source code.

However, I will say this, I've test built this project - and it appears to be functioning well. Several people have tested this demo program, more than have ever tested one of my demo programs before I "put it out there".

Building the source code

You'll need the directX 5 SDK to build this project.

Download the zip, I recommend using WinZip to unzip it... unzip it someplace...

Open your VC++ 4, goto File->New, (select a new workspace) make a new workspace in the directory with the unzipped files...

Goto ADD->Files into project, and add these files:
    tiled3.cpp
    polydraw.cpp
    ddutil.cpp
    tiled3.rc (I'm a little iffy about whether you have to make your own dialog resource for this project)
    ddraw.lib (from your directX sdk\lib folder)
    winmm.lib (from your MSDEVSTD\LIB folder)
Save ALL, and the BUILD

Have fun!

Download the demo and source code


LinkExchange
LinkExchange Member



[ Homepage | my Games | Dos C Programming | Win95/DirectX C/C++ | VB Programming | Links ]


Jim the Loiterer's first web page - copyright 1997©,1998© - by James McCue

Get your FREE web pages here...