- How Does the Game Play?
- Designing the Game
- Building the Game
- Testing the Game
- Summary
- Q&A
- Workshop
Building the Game
Hopefully by now you're getting antsy to see how the Henway game is put together. The next couple of sections explore the code development for the Henway game, which is relatively simple when you consider that this is the first fully functioning game you've created that supports double-buffer sprite animation. The revamped game engine with sprite management really makes the Henway game a smooth game to develop.
Writing the Game Code
The code for the Henway game begins with the Henway.h header file, which is shown in Listing 12.1.
Listing 12.1 The Henway.h Header File Declares Global Variables that Are Used to Manage the Game, as well as a Helper Function
1: #pragma once 2: 3: //----------------------------------------------------------------- 4: // Include Files 5: //----------------------------------------------------------------- 6: #include <windows.h> 7: #include "Resource.h" 8: #include "GameEngine.h" 9: #include "Bitmap.h" 10: #include "Sprite.h" 11: 12: //----------------------------------------------------------------- 13: // Global Variables 14: //----------------------------------------------------------------- 15: HINSTANCE _hInstance; 16: GameEngine* _pGame; 17: HDC _hOffscreenDC; 18: HBITMAP _hOffscreenBitmap; 19: Bitmap* _pHighwayBitmap; 20: Bitmap* _pChickenBitmap; 21: Bitmap* _pCarBitmaps[4]; 22: Bitmap* _pChickenHeadBitmap; 23: Sprite* _pChickenSprite; 24: int _iInputDelay; 25: int _iNumLives; 26: int _iScore; 27: BOOL _bGameOver; 28: 29: //----------------------------------------------------------------- 30: // Function Declarations 31: //----------------------------------------------------------------- 32: void MoveChicken(int iXDistance, int iYDistance);
The global variables for the Henway game consist largely of the different bitmaps used throughout the game. The offscreen device context and bitmap are declared (lines 17 and 18), as well as the different bitmaps that comprise the game's graphics (lines 1922). Even the sprite management features of the game engine are being used in this game; it's necessary to keep a pointer to the chicken sprite so that you can change its position in response to user input events. The _pChickenSprite member variable is used to store the chicken sprite pointer (line 23). The input delay for the keyboard and joystick is declared next (line 24), along with the number of chicken lives remaining (line 25) and the score (line 26). The last variable is the Boolean game over variable, which simply keeps track of whether the game is over (line 27).
You'll notice that a helper function named MoveChicken() is declared in the Henway.h file. This function is called by other game functions within the game to move the chicken sprite in response to user input events. The two arguments to the MoveChicken() function are the X and Y amounts to move the chicken.
The actual game functions appear in the Henway.cpp source code file, which is located on the accompanying CD-ROM. The first game function worth mentioning is the GameInitialize() function, which creates the game engine, establishes the frame rate, and initializes the joystick.
The GameStart() function is a little more interesting than GameInitialize(). This function is responsible for initializing the game data, as shown in Listing 12.2.
Listing 12.2 The GameStart() Function Creates the Offscreen Buffer, Loads the Game Bitmaps, Creates the Game Sprites, and Initializes Game State Member Variables
1: void GameStart(HWND hWindow) 2: { 3: // Seed the random number generator 4: srand(GetTickCount()); 5: 6: // Create the offscreen device context and bitmap 7: _hOffscreenDC = CreateCompatibleDC(GetDC(hWindow)); 8: _hOffscreenBitmap = CreateCompatibleBitmap(GetDC(hWindow), 9: _pGame->GetWidth(), _pGame->GetHeight()); 10: SelectObject(_hOffscreenDC, _hOffscreenBitmap); 11: 12: // Create and load the bitmaps 13: HDC hDC = GetDC(hWindow); 14: _pHighwayBitmap = new Bitmap(hDC, IDB_HIGHWAY, _hInstance); 15: _pChickenBitmap = new Bitmap(hDC, IDB_CHICKEN, _hInstance); 16: _pCarBitmaps[0] = new Bitmap(hDC, IDB_CAR1, _hInstance); 17: _pCarBitmaps[1] = new Bitmap(hDC, IDB_CAR2, _hInstance); 18: _pCarBitmaps[2] = new Bitmap(hDC, IDB_CAR3, _hInstance); 19: _pCarBitmaps[3] = new Bitmap(hDC, IDB_CAR4, _hInstance); 20: _pChickenHeadBitmap = new Bitmap(hDC, IDB_CHICKENHEAD, _hInstance); 21: 22: // Create the chicken and car sprites 23: Sprite* pSprite; 24: RECT rcBounds = { 0, 0, 465, 400 }; 25: _pChickenSprite = new Sprite(_pChickenBitmap, rcBounds, BA_STOP); 26: _pChickenSprite->SetPosition(4, 175); 27: _pChickenSprite->SetVelocity(0, 0); 28: _pChickenSprite->SetZOrder(1); 29: _pGame->AddSprite(_pChickenSprite); 30: pSprite = new Sprite(_pCarBitmaps[0], rcBounds, BA_WRAP); 31: pSprite->SetPosition(70, 0); 32: pSprite->SetVelocity(0, 7); 33: pSprite->SetZOrder(2); 34: _pGame->AddSprite(pSprite); 35: pSprite = new Sprite(_pCarBitmaps[1], rcBounds, BA_WRAP); 36: pSprite->SetPosition(160, 0); 37: pSprite->SetVelocity(0, 3); 38: pSprite->SetZOrder(2); 39: _pGame->AddSprite(pSprite); 40: pSprite = new Sprite(_pCarBitmaps[2], rcBounds, BA_WRAP); 41: pSprite->SetPosition(239, 400); 42: pSprite->SetVelocity(0, -5); 43: pSprite->SetZOrder(2); 44: _pGame->AddSprite(pSprite); 45: pSprite = new Sprite(_pCarBitmaps[3], rcBounds, BA_WRAP); 46: pSprite->SetPosition(329, 400); 47: pSprite->SetVelocity(0, -10); 48: pSprite->SetZOrder(2); 49: _pGame->AddSprite(pSprite); 50: 51: // Initialize the remaining global variables 52: _iInputDelay = 0; 53: _iNumLives = 3; 54: _iScore = 0; 55: _bGameOver = FALSE; 56: }
The GameStart() function contains a fair amount of code, which primarily has to do with the fact that creating each sprite requires a few lines of code. The function starts out by creating the offscreen device context and bitmap (lines 710). All the bitmaps for the game are then loaded (lines 1320). Finally, the really interesting part of the function involves the creation of the sprites, which you should be able to follow without too much difficulty. The chicken sprite is first created at a position in the Start Area of the game screen, and with 0 velocity (lines 2529). The car sprites are then created at different positions and with varying velocities (lines 3049). Notice that the bounds actions for the car sprites are set so that the cars wrap around the game screen, whereas the chicken sprite stops when it encounters a boundary. Also, the Z-order of the cars is set higher than the chicken so that the chicken will appear under the cars when it gets run over.
The remaining member variables in the Henway game are initialized in the GameStart() function after the sprites are created. The input delay is set to 0 (line 52), whereas the number of chicken lives is set to 3 (line 53). The score is also set to 0 (line 54), and the game over variable is set to FALSE to indicate that the game isn't over (line 55).
The Henway game relies on the keyboard and joystick for user input. In order to support joystick input, it's important to capture and release the joystick whenever the game window is activated and deactivated. Listing 12.3 shows the code for the GameActivate() and GameDeactivate() functions, which are responsible in this case for capturing and releasing the joystick.
Listing 12.3 The GameActivate() and GameDeactivate() Functions Capture and Release the Joystick, Respectively
1: void GameActivate(HWND hWindow) 2: { 3: // Capture the joystick 4: _pGame->CaptureJoystick(); 5: } 6: 7: void GameDeactivate(HWND hWindow) 8: { 9: // Release the joystick 10: _pGame->ReleaseJoystick(); 11: }
The GameActivate() function calls the CaptureJoystick() method on the game engine to capture the joystick (line 4), whereas the GameDeactivate() function calls ReleaseJoystick() to release the joystick (line 10).
As you know, the GamePaint() function is responsible for painting games. Listing 12.4 contains the code for the Henway game's GamePaint() function.
Listing 12.4 The GamePaint()Function Draws the Highway Background Image, the Game Sprites, and the Number of Remaining Chicken Lives
1: void GamePaint(HDC hDC) 2: { 3: // Draw the background highway 4: _pHighwayBitmap->Draw(hDC, 0, 0); 5: 6: // Draw the sprites 7: _pGame->DrawSprites(hDC); 8: 9: // Draw the number of remaining chicken lives 10: for (int i = 0; i < _iNumLives; i++) 11: _pChickenHeadBitmap->Draw(hDC, 12: 406 + (_pChickenHeadBitmap->GetWidth() * i), 382, TRUE); 13: }
This GamePaint() function must draw all the game graphics for the Henway game. The function begins by drawing the background highway image (line 4), and then it draws the game sprites (line 7). The remainder of the function draws the number of remaining chicken lives in the lower right corner of the game screen using small chicken head bitmaps (lines 1012). A small chicken head is drawn for each chicken life remaining, which helps you to know how many times you can get run over before the game ends.
The GameCycle() function works hand in hand with GamePaint() to update the game's sprites and then reflect the changes onscreen. Listing 12.5 shows the code for the GameCycle() function.
Listing 12.5 The GameCycle() Function Updates the Game Sprites and Repaints the Game Screen Using an Offscreen Buffer to Eliminate Flicker
1: void GameCycle() 2: { 3: if (!_bGameOver) 4: { 5: // Update the sprites 6: _pGame->UpdateSprites(); 7: 8: // Obtain a device context for repainting the game 9: HWND hWindow = _pGame->GetWindow(); 10: HDC hDC = GetDC(hWindow); 11: 12: // Paint the game to the offscreen device context 13: GamePaint(_hOffscreenDC); 14: 15: // Blit the offscreen bitmap to the game screen 16: BitBlt(hDC, 0, 0, _pGame->GetWidth(), _pGame->GetHeight(), 17: _hOffscreenDC, 0, 0, SRCCOPY); 18: 19: // Cleanup 20: ReleaseDC(hWindow, hDC); 21: } 22: }
The GameCycle() function first checks to make sure that the game isn't over (line 3); in which case, there would be no need to update anything. The function then updates the sprites (line 6) and goes about redrawing the game graphics using double-buffer animation. This double-buffer code should be fairly familiar to you by now, so I won't go into the details. The main thing to notice is that the GamePaint() function is ultimately being used to draw the game graphics (line 13).
I mentioned earlier that the Henway game supports both keyboard and joystick input. Listing 12.6 contains the code for the HandleKeys() function, which takes care of processing and responding to keyboard input in the game.
Listing 12.6 The HandleKeys() Function Responds to the Arrow Keys on the Keyboard by Moving the Chicken
1: void HandleKeys() 2: { 3: if (!_bGameOver && (++_iInputDelay > 2)) 4: { 5: // Move the chicken based upon key presses 6: if (GetAsyncKeyState(VK_LEFT) < 0) 7: MoveChicken(-20, 0); 8: else if (GetAsyncKeyState(VK_RIGHT) < 0) 9: MoveChicken(20, 0); 10: if (GetAsyncKeyState(VK_UP) < 0) 11: MoveChicken(0, -20); 12: else if (GetAsyncKeyState(VK_DOWN) < 0) 13: MoveChicken(0, 20); 14: 15: // Reset the input delay 16: _iInputDelay = 0; 17: } 18: }
The HandleKeys() function begins by making sure that the game isn't over, as well as incrementing and testing the input delay (line 3). By testing the input delay before processing any keyboard input, the HandleKeys() function effectively slows down the input so that the chicken is easier to control. The chicken is actually controlled via the arrow keys, which are checked using the Win32 GetAsyncKeyState() function. Each arrow key is handled by calling the MoveChicken() function, which moves the chicken by a specified amount (lines 613). After processing the keys, the input delay is reset so that the input process can be repeated (line 16).
The joystick is handled in the Henway game in a similar manner as the mouse, as Listing 12.7 reveals.
Listing 12.7 The HandleJoystick() Function Responds to Joystick Movements by Moving the Chicken, and also Supports Using the Primary Joystick Button to Start a New Game
1: void HandleJoystick(JOYSTATE jsJoystickState) 2: { 3: if (!_bGameOver && (++_iInputDelay > 2)) 4: { 5: // Check horizontal movement 6: if (jsJoystickState & JOY_LEFT) 7: MoveChicken(-20, 0); 8: else if (jsJoystickState & JOY_RIGHT) 9: MoveChicken(20, 0); 10: 11: // Check vertical movement 12: if (jsJoystickState & JOY_UP) 13: MoveChicken(0, -20); 14: else if (jsJoystickState & JOY_DOWN) 15: MoveChicken(0, 20); 16: 17: // Reset the input delay 18: _iInputDelay = 0; 19: } 20: 21: // Check the joystick button and start a new game, if necessary 22: if (_bGameOver && (jsJoystickState & JOY_FIRE1)) 23: { 24: _iNumLives = 3; 25: _iScore = 0; 26: _bGameOver = FALSE; 27: } 28: }
The HandleJoystick() function performs the same check on the _bGameOver and _iInputDelay variables to make sure that it is time to check the joystick for input (line 3). If so, the joystick is first checked for horizontal movement by examining the jsJoystickState argument passed in to the function. The chicken is then moved left or right, if necessary, by calling the MoveChicken() function (lines 69). A similar process is then repeated for vertical joystick movement (lines 1215). After handling joystick movement, the HandleJoystick() function resets the _iInputDelay variable (line 18). The function then concludes by checking to see if the primary joystick button was pressed (line 22); in which case, a new game is started (lines 2426).
Speaking of starting a new game, the mouse is used in the Henway game solely to start a new game if the current game has ended. Listing 12.8 shows the code for the MouseButtonDown() function, which starts a new game in response to a mouse button click.
Listing 12.8 The MouseButtonDown() Function Starts a New Game if the Current Game Is Over
1: void MouseButtonDown(int x, int y, BOOL bLeft) 2: { 3: // Start a new game, if necessary 4: if (_bGameOver) 5: { 6: _iNumLives = 3; 7: _iScore = 0; 8: _bGameOver = FALSE; 9: } 10: }
If a mouse button is clicked and the current game is over, the MouseButtonDown() function starts a new game by clearing the game state variables (lines 68).
The game play of the Henway game is largely dictated by the sprites in the game. These sprites are capable of colliding; in which case, the SpriteCollision() function gets called. Of course, the car sprites are designed to only move vertically up and down the screen, so they'll never hit each other. This means that the SpriteCollision() function only gets called when a car hits the chicken, or vice versa. Listing 12.9 shows how this collision is handled in the SpriteCollision() function.
Listing 12.9 The SpriteCollision() Function Checks to See if the Chicken Was Hit by a Car, and Then Responds Accordingly
1: BOOL SpriteCollision(Sprite* pSpriteHitter, Sprite* pSpriteHittee) 2: { 3: // See if the chicken was hit 4: if (pSpriteHittee == _pChickenSprite) 5: { 6: // Move the chicken back to the start 7: _pChickenSprite->SetPosition(4, 175); 8: 9: // See if the game is over 10: if (--_iNumLives > 0) 11: MessageBox(_pGame->GetWindow(), TEXT("Ouch!"), TEXT("Henway"), MB_OK); 12: else 13: { 14: // Display game over message 15: TCHAR szText[64]; 16: wsprintf(szText, "Game Over! You scored %d points.", _iScore); 17: MessageBox(_pGame->GetWindow(), szText, TEXT("Henway"), MB_OK); 18: _bGameOver = TRUE; 19: } 20: 21: return FALSE; 22: } 23: 24: return TRUE; 25: }
Most of the game logic in the Henway game is located in the SpriteCollision() function. First, a check is made to ensure that the chicken was indeed involved in the collision (line 4). If so, you can safely assume that the chicken was hit by a car, so the chicken's position is restored to its starting position in the Start Area (line 7). The number of chicken lives is then decremented and checked to see if the game is over (line 10). If the game isn't over, a message is displayed indicating that you lost a chicken (line 11), but the game ultimately continues on. If the game is over, however, a special "Game Over" message is displayed and the _bGameOver variable is set to TRUE (lines 1518).
If you recall from the design of the sprite manager, the return value of the SpriteCollision() function determines whether the sprite's old position is restored; a value of TRUE restores the old position, whereas FALSE allows the sprite to keep its newly updated position. In this case, the sprite keeps its new position when it collides with a car (line 21). Because the position was just set to the Start Area in line 7, the effect is that the chicken is allowed to move to the start area, which gives the appearance of a new chicken appearing.
The final function in the Henway game is the MoveChicken() function, which you've used several times throughout the game code. Listing 12.10 shows the code for the MoveChicken() function.
Listing 12.10 The MoveChicken() Function Moves the Chicken Sprite by a Specified Distance While Checking to See if the Chicken Made It Across the Highway
1: void MoveChicken(int iXDistance, int iYDistance) 2: { 3: // Move the chicken to its new position 4: _pChickenSprite->OffsetPosition(iXDistance, iYDistance); 5: 6: // See if the chicken made it across 7: if (_pChickenSprite->GetPosition().left > 400) 8: { 9: // Move the chicken back to the start and add to the score 10: _pChickenSprite->SetPosition(4, 175); 11: _iScore += 150; 12: MessageBox(_pGame->GetWindow(), TEXT("You made it!"), TEXT("Henway"), 13: MB_OK); 14: } 15: }
The MoveChicken() function is a helper function that simplifies the task of moving the chicken around on the game screen. The iXDistance and iYDistance arguments specify how many pixels to move the chicken in the X and Y directions (line 1). These arguments are used to move the chicken by calling the OffsetPosition() method on the Sprite class (line 4). If you didn't care what happened to the chicken, this is all the code you would need in the MoveChicken() function. However, you need to know when the chicken makes it across the highway, and this is a perfect place to perform the check (line 7). If the chicken made it safely across, its position is set back to the Start Area (line 10) and the score is increased (11). A message is also displayed that notifies the player of a successful highway crossing (lines 1213) .