Chapter 19. Example Game: Meteor Defense

This hour guides you through the design and development of another complete game. You’ve spent the past couple of hours learning how to animate the appearance of sprites and spruce up the background of games, and it’s now time to put this knowledge to work in an entirely new game. The game is called Meteor Defense, and it is loosely based on the classic Missile Command arcade game. Seeing as how there have been several news reports in the past few years about the potential of a significant meteor collision with the Earth, I thought it might make a neat premise for a game. It wouldn’t necessarily be a bad idea to have a missile-based system for stopping incoming meteors, which is the premise behind the game Meteor Defense.

In this hour, you’ll learn:

  • About the conceptual overview of the Meteor Defense game

  • How to design the Meteor Defense game

  • How to add a few new sprite features to the game engine

  • What it takes to build the Meteor Defense game

  • How much fun it can be testing a new game

How Does the Game Play?

One of the classic arcade games that many people remember is Missile Command, which involves the defense of a group of cities against a nuclear attack. The nuclear attack involves missiles that travel down from the top of the screen toward the cities at the bottom. Your job is to fire upon the missiles and destroy them before they reach the cities. Although Missile Command made for an interesting game in the era of the hit movie War Games, the threat of nuclear attack is somewhat diminished these days, at least in terms of what most of us perceive as a realistic threat. However, there has been increasing talk in the past few years of the possibility of a meteor striking the Earth and causing major damage.

The premise of the game you develop in this hour is similar to Missile Command in that you’re defending helpless cities against an incoming threat. In this case, however, the threat is giant meteors, not nuclear warheads. The Meteor Defense game employs a game play strategy similar to Missile Command in that you fire missiles at the incoming meteors to stop them from destroying the cities below. As you’ll find out, the critical skill to becoming a good player at Meteor Defense (and Missile Command as well) is learning how to target a meteor and give a missile time to get there. In other words, you often have to lead a meteor by a certain distance to give the missile time to get there and make contact.

The object of Meteor Defense is to simply protect the cities against the incoming meteors for as long as possible. One interesting aspect of the game is that you lose points whenever you fire a missile, which makes it important to be efficient when you fire on the meteors. In other words, if you unleash missiles indiscriminately, you will no doubt protect the cities but your score will suffer. This is a subtle way to discourage sloppy game play. Small touches like this can often make a game much more appealing to serious game players.

Designing the Game

The design of the Meteor Defense game flows directly from the overview of the game that you just went through. The game is well suited for the starry background that you created and used in previous hours. It’s also fairly obvious that the meteors should be represented by animated sprites similar to those found in the Roids program example from the previous hour. Because you must be able to detect a collision between a meteor and a city, the cities also need to be modeled as sprites even though they don’t move. Representing cities as sprites also gives you the freedom to hide them whenever they are destroyed by a meteor.

Note

Designing the Game

In case you were wondering, the terms meteor, meteorite, and meteoroid are all closely related. A meteoroid is a chunk of rock in space that ranges in size from a speck of dust to 100 meters across. A meteor is the bright flash of light generated by a meteoroid when it travels through Earth’s atmosphere. And finally, a meteorite is a meteor that has survived the transition through Earth’s atmosphere and comes to rest on the Earth’s surface.

The missiles in the game are ideal candidates for sprites because they move and collide with meteors. This is generally a good rule of thumb when it comes to deciding which graphical objects should be represented as sprites and which ones can simply be placed in the background: If the object needs to move, animate, or collide with other objects, it should be a sprite. A good application of this test is the guns used to fire missiles in the Meteor Defense game. The guns don’t move, they don’t animate, and it isn’t important to detect a collision between them and anything else. Therefore, you can simply include the guns in the background image for the game.

Wait a minute, I just mentioned using a background image, but I already said that the background is the starry background from earlier in the book. The Meteor Defense game actually uses a hybrid background in that it displays an image of the ground over the starry background. This allows you to get the benefits of the animated starry sky while also showing the ground where the cities are located—not to mention the guns that fire the missiles. To help you get a feel for how the Meteor Defense game is laid out, take a look at Figure 19.1.

The Meteor Defense game consists of a starry background, a ground image with guns, several cities, incoming meteors, and missiles.

Figure 19.1. The Meteor Defense game consists of a starry background, a ground image with guns, several cities, incoming meteors, and missiles.

The figure reveals how the guns are part of the background image that appears along the bottom edge of the game screen. Keep in mind that a starry background still appears in the majority of the screen—the background image just shows the ground where the cities are located. The city sprites are laid on top of the edge of the ground so that they blend into the ground image. The missile and meteor sprites move around on top of the starry background, while the score is displayed centered near the top of the game screen. One last piece of the game not shown in the figure is the target sprite that you guide with the mouse, much like the target sprite you saw in the Battle Office game from earlier in the book.

Now that you understand the basics of the game and how it is laid out, it’s time to examine what kinds of sprites need to be used. Following is a list of the sprites that go into the Meteor Defense game:

  • City sprites

  • Meteor sprites

  • Missile sprites

  • Explosion sprites

  • Target sprite

The city sprites appear along the bottom of the screen, as shown in Figure 19.1. The meteor sprites are created at random, and fall from the top of the screen toward the cities at the bottom. The missile sprites are fired from the gun locations on the screen upward toward the meteors. I haven’t mentioned the explosion sprites yet, and that’s because there isn’t a whole lot to them. An explosion sprite appears whenever a meteor is blown up or a city is destroyed and involves displaying an animation of a fiery explosion. Finally, the target sprite appears as a crosshair that you guide with the mouse to aim missiles that you launch via the left mouse button.

Beyond the sprites, the Meteor Defense game also requires several bitmaps. Following are the seven bitmap images required of the game:

  • Background ground image

  • City image

  • Meteor image (animated)

  • Missile image

  • Explosion image (animated)

  • Crosshair target image

  • Game over image

These images should make sense based on the description of the game and the sprites that you’ve learned about.

With the graphical objects squared away, you need to turn your attention to the other information that must be maintained by the game. For example, the score needs to be maintained throughout the game, as well as the number of remaining cities; the game ends when all four of the cities are destroyed by meteors. This is a game in which it is important to increase the difficulty level as the game progresses. So, it’s important to store the difficulty level of the game and gradually increase it as the player continues to keep his cities protected. The last critical piece of information to maintain is a Boolean variable to keep track of whether the game is over.

To recap, the design of the Meteor Defense game has led us to the following pieces of information that must be managed by the game:

  • The number of cities remaining

  • The score

  • The difficulty level

  • A Boolean game over variable

With this information in mind, you’re now almost ready to take the big step of assembling the code for the Meteor Defense game. However, there is a modification you need to make to the game engine to support a critical feature of the game, and ultimately improve the game engine for future games.

Enhancing Sprites in the Game Engine

If you’re a particularly inquisitive person, you might have wondered how exactly the explosion sprites will work in the game. The animation part is simple enough because the Sprite class in the game engine now allows you to flip through a series of animation images. However, an explosion sprite must cycle through its animation frames and then immediately go away, which sounds simple but presents a problem for the game engine. The problem is that there currently is no mechanism for a sprite to hide or kill itself automatically when it is no longer needed. Granted, you can kill a sprite from within the game code, but how would you keep track of when the frame animation for an explosion is finished?

The real problem I’m talking about here is that of allowing a sprite to animate through one cycle and then go away. The game engine doesn’t currently support this feature, but it’s not too difficult to add. Without this feature, there is no straightforward way to use a sprite such as an explosion that must cycle through its animation once and then exit the game. The key to adding this feature to the game engine is including a couple of new variables to the Sprite class:

BOOL m_bDying;
BOOL m_bOneCycle;

The m_bDying member variable determines whether a sprite has been flagged as dying. In other words, normal sprites have their m_bDying variable set to FALSE, whereas a sprite on the way out has it set to TRUE. The cool thing about this variable is that it allows you to kill a sprite at any time by simply setting the variable to TRUE. Of course, this requires some additional code in both the Sprite and GameEngine classes to make it actually work.

The second member variable, m_bOneCycle, indicates whether a sprite should animate once through its frames and then kill itself. Because this variable only makes sense within the context of a frame animated sprite, it is set when you call the SetNumFrames() method. You see how the SetNumFrames() method is modified to account for the m_bOneCycle variable in a moment.

For now, let’s take a look at how the two new Sprite member variables are initialized in the Sprite() constructors:

m_bDying = FALSE;
m_bOneCycle = FALSE;

As you might expect, the default value of both variables is FALSE, which makes sprites behave normally.

The m_bDying member variable can be set to TRUE through the Kill() method, which is really just an accessor method because all it does is set the variable:

void Kill()  { m_bDying = TRUE; };

This method now gives you a clean and efficient means of killing any sprite—with the security of knowing that it will be properly removed from the sprite list maintained by the game engine. This is a crucial aspect of destroying a sprite because the sprite list in the game engine would go haywire if you were to simply delete a sprite from memory and not remove it from the list. The Kill() method provides a clean interface for carrying out this task.

Whereas the Kill() method provides an immediate way to kill a sprite that can be useful in some situations, the more elegant approach is to allow a sprite to kill itself when it finishes cycling through its frame animations. The UpdateFrame() method now supports this feature by examining the m_bOneCycle variable, and then setting the m_bDying variable accordingly. The original version of this method simply set the m_iCurFrame variable to 0 so that the animation starts over, which this method still does if the m_bOneCycle variable is FALSE. However, if the m_bOneCycle variable is TRUE, the m_bDying variable is set to TRUE, which starts the sprite on a path to destruction.

The m_bOneCycle variable is set in the SetNumFrames() method, which now looks like this:

void SetNumFrames(int iNumFrames, BOOL bOneCycle = FALSE);

As you can see, the SetNumFrames() method now includes a second argument for setting the m_bOneCycle member variable. To help make the transition to using the new version of the method easier, the bOneCycle argument is given a default value of FALSE. This allows you to use the method just as you’ve already grown accustomed. However, if you want to create a sprite that cycles through its animation once and then goes away, just pass TRUE as the second argument to SetNumFrames().

Getting back to the killing of a sprite via the m_bDying member variable, the place where the murder plot unfolds is in the Update() method, which is shown in Listing 19.1.

Example 19.1. The Sprite::Update() Method Kills a Sprite if It Is Flagged as Dying

 1: SPRITEACTION Sprite::Update()
 2: {
 3:   // See if the sprite needs to be killed
 4:   if (m_bDying)
 5:     return SA_KILL;
 6:
 7:   // Update the frame
 8:   UpdateFrame();
 9:
10:   ...
11: }

If you recall, the Sprite::Update() method is actually a large method, so I’m only showing you the beginning of it here because this is all that has changed. The method now checks the value of the m_bDying member variable (line 4), and then returns SA_KILL if it is set to TRUE (line 5). If you recall from earlier in the book, SA_KILL is a sprite action you created that notifies the game engine when a sprite needs to be killed. Prior to now, it was only used to kill a sprite when it encounters a boundary.

Simply killing a sprite isn’t quite sufficient when it comes to improving the game engine for future games. You’ll find out later in the hour that it can be incredibly useful to know when a sprite is being destroyed—regardless of why it is being destroyed. For example, when a meteor sprite is destroyed, you can create an explosion sprite to show the destruction of the meteor. The notification of a sprite being killed is made possible through the SpriteDying() function, which is called whenever a sprite is dying:

void SpriteDying(Sprite* pSprite);

Understand that the SpriteDying() function is a function that you must provide as part of a game. In other words, it is designed to house game-specific code that responds to particular types of sprites dying within a game.

The final change to the game engine to support the new sprite killing features appears within the GameEngine class in the UpdateSprites() method, which now includes a call to SpriteDying() that notifies a game of a sprite being destroyed and removed from the sprite list. This gives the game a chance to respond to the sprite’s demise and carry out any appropriate actions.

Building the Game

You can breathe a sigh of relief because you’re finished making changes to the game engine for a little while. However, the real challenge of putting together the Meteor Defense game now awaits you. Fortunately, the game isn’t too difficult to understand because you’ve already worked through a reasonably detailed game design. The next few sections guide you through the development of the game’s code and resources.

Writing the Game Code

The code for the Meteor Defense game begins in the MeteorDefense.h header file, which is responsible for declaring the global variables used throughout the game, as well as a couple of useful functions. Listing 19.2 contains the code for this file.

Example 19.2. The MeteorDefense.h Header File Declares Global Variables and Game-Specific Functions for the Meteor Defense Game

 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: #include "Background.h"
12:
13: //-----------------------------------------------------------------
14: // Global Variables
15: //-----------------------------------------------------------------
16: HINSTANCE         _hInstance;
17: GameEngine*       _pGame;
18: HDC               _hOffscreenDC;
19: HBITMAP           _hOffscreenBitmap;
20: Bitmap*           _pGroundBitmap;
21: Bitmap*           _pTargetBitmap;
22: Bitmap*           _pCityBitmap;
23: Bitmap*           _pMeteorBitmap;
24: Bitmap*           _pMissileBitmap;
25: Bitmap*           _pExplosionBitmap;
26: Bitmap*           _pGameOverBitmap;
27: StarryBackground* _pBackground;
28: Sprite*           _pTargetSprite;
29: int               _iNumCities, _iScore, _iDifficulty;
30: BOOL              _bGameOver;
31:
32: //-----------------------------------------------------------------
33: // Function Declarations
34: //-----------------------------------------------------------------
35: void NewGame();
36: void AddMeteor();

As the listing reveals, the global variables for the Meteor Defense game largely consist of the different bitmaps that are used throughout the game. The starry background for the game is declared after the bitmaps (line 27), followed by the crosshair target sprite (line 28). Member variables storing the number of remaining cities, the score, and the difficulty level are then declared (line 29), along with the familiar game over Boolean variable (line 30).

The NewGame() function declared in the MeteorDefense.h file is important because it is used to set up and start a new game (line 35). Unlike the GameStart() function, which performs critical initialization tasks such as loading bitmaps, the NewGame() function deals with actually starting a new game once everything else is in place. The AddMeteor() function is a support function used to simplify the task of adding meteor sprites to the game (line 36). You find out more about how these functions work later in the hour.

The initialization of the game variables primarily takes place in the GameStart() function, which is shown in Listing 19.3.

Example 19.3. The GameStart() Function Initializes the Bitmaps and Background for the Game, and Calls the NewGame() Function

 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:    _pGroundBitmap = new Bitmap(hDC, IDB_GROUND, _hInstance);
15:    _pTargetBitmap = new Bitmap(hDC, IDB_TARGET, _hInstance);
16:    _pCityBitmap = new Bitmap(hDC, IDB_CITY, _hInstance);
17:    _pMeteorBitmap = new Bitmap(hDC, IDB_METEOR, _hInstance);
18:    _pMissileBitmap = new Bitmap(hDC, IDB_MISSILE, _hInstance);
19:    _pExplosionBitmap = new Bitmap(hDC, IDB_EXPLOSION, _hInstance);
20:    _pGameOverBitmap = new Bitmap(hDC, IDB_GAMEOVER, _hInstance);
21:
22:    // Create the starry background
23:    _pBackground = new StarryBackground(600, 450);
24:
25:    // Play the background music
26:    _pGame->PlayMIDISong(TEXT("Music.mid"));
27:
28:    // Start the game
29:    NewGame();
30: }

After loading the bitmaps for the game (lines 14–20) and creating the starry background (line 23), the GameStart() function starts playing the background music (line 26). The function then finishes by calling the NewGame() function to start a new game (line 29).

The GameEnd() function plays a complementary role to the GameStart() function and cleans up after the game. Listing 19.4 contains the code for the GameEnd() function.

Example 19.4. The GameEnd() Function Cleans Up After the Game

 1: void GameEnd()
 2: {
 3:   // Close the MIDI player for the background music
 4:   _pGame->CloseMIDIPlayer();
 5:
 6:   // Cleanup the offscreen device context and bitmap
 7:   DeleteObject(_hOffscreenBitmap);
 8:   DeleteDC(_hOffscreenDC);
 9:
10:   // Cleanup the bitmaps
11:   delete _pGroundBitmap;
12:   delete _pTargetBitmap;
13:   delete _pCityBitmap;
14:   delete _pMeteorBitmap;
15:   delete _pMissileBitmap;
16:   delete _pExplosionBitmap;
17:   delete _pGameOverBitmap;
18:
19:   // Cleanup the background
20:   delete _pBackground;
21:
22:   // Cleanup the sprites
23:   _pGame->CleanupSprites();
24:
25:   // Cleanup the game engine
26:   delete _pGame;
27: }

The first step in the GameEnd() function is to close the MIDI player (line 4). The bitmaps and sprites are then wiped away (lines 7–20), as well as the background (line 20). Finally, the sprites are cleaned up (line 23) and the game engine is destroyed (line 26).

The game screen in the Meteor Defense game is painted by the GamePaint() function, which is shown in Listing 19.5.

Example 19.5. The GamePaint() Function Draws the Background, the Ground Bitmap, the Sprites, the Score, and the Game Over Message

 1: void GamePaint(HDC hDC)
 2: {
 3:   // Draw the background
 4:   _pBackground->Draw(hDC);
 5:
 6:   // Draw the ground bitmap
 7:   _pGroundBitmap->Draw(hDC, 0, 398, TRUE);
 8:
 9:   // Draw the sprites
10:   _pGame->DrawSprites(hDC);
11:
12:   // Draw the score
13:   TCHAR szText[64];
14:   RECT  rect = { 275, 0, 325, 50 };
15:   wsprintf(szText, "%d", _iScore);
16:   SetBkMode(hDC, TRANSPARENT);
17:   SetTextColor(hDC, RGB(255, 255, 255));
18:   DrawText(hDC, szText, -1, &rect, DT_SINGLELINE | DT_CENTER | DT_VCENTER);
19:
20:   // Draw the game over message, if necessary
21:   if (_bGameOver)
22:     _pGameOverBitmap->Draw(hDC, 170, 150, TRUE);
23: }

This GamePaint() function is responsible for drawing all the graphics for the game. The function begins by drawing the starry background (line 4), followed by the ground image (line 7). The sprites are drawn next (line 10), followed by the score (lines 13–18). Notice that the score text is set to white (line 17), whereas the background is set to transparent for drawing the score so that the starry sky shows through the numbers in the score (line 16). The GamePaint() function finishes up by drawing the game over image, if necessary (lines 21–22).

The GameCycle() function works closely with GamePaint() to update the game’s sprites and reflect the changes onscreen. Listing 19.6 shows the code for the GameCycle() function.

Example 19.6. The GameCycle() Function Randomly Adds Meteors to the Game Based on the Difficulty Level

 1: void GameCycle()
 2: {
 3:   if (!_bGameOver)
 4:   {
 5:     // Randomly add meteors
 6:     if ((rand() % _iDifficulty) == 0)
 7:       AddMeteor();
 8:
 9:     // Update the background
10:     _pBackground->Update();
11:
12:     // Update the sprites
13:     _pGame->UpdateSprites();
14:
15:     // Obtain a device context for repainting the game
16:     HWND  hWindow = _pGame->GetWindow();
17:     HDC   hDC = GetDC(hWindow);
18:
19:     // Paint the game to the offscreen device context
20:     GamePaint(_hOffscreenDC);
21:
22:     // Blit the offscreen bitmap to the game screen
23:     BitBlt(hDC, 0, 0, _pGame->GetWidth(), _pGame->GetHeight(),
24:       _hOffscreenDC, 0, 0, SRCCOPY);
25:
26:     // Cleanup
27:     ReleaseDC(hWindow, hDC);
28:   }
29: }

Aside from the standard GameCycle() code that you’ve grown accustomed to seeing, this function doesn’t contain a whole lot of additional code. The new code involves randomly adding new meteors, which is accomplished by calling the AddMeteor() function after using the difficulty level to randomly determine if a meteor should be added (lines 6–7).

The MouseButtonDown() function is where most of the game logic for the Meteor Defense game is located, as shown in Listing 19.7.

Example 19.7. The MouseButtonDown() Function Launches a Missile Sprite Toward the Location of the Mouse Pointer

 1: void MouseButtonDown(int x, int y, BOOL bLeft)
 2: {
 3:   if (!_bGameOver && bLeft)
 4:   {
 5:     // Create a new missile sprite and set its position
 6:     RECT    rcBounds = { 0, 0, 600, 450 };
 7:     int     iXPos = (x < 300) ? 144 : 449;
 8:     Sprite* pSprite = new Sprite(_pMissileBitmap, rcBounds, BA_DIE);
 9:     pSprite->SetPosition(iXPos, 365);
10:
11:     // Calculate the velocity so that it is aimed at the target
12:     int iXVel, iYVel = -6;
13:     y = min(y, 300);
14:     iXVel = (iYVel * ((iXPos + 8) - x)) / (365 - y);
15:     pSprite->SetVelocity(iXVel, iYVel);
16:
17:     // Add the missile sprite
18:     _pGame->AddSprite(pSprite);
19:
20:     // Play the fire sound
21:     PlaySound((LPCSTR)IDW_FIRE, _hInstance, SND_ASYNC |
22:       SND_RESOURCE | SND_NOSTOP);
23:
24:     // Update the score
25:     _iScore = max(--_iScore, 0);
26:   }
27:   else if (_bGameOver && !bLeft)
28:     // Start a new game
29:     NewGame();
30: }

The MouseButtonDown() function handles firing a missile toward the target. The function first checks to make sure that the game isn’t over and that the player clicked the left mouse button (line 3). If so, a missile sprite is created based on the position of the mouse (lines 6–9). The position is important first because it determines which gun is used to fire the missile—the left gun fires missiles toward targets on the left side of the game screen, whereas the right gun takes care of the right side of the screen. The target position is also important because it determines the trajectory and therefore the velocity of the missile (lines 12–15).

After the missile sprite is created, the MouseButtonDown() function adds it to the game engine (line 18). A sound effect is then played to indicate that the missile has been fired (lines 21 and 22). Earlier in the hour during the design of the game, I mentioned how the score would be decreased slightly with each missile firing, which discourages inaccuracy in firing missiles because you can only build up your score by destroying meteors with the missiles. The score is decreased upon firing a missile, as shown in line 25. The last code in the MouseButtonDown() function takes care of starting a new game via the right mouse button if the game is over (lines 27–29).

Another mouse-related function used in the Meteor Defense game is MouseMove(), which is used to move the crosshair target sprite so that it tracks the mouse pointer. This is necessary so that you are always in control of the target sprite with the mouse. Listing 19.8 shows the code for the MouseMove() function.

Example 19.8. The MouseMove() Function Tracks the Mouse Cursor with the Target Sprite

 1: void MouseMove(int x, int y)
 2: {
 3:   // Track the mouse with the target sprite
 4:   _pTargetSprite->SetPosition(x - (_pTargetSprite->GetWidth() / 2),
 5:     y - (_pTargetSprite->GetHeight() / 2));
 6: }

The target sprite is made to follow the mouse pointer by simply calling the SetPosition() method on the sprite and passing in the mouse coordinates (lines 4 and 5). The position of the target sprite is calculated so that the target sprite always appears centered on the mouse pointer.

The SpriteCollision() function is called in response to sprites colliding, and is extremely important in the Meteor Defense game, as shown in Listing 19.9.

Example 19.9. The SpriteCollision() Function Detects and Responds to Collisions Between Missiles, Meteors, and Cities

 1: BOOL SpriteCollision(Sprite* pSpriteHitter, Sprite* pSpriteHittee)
 2: {
 3:   // See if a missile and a meteor have collided
 4:   if ((pSpriteHitter->GetBitmap() == _pMissileBitmap &&
 5:     pSpriteHittee->GetBitmap() == _pMeteorBitmap) ||
 6:     (pSpriteHitter->GetBitmap() == _pMeteorBitmap &&
 7:     pSpriteHittee->GetBitmap() == _pMissileBitmap))
 8:   {
 9:     // Kill both sprites
10:     pSpriteHitter->Kill();
11:     pSpriteHittee->Kill();
12:
13:     // Update the score
14:     _iScore += 6;
15:     _iDifficulty = max(50 - (_iScore / 10), 5);
16:   }
17:
18:   // See if a meteor has collided with a city
19:   if (pSpriteHitter->GetBitmap() == _pMeteorBitmap &&
20:     pSpriteHittee->GetBitmap() == _pCityBitmap)
21:   {
22:     // Play the big explosion sound
23:     PlaySound((LPCSTR)IDW_BIGEXPLODE, _hInstance, SND_ASYNC |
24:       SND_RESOURCE);
25:
26:     // Kill both sprites
27:     pSpriteHitter->Kill();
28:     pSpriteHittee->Kill();
29:
30:     // See if the game is over
31:     if (--_iNumCities == 0)
32:       _bGameOver = TRUE;
33:   }
34:
35:   return FALSE;
36: }

The first collision detected in the SpriteCollision() function is the collision between a missile and a meteor (lines 4–7). You might be a little surprised by how the code is determining what kinds of sprites are colliding. In order to distinguish between sprites, you need a piece of information that uniquely identifies each type of sprite in the game. The bitmap pointer turns out being a handy and efficient way to identify and distinguish between sprites. Getting back to the collision between a missile and a meteor, the SpriteCollision() function kills both sprites (lines 10 and 11) and increases the score because a meteor has been successfully hit with a missile (line 14).

The difficulty level is also modified so that it gradually increases along with the score (line 15). It’s worth pointing out that the game gets harder as the difficulty level decreases, with the maximum difficulty being reached at a value of 5 for the _iDifficulty global variable; the game is pretty much raining meteors at this level, which corresponds to reaching a score of 450. You can obviously tweak these values to suit your own tastes if you decide that the game gets difficult too fast or if you want to stretch out the time it takes for the difficulty level to increase.

The second collision detected in the SpriteCollision() function is between a meteor and a city (lines 19 and 20). If this collision takes place, a big explosion sound is played (lines 23 and 24), and both sprites are killed (lines 27 and 28). The number of cities is then checked to see if the game is over (lines 31 and 32).

Earlier in the hour, you added a new SpriteDying() function to the game engine that allows you to respond to a sprite being destroyed. This function comes in quite handy in the Meteor Defense game because it allows you to conveniently create an explosion sprite any time a meteor sprite is destroyed. Listing 19.10 shows how this is made possible by the SpriteDying() function.

Example 19.10. The SpriteDying() Function Creates an Explosion Whenever a Meteor Sprite Is Destroyed

 1: void SpriteDying(Sprite* pSprite)
 2: {
 3:   // See if a meteor sprite is dying
 4:   if (pSprite->GetBitmap() == _pMeteorBitmap)
 5:   {
 6:     // Play the explosion sound
 7:     PlaySound((LPCSTR)IDW_EXPLODE, _hInstance, SND_ASYNC |
 8:       SND_RESOURCE | SND_NOSTOP);
 9:
10:     // Create an explosion sprite at the meteor's position
11:     RECT rcBounds = { 0, 0, 600, 450 };
12:     RECT rcPos = pSprite->GetPosition();
13:     Sprite* pSprite = new Sprite(_pExplosionBitmap, rcBounds);
14:     pSprite->SetNumFrames(12, TRUE);
15:     pSprite->SetPosition(rcPos.left, rcPos.top);
16:     _pGame->AddSprite(pSprite);
17:   }
18: }

The bitmap pointer of the sprite is used again to determine if the dying sprite is indeed a meteor sprite (line 4). If so, an explosion sound effect is played (lines 7 and 8), and an explosion sprite is created (lines 11–16). Notice that the SetNumFrames() method is called to set the number of animation frames for the explosion sprite, as well as to indicate that the sprite should be destroyed after finishing its animation cycle (line 14). If you recall, this is one of the other important sprite-related features you added to the game engine earlier in the hour.

The remaining two functions in the Meteor Defense game are support functions that are completely unique to the game. The first one is NewGame(), which performs the steps necessary to start a new game (Listing 19.11).

Example 19.11. The NewGame() Function Gets Everything Ready for a New Game

 1: void NewGame()
 2: {
 3:   // Clear the sprites
 4:   _pGame->CleanupSprites();
 5:
 6:   // Create the target sprite
 7:   RECT rcBounds = { 0, 0, 600, 450 };
 8:   _pTargetSprite = new Sprite(_pTargetBitmap, rcBounds, BA_STOP);
 9:   _pTargetSprite->SetZOrder(10);
10:   _pGame->AddSprite(_pTargetSprite);
11:
12:   // Create the city sprites
13:   Sprite* pSprite = new Sprite(_pCityBitmap, rcBounds);
14:   pSprite->SetPosition(2, 370);
15:   _pGame->AddSprite(pSprite);
16:   pSprite = new Sprite(_pCityBitmap, rcBounds);
17:   pSprite->SetPosition(186, 370);
18:   _pGame->AddSprite(pSprite);
19:   pSprite = new Sprite(_pCityBitmap, rcBounds);
20:   pSprite->SetPosition(302, 370);
21:   _pGame->AddSprite(pSprite);
22:   pSprite = new Sprite(_pCityBitmap, rcBounds);
23:   pSprite->SetPosition(490, 370);
24:   _pGame->AddSprite(pSprite);
25:
26:   // Initialize the game variables
27:   _iScore = 0;
28:   _iNumCities = 4;
29:   _iDifficulty = 50;
30:   _bGameOver = FALSE;
31:
32:   // Play the background music
33:   _pGame->PlayMIDISong();
34: }

The NewGame() function starts off by clearing the sprite list (line 4), which is important because you don’t really know what might have been left over from the previous game. The crosshair target sprite is then created (lines 7–10), as well as the city sprites (lines 13–24). The global game variables are then set (lines 27–30), and the background music is started up (line 33).

The AddMeteor() function is shown in Listing 19.12, and its job is to add a new meteor to the game at a random position and aimed at a random city.

Example 19.12. The AddMeteor() Function Adds a New Meteor at a Random Position and Aimed at a Random City

 1: void AddMeteor()
 2: {
 3:   // Create a new meteor sprite and set its position
 4:   RECT    rcBounds = { 0, 0, 600, 390 };
 5:   int     iXPos = rand() % 600;
 6:   Sprite* pSprite = new Sprite(_pMeteorBitmap, rcBounds, BA_DIE);
 7:   pSprite->SetNumFrames(14);
 8:   pSprite->SetPosition(iXPos, 0);
 9:
10:   // Calculate the velocity so that it is aimed at one of the cities
11:   int iXVel, iYVel = (rand() % 4) + 3;
12:   switch(rand() % 4)
13:   {
14:   case 0:
15:     iXVel = (iYVel * (56 - (iXPos + 50))) / 400;
16:     break;
17:   case 1:
18:     iXVel = (iYVel * (240 - (iXPos + 50))) / 400;
19:     break;
20:   case 2:
21:     iXVel = (iYVel * (360 - (iXPos + 50))) / 400;
22:     break;
23:   case 3:
24:     iXVel = (iYVel * (546 - (iXPos + 50))) / 400;
25:     break;
26:   }
27:   pSprite->SetVelocity(iXVel, iYVel);
28:
29:   // Add the meteor sprite
30:   _pGame->AddSprite(pSprite);
31: }

The AddMeteor() function adds a new meteor to the game, and its code is probably a little longer than you expected simply because it tries to add meteors so that they are aimed at cities, as opposed to just zinging around the game screen aimlessly. Granted, a real meteor wouldn’t target a city; but this is a game, and the idea is to challenge the player by forcing them to save cities from incoming meteors. So, in the world of Meteor Defense, the meteors act more like incoming nuclear warheads in that they tend to target cities.

The function begins by creating a meteor sprite and setting it to a random position along the top edge of the game screen (lines 4–8). The big section of code in the middle of the function takes care of setting the meteor’s velocity so that it targets one of the four cities positioned along the bottom of the screen (lines 11–27). I realize that this code looks a little tricky, but all that’s going on is some basic trigonometry to figure out what velocity is required to get the meteor from point A to point B, where point A is the meteor’s random position and point B is the position of the city. The AddMeteor() function ends by adding the new meteor sprite to the game engine (line 30).

That wraps up the code for the Meteor Defense game, which hopefully doesn’t have your head spinning too much. Now you just need to put the resources together and you can start playing.

Testing the Game

It’s safe to congratulate yourself at this point because you’ve now worked through the design and development of the Meteor Defense game, and you can now bask in the glory of playing the game. Granted, playing a game for the first time is certainly more of a test than it is a true playing experience, but in this case I can vouch that the game works pretty well. Figure 19.2 shows the game at the start, with a couple of meteors hurtling toward cities.

The Meteor Defense game gets started with a couple of meteors hurtling at the cities.

Figure 19.2. The Meteor Defense game gets started with a couple of meteors hurtling at the cities.

Saving the cities from the meteors involves targeting the meteors and blasting them with missiles. When you successfully blast a meteor, you’ll see an explosion as the missile and meteor both are destroyed (see Figure 19.3)

Successfully destroying a meteor results in an explosion being displayed.

Figure 19.3. Successfully destroying a meteor results in an explosion being displayed.

When the game progresses as more meteors fall, you’ll eventually start losing cities to the meteors. Figure 19.4 shows an example of how the meteors can begin to overwhelm you late in the game.

As the game progresses, you tend to lose cities as the meteors start to overwhelm you.

Figure 19.4. As the game progresses, you tend to lose cities as the meteors start to overwhelm you.

When you finally lose all four cities to the meteors, the game ends. Figure 19.5 shows the end of the game, which involves displaying the game over image on the screen on top of the other game graphics.

When all four cities are destroyed, the game ends and the game over image is displayed.

Figure 19.5. When all four cities are destroyed, the game ends and the game over image is displayed.

Although it’s sad to think that you’ve ultimately failed to save the cities in the Meteor Defense game, you can sleep well knowing that you can always right-click the mouse to start over and take another stab at it.

Summary

This hour carried you through the design and construction of another complete game, Meteor Defense. The Meteor Defense game took advantage of the new game engine features you developed in the previous two hours, and also took a leap forward in terms of showing you how to create engaging action games. The game makes heavy use of sprites, including frame animation—not to mention a great deal of collision detection. Perhaps most important is the fact that the Meteor Defense game can serve as a great starting point for creating an action game of your own that takes full advantage of the game engine.

This hour concludes this part of the book. The next part of the book tackles artificial intelligence, or AI, which allows you to make your games more intelligent.

Q&A

Q1:

Why was it necessary to add the SpriteDying() function to the game engine?

A1:

The SpriteDying() function is important because it provides a means of carrying out a particular task in response to a sprite being destroyed. Keep in mind that the sprites in the game engine in many ways coexist in their own little world, and at times it can be difficult to interact with them because they are shielded from you. One of the limitations of the game engine prior to this hour was that there was no way to know when a sprite was being destroyed. The SpriteDying() function gives you a glimpse into the “sprite world” and lets you know whenever one of them is being killed. In the Meteor Defense game, this is important because it allows the game to create an explosion whenever a meteor is destroyed.

Q2:

How does the _iDifficulty global variable control the difficulty level of the Meteor Defense game?

A2:

The _iDifficulty global variable is used to determine how often new meteors are added. To make the game harder, you just add meteors more rapidly, which is carried out in the game by decreasing the value of the _iDifficulty variable in response to the score getting higher.

Workshop

The Workshop is designed to help you anticipate possible questions, review what you’ve learned, and begin learning how to put your knowledge into practice. The answers to the quiz can be found in Appendix A, “Quiz Answers.”

Quiz

1:

What’s the difference between a meteor, a meteorite, and a meteoroid?

2:

What is the purpose of the m_bOneCycle member variable in the Sprite class?

3:

How do you destroy a sprite and have it removed from the sprite list in the game engine?

Exercises

  1. Modify the calculation of the score and difficulty level so that the Meteor Defense game lasts a little longer and allows you to attain higher scores.

  2. Modify the game so that you lose points and temporarily lose the ability to fire from one of the guns if the gun is hit by a meteor. You don’t need to change the guns to sprites in order to add this feature—just use the position of the meteor when it is destroyed to determine if it hit one of the guns.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
13.58.51.36