Building the Roids 2 Program Example


Rather than modify an existing game to demonstrate AI programming, I decided that it would be better to demonstrate AI within the context of a program example that doesn't involve an objective. In other words, I wanted to create a program that you could tinker with without worrying about messing up the outcome of a game. The program I'm talking about is called Roids 2, and it's a revamped version of the Roids program from Hour 18. If you recall, the original Roids program displayed an animated asteroid field. You're now going to add a flying saucer to the program that is intelligent enough to dodge the asteroids, or at least do its best to dodge the asteroids .

The Roids 2 program example is very similar to the original Roids program, except for the addition of the flying saucer sprite. The remainder of the hour focuses on the development of this program, and how AI influences the flying saucer sprite.

Writing the Program Code

The Roids 2 program begins with the Roids.h header file, which declares global variables that are important to the program. More specifically , a flying saucer bitmap has been declared, along with sprites for the asteroids and the flying saucer:

 Bitmap*           _pSaucerBitmap; Sprite*           _pAsteroids[3]; Sprite*           _pSaucer; 

The _pSaucerBitmap is a bitmap for the flying saucer image. The _pAsteroids and _pSaucer variables both store sprite pointers. These pointers are necessary so that you can compare the positions of the saucer and asteroids and alter the saucer's velocity; this is how you add "intelligence" to the flying saucer.

The Roids 2 program also includes a new helper function named UpdateSaucer() , which is responsible for updating the saucer sprite. Of course, the saucer sprite is already being updated in terms of its position and velocity in the game engine. However, in this case an additional update is taking place that alters the saucer's velocity based on its proximity to nearby asteroids. You learn exactly how this facet of the program works a little later in the hour.

The GameStart() function is similar to the previous version, except that it now contains code to initialize the flying saucer. Listing 20.1 shows the code for this function.

Listing 20.1 The GameStart() Function Initializes the Flying Saucer Bitmap and Sprite
 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 asteroid and saucer bitmaps 13:   HDC hDC = GetDC(hWindow); 14:   _pAsteroidBitmap = new Bitmap(hDC, IDB_ASTEROID, _hInstance); 15:   _pSaucerBitmap = new Bitmap(hDC, IDB_SAUCER, _hInstance); 16: 17:   // Create the starry background 18:   _pBackground = new StarryBackground(500, 400); 19: 20:   // Create the asteroid sprites 21:   RECT    rcBounds = { 0, 0, 500, 400 }; 22:   _pAsteroids[0] = new Sprite(_pAsteroidBitmap, rcBounds, BA_WRAP); 23:   _pAsteroids[0]->SetNumFrames(14); 24:   _pAsteroids[0]->SetFrameDelay(1); 25:   _pAsteroids[0]->SetPosition(250, 200); 26:   _pAsteroids[0]->SetVelocity(-3, 1); 27:   _pGame->AddSprite(_pAsteroids[0]); 28:   _pAsteroids[1] = new Sprite(_pAsteroidBitmap, rcBounds, BA_WRAP); 29:   _pAsteroids[1]->SetNumFrames(14); 30:   _pAsteroids[1]->SetFrameDelay(2); 31:   _pAsteroids[1]->SetPosition(250, 200); 32:   _pAsteroids[1]->SetVelocity(3, -2); 33:   _pGame->AddSprite(_pAsteroids[1]); 34:   _pAsteroids[2] = new Sprite(_pAsteroidBitmap, rcBounds, BA_WRAP); 35:   _pAsteroids[2]->SetNumFrames(14); 36:   _pAsteroids[2]->SetFrameDelay(3); 37:   _pAsteroids[2]->SetPosition(250, 200); 38:   _pAsteroids[2]->SetVelocity(-2, -4); 39:   _pGame->AddSprite(_pAsteroids[2]); 40: 41:   // Create the saucer sprite 42:   _pSaucer = new Sprite(_pSaucerBitmap, rcBounds, BA_WRAP); 43:   _pSaucer->SetPosition(0, 0); 44:   _pSaucer->SetVelocity(3, 1); 45:   _pGame->AddSprite(_pSaucer); 46: } 

The changes to the GameStart() function primarily involve the addition of the flying saucer sprite. The saucer bitmap is first loaded (line 15), and then the saucer sprite is created and added to the game engine (lines 42 “45). It's also worth pointing out that the asteroid sprite pointers are now being stored in the _pAsteroids array (lines 22, 28, and 34) because you need to reference them later when helping the saucer avoid hitting the asteroids.

The GameCycle() function in Roids 2 requires a slight modification to ensure that the flying saucer sprite is updated properly. This change involves the addition of a call to the UpdateSaucer() function, which is responsible for updating the velocity of the flying saucer sprite to help it dodge the asteroids.

Speaking of the UpdateSaucer() function, its code is shown in Listing 20.2.

Listing 20.2 The UpdateSaucer() Function Updates the Flying Saucer's Velocity to Help It Dodge Asteroids
 1: void UpdateSaucer()  2: {  3:   // Obtain the saucer's position  4:   RECT rcSaucer, rcRoid;  5:   rcSaucer = _pSaucer->GetPosition();  6:  7:   // Find out which asteroid is closest to the saucer  8:   int iXCollision = 500, iYCollision = 400, iXYCollision = 900;  9:   for (int i = 0; i < 3; i++) 10:   { 11:     // Get the asteroid position 12:     rcRoid = _pAsteroids[i]->GetPosition(); 13: 14:     // Calculate the minimum XY collision distance 15:     int iXCollisionDist = (rcSaucer.left + 16:       (rcSaucer.right - rcSaucer.left) / 2) - 17:       (rcRoid.left + 18:       (rcRoid.right - rcRoid.left) / 2); 19:     int iYCollisionDist = (rcSaucer.top + 20:       (rcSaucer.bottom - rcSaucer.top) / 2) - 21:       (rcRoid.top + 22:       (rcRoid.bottom - rcRoid.top) / 2); 23:     if ((abs(iXCollisionDist) < abs(iXCollision))  24:       (abs(iYCollisionDist) < abs(iYCollision))) 25:       if ((abs(iXCollisionDist) + abs(iYCollisionDist)) < iXYCollision) 26:       { 27:         iXYCollision = abs(iXCollision) + abs(iYCollision); 28:         iXCollision = iXCollisionDist; 29:         iYCollision = iYCollisionDist; 30:       } 31:   } 32: 33:   // Move to dodge the asteroids, if necessary 34:   POINT ptVelocity; 35:   ptVelocity = _pSaucer->GetVelocity(); 36:   if (abs(iXCollision) < 60) 37:   { 38:     // Adjust the X velocity 39:     if (iXCollision < 0) 40:       ptVelocity.x = max(ptVelocity.x - 1, -8); 41:     else 42:       ptVelocity.x = min(ptVelocity.x + 1, 8); 43:   } 44:   if (abs(iYCollision) < 60) 45:   { 46:     // Adjust the Y velocity 47:     if (iYCollision < 0) 48:       ptVelocity.y = max(ptVelocity.y - 1, -8); 49:     else 50:       ptVelocity.y = min(ptVelocity.y + 1, 8); 51:   } 52: 53:   // Update the saucer to the new position 54:   _pSaucer->SetVelocity(ptVelocity); 55: } 

I realize that this function contains a lot of code, but if you take it a section at a time, it's really not too complex. First of all, let's understand how the UpdateSaucer() function is helping the flying saucer to dodge the asteroids. The idea is to check for the closest asteroid in relation to the saucer, and then alter the saucer's velocity so that it has a tendency to move in the opposite direction of the asteroid. I say "tendency" because you don't want the saucer to jerk and immediately start moving away from the asteroid. Instead, you gradually alter its velocity so that it appears to steer away from the asteroid. This is a subtle difference, but the effect is dramatic because it looks as if the saucer is truly steering through the asteroid field.

The first step in the UpdateSaucer() function is to obtain the position of the flying saucer (line 5). You can then loop through the asteroids and find out the minimum X and Y collision distance (lines 8 and 9), which is the closest distance between an asteroid and the saucer. Inside the loop, the asteroid position is first obtained (line 12), which is critical for determining the collision distance. The minimum XY collision distance is then calculated (lines 15 “22) and used as the basis for determining if this asteroid is currently the closest one to the saucer. This is where the function gets a little tricky because you must add the X and Y components of the collision distance to see which asteroid is closer to the saucer (lines 23 “30). This technique isn't flawless, but it helps to eliminate "false alarm" situations in which an asteroid is close to the saucer horizontally but far away vertically.

When the asteroid loop is exited, you have two pieces of important information to work with: the X collision distance and the Y collision distance. It's now possible to check and see if these distances are below a certain minimum distance that is required in order for the saucer to be in danger of colliding with an asteroid. My own trial-and-error testing led to a value of 60 , but you might decide on a slightly different value in your own testing. In order to steer the saucer to safety horizontally, the X collision distance is checked to see if it is below the minimum distance of 60 ; in which case, the saucer's velocity is adjusted (lines 36 “43). The same process is then repeated for the saucer's Y collision distance (lines 44 “51). Finally, the new velocity of the saucer is set by calling the SetVelocity() method on the saucer sprite (line 54).

Testing the Finished Product

The premise behind the Roids 2 program is to show how a certain degree of AI can be injected into a sprite object so that it can avoid other sprite objects. In this context, the evading sprite object is a flying saucer that is desperately trying to keep from colliding with asteroids in an asteroid field. Although you have to actually run the program yourself to see the flying saucer evade the asteroids, Figure 20.3 shows the saucer in the process of steering away from a close call.

Figure 20.3. The flying saucer in the Roids 2 program does its best to dodge the asteroids that are floating around the game screen.

graphics/20fig03.gif

This is one of those rare program examples that is actually fun to just sit back and watch because the program appears to have a mind of its own. In other words, you've created the logic for the flying saucer, and now you get to see how it responds in different situations. This is the part of the process of developing AI in games that is often frustrating because objects can do strange things that are quite unexpected at times. Even so, the flying saucer does a pretty good job of avoiding asteroids in the Roids 2 program.



Sams Teach Yourself Game Programming in 24 Hours
Sams Teach Yourself Game Programming in 24 Hours
ISBN: 067232461X
EAN: 2147483647
Year: 2002
Pages: 271

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net