JavaScript EditorFree JavaScript Editor     Ajax Editor 

Main Page
  Previous Section Next Section

An Example: FreakOut

Before we both lose our minds with all this talk about Windows, DirectX, and 3D graphics, I would like to take a pause and show you a complete game—albeit a simple one, but a game nonetheless. This way you can see a real game loop and some graphics calls, and take a shot at compilation. Sound good? Alrighty, then!

The problem is, we're only on Chapter 1. It's not like I can use stuff from later chapters…that would be cheating, right? So what I've decided to do is get you used to using black box APIs for game programming. Based on that requirement, I asked, "What are the absolute minimum requirements for creating a 2D Breakout-like game?" All we really need is the following functionality:

  • Change into any graphics mode.

  • Draw colored rectangles on the screen.

  • Get the keyboard input.

  • Synchronize the game loop using some timing functions.

  • Draw a string of colored text on the screen.

So I created a library called BLACKBOX.CPP|H. Within it is a DirectX (DirectDraw only) set of functions, along with support code that implements the required functionality. The beauty is, you don't need to look at the code; you just have to use the functions, based on their prototypes, and make sure to link with BLACKBOX.CPP|H to make an .EXE.

Based on the BLACKBOX library, I wrote a game called FreakOut that demonstrates a number of the concepts that we have discussed in this chapter. FreakOut contains all the major components of a real game, including a game loop, scoring, levels, and even a little baby physics model for the ball. And I do mean baby! Figure 1.9 is a screenshot of the game in action. Granted, it's not Arkanoid, but it's not bad for four hours of work!

Figure 1.9. A screenshot of FreakOut.


Before I show you the source code to the game, I want you to take a look at how the project and its various components fit together. Refer to Figure 1.10.

Figure 1.10. The structure of FreakOut.


As you can see from the figure, the game is composed of the following files:

FREAKOUT.CPP— The main game logic that uses BLACKBOX.CPP and creates a minimum Win32 application.

BLACKBOX.CPP— The game library (don't peek).

BLACKBOX.H— The header file for the game library.

DDRAW.LIB— The DirectDraw import library needed to build the application. This doesn't contain the real DirectX code. It's more of an intermediary library that you make calls to, which in turn loads the dynamic link library DDRAW.DLL that does the real work. You can find this in the DirectX SDK installation under <LIB>.

DDRAW.DLL— The runtime DirectDraw library that actually contains the COM implementation of the DirectDraw interface functions that are called through the DDRAW.LIB import library. You don't need to worry about this per se; you just need to make sure that the DirectX runtime files are installed.

So, to compile, you need to include in your project the source files BLACKBOX.CPP and FREAKOUT.CPP, link to the library DDRAW.LIB, and make sure that BLACKBOX.H is in the search path or the working directory where you are compiling so that the compiler can find it.

Now that we have that all straight, let's take a look at the BLACKBOX.H header file and see what the functions are within it.

Listing 1.2 BLACKBOX.H Header File
// BLACKBOX.H - Header file for demo game engine library

// watch for multiple inclusions
#ifndef BLACKBOX
#define BLACKBOX

// DEFINES ////////////////////////////////////////////////////

// default screen size
#define SCREEN_WIDTH    640  // size of screen
#define SCREEN_HEIGHT   480
#define SCREEN_BPP      8    // bits per pixel
#define MAX_COLORS      256  // maximum colors

// MACROS /////////////////////////////////////////////////////

// these read the keyboard asynchronously
#define KEY_DOWN(vk_code) ((GetAsyncKeyState(vk_code) & 0x8000) ? 1 : 0)
#define KEY_UP(vk_code)   ((GetAsyncKeyState(vk_code) & 0x8000) ? 0 : 1)

// initializes a direct draw struct
#define DD_INIT_STRUCT(ddstruct) {memset(&ddstruct,0,sizeof(ddstruct));
ddstruct.dwSize=sizeof(ddstruct); }

// TYPES //////////////////////////////////////////////////////

// basic unsigned types
typedef unsigned short USHORT;
typedef unsigned short WORD;
typedef unsigned char  UCHAR;
typedef unsigned char  BYTE;

// EXTERNALS //////////////////////////////////////////////////

extern LPDIRECTDRAW7         lpdd;             // dd object
extern LPDIRECTDRAWSURFACE7  lpddsprimary;     // dd primary surface
extern LPDIRECTDRAWSURFACE7  lpddsback;        // dd back surface
extern LPDIRECTDRAWPALETTE   lpddpal;          // a pointer dd palette
extern LPDIRECTDRAWCLIPPER   lpddclipper;      // dd clipper
extern PALETTEENTRY          palette[256];     // color palette
extern PALETTEENTRY          save_palette[256]; // used to save palettes
extern DDSURFACEDESC2        ddsd;    // a ddraw surface description struct
extern DDBLTFX               ddbltfx;           // used to fill
extern DDSCAPS2              ddscaps; // a ddraw surface capabilities struct
extern HRESULT               ddrval;            // result back from dd calls
extern DWORD                 start_clock_count; // used for timing
// these defined the general clipping rectangle
extern int min_clip_x,                             // clipping rectangle

// these are overwritten globally by DD_Init()
extern int screen_width,                            // width of screen
           screen_height,                           // height of screen
           screen_bpp;                              // bits per pixel

// PROTOTYPES /////////////////////////////////////////////////

// DirectDraw functions
int DD_Init(int width, int height, int bpp);
int DD_Shutdown(void);
                                      int num_rects, LPRECT clip_list);
int DD_Flip(void);
int DD_Fill_Surface(LPDIRECTDRAWSURFACE7 lpdds,int color);

// general utility functions
DWORD Start_Clock(void);
DWORD Get_Clock(void);
DWORD Wait_Clock(DWORD count);

// graphics functions
int Draw_Rectangle(int x1, int y1, int x2, int y2,
                   int color,LPDIRECTDRAWSURFACE7 lpdds=lpddsback);

// gdi functions
int Draw_Text_GDI(char *text, int x,int y,COLORREF color,
                  LPDIRECTDRAWSURFACE7 lpdds=lpddsback);
int Draw_Text_GDI(char *text, int x,int y,int color,
                  LPDIRECTDRAWSURFACE7 lpdds=lpddsback);


Now, don't waste too much time straining your brain on the code and what all those weird global variables are. Rather, just look at the functions themselves. As you can see, there are functions to do everything that we needed for our little graphics interface. Based on that and a minimum Win32 application (the less Windows programming I have to do, the better), I have created the game FREAKOUT.CPP, which is shown in Listing 1.3. Take a good look at it, especially the main game loop and the calls to the game processing functions.

Listing 1.3 The Source File FREAKOUT.CPP
// INCLUDES ///////////////////////////////////////////////////

#define WIN32_LEAN_AND_MEAN // include all macros
#define INITGUID            // include all GUIDs

#include <windows.h>        // include important windows stuff
#include <windowsx.h>
#include <mmsystem.h>

#include <iostream.h>       // include important C/C++ stuff
#include <conio.h>
#include <stdlib.h>
#include <malloc.h>
#include <memory.h>
#include <string.h>
#include <stdarg.h>
#include <stdio.h>
#include <math.h>
#include <io.h>
#include <fcntl.h>

#include <ddraw.h>          // directX includes
#include "blackbox.h"       // game library includes

// DEFINES ////////////////////////////////////////////////////

// defines for windows
#define WINDOW_CLASS_NAME "WIN3DCLASS"  // class name

#define WINDOW_WIDTH            640     // size of window
#define WINDOW_HEIGHT           480

// states for game loop
#define GAME_STATE_INIT         0
#define GAME_STATE_RUN          2
#define GAME_STATE_EXIT         4

// block defines
#define NUM_BLOCK_ROWS          6
#define NUM_BLOCK_COLUMNS       8

#define BLOCK_WIDTH             64
#define BLOCK_HEIGHT            16
#define BLOCK_ORIGIN_X          8
#define BLOCK_ORIGIN_Y          8
#define BLOCK_X_GAP             80
#define BLOCK_Y_GAP             32

// paddle defines
#define PADDLE_START_X          (SCREEN_WIDTH/2 - 16)
#define PADDLE_START_Y          (SCREEN_HEIGHT - 32);
#define PADDLE_WIDTH            32
#define PADDLE_HEIGHT           8
#define PADDLE_COLOR            191

// ball defines
#define BALL_START_Y            (SCREEN_HEIGHT/2)
#define BALL_SIZE                4

// PROTOTYPES /////////////////////////////////////////////////

// game console
int Game_Init(void *parms=NULL);
int Game_Shutdown(void *parms=NULL);
int Game_Main(void *parms=NULL);

// GLOBALS ////////////////////////////////////////////////////

HWND main_window_handle  = NULL; // save the window handle
HINSTANCE main_instance  = NULL; // save the instance
int game_state           = GAME_STATE_INIT; // starting state

int paddle_x = 0, paddle_y = 0; // tracks position of paddle
int ball_x   = 0, ball_y   = 0; // tracks position of ball
int ball_dx  = 0, ball_dy  = 0; // velocity of ball
int score    = 0;               // the score
int level    = 1;               // the current level
int blocks_hit = 0;             // tracks number of blocks hit

// this contains the game grid data


// FUNCTIONS //////////////////////////////////////////////////

                      UINT msg,
                            WPARAM wparam,
                            LPARAM lparam)
// this is the main message handler of the system
PAINTSTRUCT    ps;           // used in WM_PAINT
HDC            hdc;       // handle to a device context

// what is the message
    case WM_CREATE:
    // do initialization stuff here
    }  break;

    case WM_PAINT:
         // start painting
         hdc = BeginPaint(hwnd,&ps);

         // the window is now validated

         // end painting
        }  break;

    case WM_DESTROY:
           // kill the application
           }  break;


    }  // end switch

// process any messages that we didn't take care of
return (DefWindowProc(hwnd, msg, wparam, lparam));

}  // end WinProc

// WINMAIN ////////////////////////////////////////////////////

int WINAPI WinMain(HINSTANCE hinstance,
            HINSTANCE hprevinstance,
            LPSTR lpcmdline,
            int ncmdshow)
// this is the winmain function

WNDCLASS winclass;  // this will hold the class we create
HWND     hwnd;         // generic window handle
MSG     msg;         // generic message
HDC      hdc;       // generic dc
PAINTSTRUCT ps;     // generic paintstruct
// first fill in the window class structure    = CS_DBLCLKS | CS_OWNDC |
                 CS_HREDRAW | CS_VREDRAW;
winclass.lpfnWndProc = WindowProc;
winclass.cbClsExtra        = 0;
winclass.cbWndExtra        = 0;
winclass.hInstance        = hinstance;
winclass.hIcon            = LoadIcon(NULL, IDI_APPLICATION);
winclass.hCursor        = LoadCursor(NULL, IDC_ARROW);
winclass.hbrBackground    = (HBRUSH)GetStockObject(BLACK_BRUSH);
winclass.lpszMenuName    = NULL;
winclass.lpszClassName    = WINDOW_CLASS_NAME;

// register the window class
if (!RegisterClass(&winclass))

// create the window, note the use of WS_POPUP
if (!(hwnd = CreateWindow(WINDOW_CLASS_NAME,    // class
        "WIN3D Game Console",    // title
        0,0,                    // initial x,y
        GetSystemMetrics(SM_CXSCREEN),  // initial width
        GetSystemMetrics(SM_CYSCREEN),  // initial height
        NULL,        // handle to parent
        NULL,        // handle to menu
        hinstance,   // instance
        NULL)))      // creation parms

// hide mouse

// save the window handle and instance in a global
main_window_handle = hwnd;
main_instance      = hinstance;

// perform all game console specific initialization

// enter main event loop
    if (PeekMessage(&msg,NULL,0,0,PM_REMOVE))
    // test if this is a quit
        if (msg.message == WM_QUIT)

    // translate any accelerator keys

    // send the message to the window proc
    } // end if
       // main game processing goes here

    }  // end while

// shutdown game and release all resources

// show mouse

// return to Windows like this

}  // end WinMain


int Game_Init(void *parms)
// this function is where you do all the initialization
// for your game

// return success

}  // end Game_Init


int Game_Shutdown(void *parms)
// this function is where you shutdown your game and
// release all resources that you allocated

// return success

}  // end Game_Shutdown


void Init_Blocks(void)
// initialize the block field
for (int row=0; row < NUM_BLOCK_ROWS; row++)
    for (int col=0; col < NUM_BLOCK_COLUMNS; col++)
         blocks[row][col] = row*16+col*3+16;

}  // end Init_Blocks

void Draw_Blocks(void)
// this function draws all the blocks in row major form
int x1 = BLOCK_ORIGIN_X, // used to track current position
    y1 = BLOCK_ORIGIN_Y;

// draw all the blocks
for (int row=0; row < NUM_BLOCK_ROWS; row++)
    // reset column position
    x1 = BLOCK_ORIGIN_X;

    // draw this row of blocks
    for (int col=0; col < NUM_BLOCK_COLUMNS; col++)
        // draw next block (if there is one)
        if (blocks[row][col]!=0)
            // draw block

            }  // end if

        // advance column position
        }  // end for col

    // advance to next row position

    }  // end for row

}  // end Draw_Blocks


void Process_Ball(void)
// this function tests if the ball has hit a block or the paddle
// if so, the ball is bounced and the block is removed from
// the playfield note: very cheesy collision algorithm :)

// first test for ball block collisions

// the algorithm basically tests the ball against each
// block's bounding box this is inefficient, but easy to
// implement, later we'll see a better way

int x1 = BLOCK_ORIGIN_X, // current rendering position
    y1 = BLOCK_ORIGIN_Y;

int ball_cx = ball_x+(BALL_SIZE/2),  // computer center of ball
    ball_cy = ball_y+(BALL_SIZE/2);

// test of the ball has hit the paddle
if (ball_y > (SCREEN_HEIGHT/2) && ball_dy > 0)
   // extract leading edge of ball
   int x = ball_x+(BALL_SIZE/2);
   int y = ball_y+(BALL_SIZE/2);

   // test for collision with paddle
   if ((x >= paddle_x && x <= paddle_x+PADDLE_WIDTH) &&
       (y >= paddle_y && y <= paddle_y+PADDLE_HEIGHT))
       // reflect ball

       // push ball out of paddle since it made contact

       // add a little english to ball based on motion of paddle
       if (KEY_DOWN(VK_RIGHT))
       if (KEY_DOWN(VK_LEFT))

       // test if there are no blocks, if so send a message
       // to game loop to start another level
       if (blocks_hit >= (NUM_BLOCK_ROWS*NUM_BLOCK_COLUMNS))
          game_state = GAME_STATE_START_LEVEL;
          }  // end if

       // make a little noise

       // return

       }  // end if

   }  // end if
// now scan thru all the blocks and see if ball hit blocks
for (int row=0; row < NUM_BLOCK_ROWS; row++)
    // reset column position
    x1 = BLOCK_ORIGIN_X;

    // scan this row of blocks
    for (int col=0; col < NUM_BLOCK_COLUMNS; col++)
        // if there is a block here then test it against ball
        if (blocks[row][col]!=0)
           // test ball against bounding box of block
           if ((ball_cx > x1) && (ball_cx < x1+BLOCK_WIDTH) &&
               (ball_cy > y1) && (ball_cy < y1+BLOCK_HEIGHT))
               // remove the block
               blocks[row][col] = 0;

               // increment global block counter, so we know
               // when to start another level up

               // bounce the ball

               // add a little english

               // make a little noise

               // add some points

               // that's it -- no more block

               }  // end if

           }  // end if

        // advance column position
        }  // end for col

    // advance to next row position

    }  // end for row
}  // end Process_Ball


int Game_Main(void *parms)
// this is the workhorse of your game it will be called
// continuously in real-time this is like main() in C
// all the calls for your game go here!

char buffer[80]; // used to print text

// what state is the game in?
if (game_state == GAME_STATE_INIT)
    // initialize everything here graphics

    // seed the random number generator
    // so game is different each play

    // set the paddle position here to the middle bottom
    paddle_x = PADDLE_START_X;
    paddle_y = PADDLE_START_Y;

    // set ball position and velocity
    ball_x = 8+rand()%(SCREEN_WIDTH-16);
    ball_y = BALL_START_Y;
    ball_dx = -4 + rand()%(8+1);
    ball_dy = 6 + rand()%2;

    // transition to start level state
    game_state = GAME_STATE_START_LEVEL;

    }  // end if
if (game_state == GAME_STATE_START_LEVEL)
    // get a new level ready to run

    // initialize the blocks

    // reset block counter
    blocks_hit = 0;

    // transition to run state
    game_state = GAME_STATE_RUN;
    }  // end if
if (game_state == GAME_STATE_RUN)
    // start the timing clock

    // clear drawing surface for the next frame of animation
    Draw_Rectangle(0,0,SCREEN_WIDTH-1, SCREEN_HEIGHT-1,200);

    // move the paddle
       // move paddle to right

       // make sure paddle doesn't go off screen
       if (paddle_x > (SCREEN_WIDTH-PADDLE_WIDTH))
          paddle_x = SCREEN_WIDTH-PADDLE_WIDTH;

       }  // end if
    if (KEY_DOWN(VK_LEFT))
       // move paddle to right

       // make sure paddle doesn't go off screen
       if (paddle_x < 0)
          paddle_x = 0;

       }  // end if

    // draw blocks

    // move the ball

    // keep ball on screen, if the ball hits the edge of
    // screen then bounce it by reflecting its velocity
    if (ball_x > (SCREEN_WIDTH - BALL_SIZE) || ball_x < 0)
       // reflect x-axis velocity

       // update position
       }  // end if
    // now y-axis
    if (ball_y < 0)
       // reflect y-axis velocity

       // update position
       }  // end if
   // penalize player for missing the ball
   if (ball_y > (SCREEN_HEIGHT - BALL_SIZE))
       // reflect y-axis velocity

       // update position

       // minus the score

       }  // end if

    // next watch out for ball velocity getting out of hand
    if (ball_dx > 8) ball_dx = 8;
    if (ball_dx < -8) ball_dx = -8;

    // test if ball hit any blocks or the paddle

    // draw the paddle and shadow
    Draw_Rectangle(paddle_x-8, paddle_y+8,

    Draw_Rectangle(paddle_x, paddle_y,

    // draw the ball
    Draw_Rectangle(ball_x-4, ball_y+4, ball_x+BALL_SIZE-4,
                   ball_y+BALL_SIZE+4, 0);
    Draw_Rectangle(ball_x, ball_y, ball_x+BALL_SIZE,
                   ball_y+BALL_SIZE, 255);

    // draw the info
    sprintf(buffer,"F R E A K O U T           Score %d   //
          Level %d",score,level);
    Draw_Text_GDI(buffer, 8,SCREEN_HEIGHT-16, 127);
    // flip the surfaces

    // sync to 33ish fps

    // check if user is trying to exit
       // send message to windows to exit
       PostMessage(main_window_handle, WM_DESTROY,0,0);

       // set exit state
       game_state = GAME_STATE_SHUTDOWN;

       }  // end if

    }  // end if
if (game_state == GAME_STATE_SHUTDOWN)
   // in this state shut everything down and release resources

   // switch to exit state
   game_state = GAME_STATE_EXIT;

   }  // end if

// return success

}  // end Game_Main

Cool, huh? That's the entire Win32/DirectX game. Well, almost. There are a few hundred lines of code in the BLACKBOX.CPP source file, but we'll just pretend that it's like DirectX and someone else wrote it (me!). Anyway, let's take a quick look at the contents of Listing 1.3.

Basically, Windows needs to have what's called an event loop. This is standard for all Windows programs since Windows is, for the most part, event-driven. However, games aren't event-driven; they run at all times, whether the user does something or not. So we need to at least support a minimum event loop to make Windows happy. The code that implements this is in WinMain()—jeez, that's a surprise, huh? WinMain() is the main entry point for all Windows programs, just like main() is the entry point for all DOS/Unix programs (please wash your mouth out if you said "Unix" out loud). In any case, the WinMain() for FreakOut creates a window and then enters right into the event loop. If Windows needs to do something, it does so. When all the basic event handling is over, Game_Main() is called. This is where the real action occurs for our game.

If you wanted to, you could loop in Game_Main() forever, never releasing it back to the main event loop in WinMain(). But this would be bad because Windows would never receive any messages and you would starve the system. Alas, what we need to do is perform one frame of animation and logic and then return back to WinMain(). This way, Windows will continue to function and process messages. If this all sounds a little hocus-pocus, don't worry—it gets worse in the next chapter <BG>.

Once in Game_Main(), the logic for FreakOut is executed. The game image is rendered into an offscreen workspace and then finally shown on the display at the end of the loop via the DD_FLIP() call. So what I want you to do is take a look at all the game states and try to follow each section of the game loop and what it does. To play the game, simply click on FREAKOUT.EXE. The program will launch immediately. The controls are

Right arrow—Move paddle right.

Left arrow—Move paddle left.

Esc—Exit back to Windows.

Also, there's a 100-point penalty if you miss the ball, so watch it!

When you feel comfortable with the game code and gameplay, try modifying the game and making changes to it. You could add different background colors (0–255 are valid colors), more balls, a paddle that changes size, and more sound effects (which I'm making right now with the Win32 API MessageBeep() function).

      Previous Section Next Section

    JavaScript EditorAjax Editor     JavaScript Editor