DirectX 2D Tilebased Games Part II

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

Contents:
First off

This time around, I've made the project a little easier to build, the tiles are smaller, and added "sprites" to the scene...

It's funny, because when I tried to find words to describe certain things that go on in my djgpp polygon graphics stuff - if I had used what goes on in 2d scrolling games as an analogy - my writing probably would have been one heck of a lot clearer. (Okay, maybe 2 hecks!)

I think you'll like this demo a bit better than the first one, it even has music (SEE! - I did include winmm.lib in the first project for a reason!) and I've made it so you can change two defines and run this thing in 800x600. (I tested) I didn't try it in 1024x768, because my video card doesn't offer 16-bit color in that mode.

I'm still reading Jered's multiplayer gaming tutorials, and I want to make the program I'm putting together here SCALABLE, as he describes at GPMega, because I have a multiplayer idea for the stuff I'm doing here.

OH -and if you want to make fun of the clouds in this demo, saying they look like marshmallows, just to try and hurt my feelings... well.. there's more to make fun of me for at my website...

That said, let's go:

Coordinate systems

Okay now, the gameworld I'm working with here (which is pretty small) is bigger than the screen. Since I would want anything that moves in the gameworld to have pretty much a free run of the whole gameworld, I'm going to have to deal with their coordinates in WORLD terms.

Let's say the world is defined in pixels, and that the world (in pixels) is 10000x2000 pixels...

Let's also say "Clumpy" <Just imagine some kind of creature here> can run around all over that vast acreage...

Your window into the game world is anything from 320x200 to what I do here (640x480) all the way to whatever.. 16hundredsomething by 12hundredsomething...

Culling and Clipping the sprites

When it comes time to draw our whole scene, "Clumpies" and all, we'll need to find out if we can see "Clumpy" or his friends.

This is easy because we have (px,py) the WORLD coordinate of the lower right hand corner of "portal" through which we can look down into the scene.

Once we've established that Clumpy will be in view, we have to figure out a way to give him (yeah, him) screen coordinates, which in the case of the demo that accompanies all this has values from 0 to 639 horizontally and 0 to 479 vertically.

In the 3d stuff I've written about, I've "talked" about using different sets of coordinates for a polygon object: local coordinates, world coordinates, and camera coordinates.

In 2d tile based scroller games, we're using (usually) world coordinates and camera coordinates. We already know what world coordinates are, (the "real" position of an object in the world) what we need to get before we do any object drawing are the camera coordinates for an object.

Here's the routine to draw the sprites, I'll talk about the clipping portion of it afterwards:

// this routine does clipping, in that it resets the values
// in rcRect, which are used in the call to BltFast...

// Again, not the neatest way

void Draw_Other_Objects(int right, int bottom)
{
    LPDIRECTDRAWSURFACE pdds; // an alias we'll use to the
                              // tile to be used in BltFast!
    RECT rcRect;        // used by Bltfast, to determine what
                        // part of our "tiles/sprites" are drawn
    HRESULT ddrval;
    int actual_screen_x, // these are the actual screen coordinates
    actual_screen_y; // for the upper left hand corner of each sprite
    int left, top;       // the left and top virtual coordinates of the 
                         // viewport
    int index;

    for (index = 0; index<NUMCLOUDS; index++)
    {
         // start of assuming we won't need to clip

         rcRect.left = 0;
         rcRect.top = 0;
         rcRect.right = CLOUD_WIDTH;
         rcRect.bottom = CLOUD_HEIGHT;

         // the right side and bottom of the viewport are
         // sent, use the screenwidth and height to determine
         // the left and top sides..

         left = right - SCREEN_WIDTH;
         top = bottom - SCREEN_HEIGHT;

         // if at least a sliver of the cloud will be in view... then...

         if ((cloud[index].x >= left-CLOUD_WIDTH) &&
             (cloud[index].x < right)  &&
             (cloud[index].y >= top-CLOUD_HEIGHT) &&
             (cloud[index].y < bottom))
         {
             // do we need to clip it to the left side of the viewport?

             if (cloud[index].x < left)
             {
                 actual_screen_x = 0;
                 rcRect.left = left-cloud[index].x;
             }
             else // apparently not...
             {
                 actual_screen_x = cloud[index].x - left;

                 // how about the right side?

                 if (cloud[index].x > (right - CLOUD_WIDTH))
                 {
                     rcRect.right = right - cloud[index].x;
                 }
             }

             // do we need to clip it to the top of the viewport?

             if (cloud[index].y < top)
             {
                 actual_screen_y = 0;
                 rcRect.top = top-cloud[index].y;
             }
             else // nope...
             {
                 actual_screen_y = cloud[index].y - top;

                 // how about the bottom?

                 if (cloud[index].y > (bottom - CLOUD_HEIGHT))
                 {
                     rcRect.bottom = bottom-cloud[index].y;
                 }
             } // end else

             pdds = lpddsenemy;

             ddrval = lpddsback->BltFast( actual_screen_x, 
                                          actual_screen_y, 
                                          pdds, 
                                          &rcRect, 
                                          DDBLTFAST_WAIT | DDBLTFAST_SRCCOLORKEY);

         } // end if not culled
    } // end for cloud
} // end Draw_Other_Objects
A couple of other terms that are commonly heard in reference to both 2d and 3d graphics are CULLING and CLIPPING.

Culling something, whether it be a 3d polybased object, or a 2d sprite, is handling an object that will not be in your viewport so that it isn't handed over to the lower levels of your GRAPHICS PIPELINE. I don't handle it in the routine above as a separate function, working it's way through an object list, but it's the same.

Objects that you won't even see a sliver of on the screen aren't sent to BltFast to be drawn. Although it works fine here, on more complicated projects, you may want to consider setting up some sort of object list, or a couple of object lists, where you can set some sort of VISIBLE flag... it might be easier for you to skip unnecessary processing that way...

The Clipping part of this function is a little bit different than what you may have seen in DOS sprite demos, (though BltFast is a bit like ALLEGRO's blit() function) It's different primarily because you actually clip the sprite before you try to draw it, rather than calling a clipping version of a Sprite_Draw function...

It's clearly the better way to go when you're clipping to a box that's lined up with rows/columns of the screen. It works like this... ignoring the actually bitmap you'll be drawing, you make the rectangle smaller, as needed, so that it's the same size as the visible part of the sprite - AND - covers the right coordinates of the sprite.

Let's say only the bottom of a sprite is going to be visible, which occurs in my demo when the "camera coordinates" of the upper left hand corner of a sprite are a little bit above screen row ZERO....

I'd set the rcRect.top structure element to whatever row of the sprite is the first one that will actually be visible, and set ZERO as the starting Y coordinate. Then BltFast() will blast the right bitmap info to the back buffer.

Oh, about the tiles

What I've just muttered about above is actually EXACTLY the way the tile engine itself works, the upper left hand corners of each tile are spaced the width of each apart horizontally, and the height of each vertically.

Here's a newer version of the routine, hopefully it's a little clearer here...

// this routine lays down all the tiles...

void Render_Scene(int right, int bottom)
{
    LPDIRECTDRAWSURFACE pdds; // an alias we'll use to the
                              // tile to be used in BltFast!
    RECT rcRect;              // used for clipping tiles
    HRESULT             ddrval;
    int start_left, start_top;
    int start_right, start_bottom;
    int start_x_cell, start_y_cell;
    int render_it_x,render_it_y;
    int clip_right, clip_bottom;
    int which_cell,cell_x_save;
    int num_x_tiles, num_y_tiles;
    int x_offset, y_offset;
    int orig_left_clip,orig_top_clip;

    // my muddleheaded setup for the tile engine

    start_left = right-SCREEN_WIDTH;
    start_top = bottom-SCREEN_HEIGHT;

    // find the cell of the "map" to inspect (the map is very close
    // to the top of this source, you could have loaded it from a disk
    // if you wished, later versions of this tile engine will do that

    start_x_cell = start_left / CELL_X_SIZE;
    start_y_cell = start_top / CELL_Y_SIZE;

    // since we're tiling left to right, top to bottom, we only
    // *really* need to keep the orig_left_clip, which is the clipping
    // info for the left side of all the rows of tiles we'll be rendering

    orig_left_clip = rcRect.left = start_left%CELL_X_SIZE;
    orig_top_clip = rcRect.top = start_top%CELL_Y_SIZE;

    // you know something, this might be a completely extraneous step..

    start_right = (start_left+CELL_X_SIZE)%CELL_X_SIZE;
    start_bottom = (start_top+CELL_Y_SIZE)%CELL_Y_SIZE;

    if (start_right<=start_left)
        rcRect.right = CELL_X_SIZE;
    else
        rcRect.right = start_right;

    if (start_bottom<=start_top)
        rcRect.bottom = CELL_Y_SIZE;
    else
        rcRect.bottom = start_bottom;

    // get starting texture to draw with

    cell_x_save = start_x_cell + (start_y_cell<<TILE_Y_NUM_SHIFT);

    // we'll save it for calculating the starting tile for each row,
    // because sometimes we'll be drawing more tiles accross than at other
    // times, because the numbers I have chosen can draw 10 tiles to
    // cover the screen, but if you 
    // move horizontally, you'll need 11

    which_cell = cell_x_save;

    x_offset = 0;
    y_offset = 0;

    num_x_tiles = ((SCREEN_WIDTH+start_left)/ CELL_X_SIZE) + 1;
    num_y_tiles = ((SCREEN_HEIGHT+start_top)/ CELL_Y_SIZE) + 1;

    // loop thru entire scene left to right, top to bottom

    for (render_it_y = 0; render_it_y<num_y_tiles; render_it_y++)
    {
        x_offset = 0;

        // get original start left clip for each x loop

        rcRect.left = orig_left_clip;

        // assume, for now, that we've got the whole tile
        // to work with horizontally

        rcRect.right = CELL_X_SIZE;

        // draw tiles left to right

        for (render_it_x = 0; render_it_x<num_x_tiles; render_it_x++)
        {
            pdds = lpddstextures[terrain_texture[which_cell]];

            ddrval = lpddsback->BltFast( x_offset, y_offset, pdds, &rcRect, DDBLTFAST_WAIT );

            // the following will only evaluate to a value other
            // than CELL_X_SIZE once for every row of tiles

            x_offset+=CELL_X_SIZE-rcRect.left;

            ++which_cell;

            rcRect.left = 0;
            rcRect.right = CELL_X_SIZE;

            // determine where to kick out of the loop
            // or just clip the right hand side of the rect

            clip_right = SCREEN_WIDTH - x_offset;

            if ((clip_right)<0)
                break;

            if ((clip_right)<CELL_X_SIZE)
                if ((clip_right)==0)
                {
                    ++which_cell;
                    break;
                }
                else
                    rcRect.right = clip_right;
        } // end for render_it_x

        // this will only evaluate to a value other than CELL_Y_SIZE
        // once...

        y_offset+=CELL_Y_SIZE-rcRect.top;

        cell_x_save += TILE_X_NUM;
        which_cell = cell_x_save;

        rcRect.top = 0;
        rcRect.bottom = CELL_Y_SIZE;

        // clip tiles w/ respect to Y screen boundary

        clip_bottom = SCREEN_HEIGHT - y_offset;

        if ((clip_bottom)<CELL_Y_SIZE)
            if ((clip_bottom)<=0)
                break;
            else
                rcRect.bottom = SCREEN_HEIGHT-y_offset;

    } // end for render_it_y

} // Render_Scene
As with the first demo, the action is in this demo is in the source to Game Main(), but this time around, it's more of the "Higher Level" action type... Jeez! This may actually approach GOOD CODING STANDARDS in the end ; )

Music

In the first version of this project, I included winmm.lib, this time around - I actually use it for something... what follows are the four short routines I use for music, I don't use all of the mciSendString() commands, because, well... I don't know them all...

In fact, SuperSamat@aol.com told me the string I need to send in mciSendString to start the music FROM THE BEGINNING each time I wanted to spark it up.

Here are the calls I make to mciSendString:

// SHORT MUSIC ROUTINES, THAT USE MCI //////////////////////////////////

void Open_Music(void)
{
    char strCommandString[80];
    MCIERROR ret;

    sprintf(strCommandString,"open PASSPORT.MID type sequencer alias jazz");

    ret = mciSendString(strCommandString, 0, 0, 0);

    if (ret == 0)
    music_enabled = TRUE;
} // end Open_Music

void Start_Or_Restart_Track(void)
{
    MCIERROR ret;

    if (music_enabled)
        ret = mciSendString("play jazz from 0", 0, 0, 0);

    if (ret)
        music_enabled=FALSE;

} // end Start_Or_Restart_Track(void)

void Stop_Music(void)
{
    MCIERROR ret;

    if (music_enabled)
    ret = mciSendString("stop jazz", 0, 0, 0);
} // end Stop_Music

void Close_Music(void)
{
    MCIERROR ret;

    if (music_enabled)
    {
        ret = mciSendString("stop jazz", 0, 0, 0);
        ret = mciSendString("close jazz", 0, 0, 0);
    } // end if music enabled
} // end close music
Running the demo

Aside from the fact you need to have the DirectX 5 runtime files installed on your computer, there really isn't much to say about running this demo...

You're flying around in a ship, but I'm not doing realtime rotation for it yet. (maybe next demo around) I simply made 8 images for the ship, each at 45 degrees from the others.

Although I am currently taking a concentrated Physics course at school, I don't apply any of what I have learned thus far to the program. (I will though, because as we all know - Physics is cool)

This ship should remind people of the AWESOME cardware game called STAR MINES - find it if you can - it's excellent! I don't intend to use this ship for the little "game" I'll be making out of this tile engine, but I figured using it for this demo would be okay.

You'll notice that I don't let you switch between tasks while this program is running... That's because, if you look over the code, I'm not checking for DDERR_SURFACELOST. If I allowed task switching, I wouldn't be able to get away with that...

Allowing the user to switch tasks is a functionality our programs will need, in spite of the fact that games aren't utilitarian - like word processors and stuff. Imagine that someone is online, and decides to tinker with your game, and then they get an instant message or an e-mail or something... when you set that cooperative level, you should set it to "cooperate" with anything else your users may doing in there win95 session.

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:
    tiled.cpp
    ddutil.cpp
    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

Article/demo/source code, Copyright 1998 - by James McCue

The Game Programming MegaSite
The Entire Site ©1996,1997,1998 Matt Reiferson.
Any Questions/Comments, Feel Free To E-Mail Me.