Chapter 11. Managing a World of Sprites

When you think about it, the real world we live in is all about actions and reactions. If you kick a ball, the ball will respond to the impact by traveling a certain distance, where it might collide with another object or eventually come to rest thanks to air and ground friction. The real world is therefore a system of objects that physically interact with one another. You can think of a system of sprites as a similar system of objects that are capable of interacting with each other in a variety of ways. The primary manner in which sprites can interact is through collisions, which involve objects running into each other. This hour focuses on the design and development of a sprite manager that allows you to establish actions and reactions within a system of sprites.

In this hour, you’ll learn:

  • Why sprite management is important to games

  • How to design a sprite manager

  • How to modify the game engine to support the management of sprites

  • How to eliminate animation flicker using a technique known as double buffering

  • How to build a program example that takes advantage of new sprite features such as collision detection

Assessing the Need for Sprite Management

In the previous hour, you developed a sprite class that modeled the basic physical properties of a graphical object that can move. You then created a program example called Fore that involved several golf ball sprites co-existing in the same space. Although the sprites in the Fore program were visually sharing the same space, no actual connection existed between them. Unlike the real world, the golf ball sprites were unable to collide with each other and respond accordingly—in other words, the sprites didn’t act very realistic. This limitation stems from the fact that the Sprite class alone can’t account for the relationship between sprites. You need a sprite manager that is capable of overseeing a system of sprites and managing their interactions.

The idea behind a sprite manager is to group all the sprites in a system together so that they can be collectively updated and drawn. Additionally, a sprite manager must be able to compare the positions of sprites to each other and determine if any collisions have taken place. If so, the sprite manager must then somehow notify the program that the collision has occurred; in which case, the program can respond accordingly. This approach to sprite collision management is incredibly important in games, which makes the sprite manager an absolute necessity toward building games that use sprite animation.

Another benefit of a sprite manager is that it provides a means of supporting an additional bounds action, Die. The Die bounds action causes a sprite to be destroyed if it encounters a boundary. This might be useful in a shoot-em-up game in which the bullet sprites need to be killed upon hitting the edge of the game screen. It’s difficult to support the Die bounds action directly in the Sprite class because the premise of the action is killing the sprite. This task is better left to an outside party whose job is to oversee all the sprites in a game—a sprite manager.

A moment ago, I mentioned that a sprite manager makes it possible to update and draw a system of sprites collectively. This is a significant feature as you move toward creating games that rely on several sprites. For example, it could quickly become a headache trying to update, draw, and generally keep tabs on 10 or 20 sprites. The sprite manager dramatically simplifies this situation by allowing you to simply update and draw all the sprites being managed at once, regardless of how many there are.

Designing a Sprite Manager

You now have a basic understanding of what is required of a sprite manager, so you can now move on to the specific design for it. You might think that the sprite manager would be created as a class similarly to the way that you created the Sprite class in the previous hour. However, the sprite manager is closely linked with the game engine, which makes it more beneficial to integrate the sprite manager directly with the game engine. So, the sprite manager will actually be created as a set of methods in the GameEngine class.

Even though the sprite manager is created as a modification on the game engine, it does require some changes outside of the GameEngine class. More specifically, some changes are required in the Sprite class in order for sprites to work smoothly with the sprite manager. The first of these changes involves supporting sprite actions, which are used to inform the sprite manager that it should take action in regard to a particular sprite. Sprite actions are sort of like bounds actions, except that they are somewhat more flexible. As an example, the first sprite action supported is Kill, which is used to inform the sprite manager that a sprite is to be destroyed. The Kill sprite action is similar to the Die bounds action, except that Kill can be issued for a variety of different reasons. Sprite actions are typically invoked when a collision occurs, which allows a missile to destroy a tank upon impact, for example.

Beyond sprite actions, another major requirement of the Sprite class and the sprite manager code is that of collision detection. You learned in Hour 9, “A Crash Course in Game Animation,” that collision detection involves checking to see if two sprites have collided with each other. You also found out that a technique known as shrunken rectangle collision detection involves using a rectangle smaller than the sprite as the basis for detecting collisions. Because this form of collision detection requires its own rectangle, it only makes sense to add a collision rectangle as a member of the Sprite class, along with supporting methods to calculate the rectangle and test for a collision with another sprite.

That covers the changes required of the Sprite class in order to support the enhanced sprite animation features offered by the sprite manager. The sprite manager itself is integrated directly into the game engine, where it primarily involves adding a member variable to keep track of a list of sprites. This member variable could be an array with a fixed size representing the maximum number of sprites allowed, or it could be a more advanced data structure such as a vector that can grow dynamically to hold additional sprites.

Regardless of the specifics of how the sprite list is established, the sprite manager must provide several methods that can be used to interact with the sprites being managed.

Following are the major tasks the sprite manager needs to make available using the following methods:

  • Add a new sprite to the sprite list

  • Draw all the sprites in the sprite list

  • Update all the sprites in the sprite list

  • Clean up all the sprites in the sprite list

  • Test to see if a point lies within a sprite in the sprite list

In addition to these tasks that must be capable of being invoked on the game engine, it is important to provide a function for a game that is called whenever a sprite collision occurs. When you think about it, handling a sprite collision is a very game-specific task, so it makes sense to let game code handle it, as opposed to including it in the game engine. So, a sprite collision notification function must be provided by any game that uses the sprite manager so that it can respond to sprite collisions. Of course, the sprite manager must make sure that this function gets called whenever a collision actually takes place.

Adding the Sprite Manager to the Game Engine

Throughout the hour thus far, I’ve drawn a distinction between the Sprite class and game engine, as if they were two different things. In reality, the Sprite class is part of the game engine even though it is a self-contained class. So, it’s safe to say that you are upgrading the game engine even when you make changes to the Sprite class. The next couple of sections reveal the code changes required in both the Sprite class and the GameEngine class to add support for a sprite manager.

Improving the Sprite Class

The first piece of code required in the Sprite class is the addition of a collision rectangle, which is used to determine if one sprite has collided with another. This rectangle is added as a member variable of the Sprite class named m_rcCollision, as the following code reveals:

RECT m_rcCollision;

A single accessor method is required for the collision rectangle so that the sprite manager can access the rectangle for collision detections. This method is called GetCollision(), and looks like the following:

RECT& GetCollision() { return m_rcCollision; };

Although there are no surprises with the GetCollision() method, you might find the CalcCollisionRect() method to be a little more interesting. This method is used internally by the Sprite class to calculate a collision rectangle based on the position rectangle. The CalcCollisionRect() method is defined as virtual in the Sprite class so that derived classes can override it and use their own specific collision rectangle calculation:

virtual void CalcCollisionRect();

Listing 11.1 shows the code for the CalcCollisionRect() method, which calculates the collision rectangle of a sprite by subtracting one-sixth of the sprite’s size off the position rectangle.

Example 11.1. The Sprite::CalcCollisionRect() Method Calculates a Collision Rectangle for a Sprite Based on the Sprite’s Position Rectangle

 1: inline void Sprite::CalcCollisionRect()
 2: {
 3:   int iXShrink = (m_rcPosition.left - m_rcPosition.right) / 12;
 4:   int iYShrink = (m_rcPosition.top - m_rcPosition.bottom) / 12;
 5:   CopyRect(&m_rcCollision, &m_rcPosition);
 6:   InflateRect(&m_rcCollision, iXShrink, iYShrink);
 7: }

This code is a little misleading because a shrink value for the X and Y dimensions of the sprite are first calculated as one-twelfth the size of the sprite (lines 3 and 4). These values are then passed into the Win32 InflateRect() function (line 6), which uses each value to shrink the sprite along each dimension. The end result is that the collision rectangle is one-sixth smaller than the position rectangle because the shrink values are applied to each side of the sprite.

Speaking of collision, the Sprite class provides a method called TestCollision() to see if the sprite has collided with another sprite:

BOOL TestCollision(Sprite* pTestSprite);

Listing 11.2 contains the code for the TestCollision() method, which simply checks to see if any part of the sprite’s collision rectangles overlap.

Example 11.2. The Sprite::TestCollision() Method Compares the Collision Rectangles of Two Sprites to See if They Overlap

 1: inline BOOL Sprite::TestCollision(Sprite* pTestSprite)
 2: {
 3:   RECT& rcTest = pTestSprite->GetCollision();
 4:   return m_rcCollision.left <= rcTest.right &&
 5:          rcTest.left <= m_rcCollision.right &&
 6:          m_rcCollision.top <= rcTest.bottom &&
 7:          rcTest.top <= m_rcCollision.bottom;
 8: }

If a collision has indeed occurred between the two sprites, the TestCollision() method returns TRUE; otherwise it returns FALSE (lines 4–7).

Getting back to the collision rectangle that was added to the Sprite class, it must be initialized in the Sprite() constructors. All three of these constructors include a call to CalcCollisionRect(), which sets the collision rectangle based on the position rectangle of the sprite. No other changes are required in the constructors to support collision detection in the Sprite class.

The other big change in the Sprite class involves the addition of sprite actions, which provide a means of allowing the sprite manager to manipulate sprites in response to events such as sprite collisions. A custom data type called SPRITEACTION is used to represent sprite actions, as follows:

typedef WORD        SPRITEACTION;
const SPRITEACTION  SA_NONE   = 0x0000L,
                    SA_KILL   = 0x0001L;

As you can see, only two sprite actions are defined for the SPRITEACTION data type, although the idea is to add new actions as necessary to expand the role of the sprite manager later. The SA_NONE sprite action indicates that nothing is to be done to any sprites. On the other hand, the SA_KILL sprite action indicates that a sprite is to be removed from the sprite list and destroyed. These sprite actions are given real meaning in the Update() method, which is now defined to return a SPRITEACTION value to indicate any actions to take with respect to the sprite.

The big change to the Update() method is that it now supports the BA_DIE bounds action, which causes a sprite to be destroyed when it encounters a boundary. This bounds action is made possible by the SA_KILL sprite action, which is returned by the Update() method in response to the BA_DIE bounds action occurring. So, the Update() method responds to the BA_DIE bounds action by returning SA_KILL, which results in the sprite being destroyed and removed from the sprite list. The remaining bounds actions return SA_NONE, which results in nothing happening to the sprite in terms of sprite actions.

Enhancing the Game Engine

The Sprite class is now whipped into shape in preparation for the new sprite manager support in the GameEngine class. Fortunately, managing a system of sprites isn’t really all that difficult of a proposition. This is largely possible thanks to a suite of data collections known as the Standard Template Library, or STL. The STL is a suite of data collection classes that can be used to store any kind of data, including sprites. Rather than use an array to store a list of sprites in the game engine, it is much more convenient and flexible to use the vector collection class from the STL. The STL vector class allows you to store away and manage a list of objects of any type, and then manipulate them using a set of handy methods. The good news is that you don’t have to know much about the vector class or the STL in order to put it to use in the game engine.

Note

Enhancing the Game Engine

The Standard Template Library is built in to most C++ compilers, and provides an extensive set of data collection classes that you can use in your programs. The STL is significant because it keeps you from having to spend time developing your own classes to perform common tasks. In other words, it saves you from having to reinvent the wheel.

The first step in using any data collection class in the STL is to properly include the header for the class, as well as its namespace. If you’ve never heard of namespaces, don’t worry because they don’t really impact the code you’re writing here. The following two lines must be placed near the top of the header file for the GameEngine class, and they take care of including the vector class header file and establishing its namespace:

#include <vector>
using namespace std;

To use an STL collection class such as the vector class, you simply declare a variable of type vector, but you also include the data type that you want stored in the vector inside angle brackets (<>). The following code shows how to create a vector of Sprite pointers:

vector<Sprite*> m_vSprites;

This code creates a vector containing Sprite pointers, and is exactly what you need in the game engine to keep track of a list of sprites. You can now use the m_vSprites vector to manage a list of sprites and interact with them as necessary. It helps to set a property on the vector variable so that it operates a little more efficiently in games. I’m referring to the amount of memory reserved for the vector, which determines how many sprite pointers can be stored in the vector before it has to allocate more memory. This doesn’t mean that you’re setting a limit on the number of sprites that can be stored in the vector; you’re just determining how often the vector class will have to allocate memory for new sprites. Because memory allocation takes time, it’s beneficial to keep it at a minimum. Given the requirements of most games, it’s safe to say that reserving room for fifty sprites before requiring additional memory allocation is sufficient. This memory reservation takes place in the GameEngine::GameEngine() constructor.

The sprite manager support in the game engine prompts you to add a new game function that must be provided by games as part of their game-specific code. This function is called SpriteCollision(), and its job is to respond to sprite collisions in a game-specific manner. Following is the function prototype for the SpriteCollision() function:

BOOL SpriteCollision(Sprite* pSpriteHitter, Sprite* pSpriteHittee);

Keep in mind that the SpriteCollision() function must be provided by each game that you create. The SpriteCollision() function is called by the CheckSpriteCollision() method within the game engine, which steps through the sprite list (vector) and checks to see if any sprites have collided:

BOOL CheckSpriteCollision(Sprite* pTestSprite);

The CheckSpriteCollision() method calls the SpriteCollision() function to handle individual sprite collisions. The CheckSpriteCollision() method steps through the entire list of sprites and checks for collisions between all of them. The first thing required to step through the sprite vector is an iterator, which is a special object used to move forward or backward through a vector. The good thing about iterators is that they are objects that provide functions for easily looping through a vector. For example, the begin() and end() iterator methods are used to establish a loop that steps through each sprite in the sprite vector. The code for the CheckSpriteCollision() method is included in the GameEngine.cpp source code file, which is available on the accompanying CD-ROM, along with all of the source code for the examples in the book.

Within the loop, a check is first performed to make sure that you aren’t comparing a sprite with itself. A collision test is then performed between the two sprites by calling the TestCollision() method. If a collision is detected, the SpriteCollision() function is called so that the game can respond appropriately to the collision. The return value of the SpriteCollision() function is also returned from the CheckSpriteCollision() method. This return value plays a vital role in determining how sprites react to collisions. More specifically, returning TRUE from CheckSpriteCollision() results in a sprite being restored to its original position prior to being updated, whereas a return value of FALSE allows the sprite to continue along its path. Without this mechanism for restoring the original position of a sprite, two sprites would tend to stick together instead of bouncing off each other when they collide. If there is no collision, FALSE is returned so that the sprite’s new position isn’t altered.

The CheckSpriteCollision() method is technically a helper method that is only used within the GameEngine class. It’s also necessary to add a suite of public sprite management methods to the GameEngine class that are used to interact with the sprite manager. Following are the sprite manager methods that can be called on the game engine:

void    AddSprite(Sprite* pSprite);
void    DrawSprites(HDC hDC);
void    UpdateSprites();
void    CleanupSprites();
Sprite* IsPointInSprite(int x, int y);

The AddSprite() method is used to add a sprite to the sprite list, and must be called in order for a sprite to be taken under management by the sprite manager. Before adding a sprite, the AddSprite() method checks to make sure the pSprite argument is not set to NULL. If the sprite pointer is okay, the sprite vector is checked to see if any sprites are already in it. If sprites are in the vector, the AddSprite() method has to find a suitable spot to add the sprite because the sprite list is ordered so that the sprites are drawn in proper Z-order. In other words, the sprites are ordered in the list according to increasing Z-order. This allows you to simply draw the sprites as they appear in the sprite list, and they will properly overlap each other naturally.

Note

Enhancing the Game Engine

You might have noticed that I’m using the terms list and vector somewhat interchangeably. This is because the list of sprites in the game engine is technically stored in a vector, but conceptually you can just think of it as a list. So, I may use one term or the other, but they are both referring to the same thing.

The DrawSprites() method is responsible for drawing all the sprites in the sprite list. The method does this by obtaining an iterator for the vector, and then using the iterator to step through the vector and draw each sprite. The Draw() method in the Sprite class is used to draw each sprite, which makes the process of drawing the entire list of sprites relatively simple.

Rivaling the DrawSprites() method in terms of importance is the UpdateSprites() method, which updates the position of each sprite. The critical consideration in this method is that it must be careful to retain the old position of the sprite in case it needs to restore the sprite to that position. An iterator is created that allows the method to step through the sprite vector and update each sprite individually. The sprite is updated with a call to the Update() method, which returns a sprite action.

The sprite action returned from the Sprite::Update() method is checked to see if it corresponds to the SA_KILL action, which requires the sprite manager to kill the sprite being updated. In order to successfully destroy the sprite, it is first deleted from memory and then removed from the sprite vector. If the SA_KILL sprite action wasn’t used on the sprite, the CheckSpriteCollision() method is called to see if the sprite has collided with any other sprites. The return value of this method determines whether the sprite’s old position is restored; TRUE means that it should be restored, whereas FALSE means that the new position should stand.

Another sprite manager method is CleanupSprites(), which is responsible for freeing sprites from memory and emptying the sprite vector. The CleanupSprites() method steps through the sprite vector and deletes each sprite in the vector. It also makes sure to remove each sprite from the vector right after it frees the sprite memory. It is important for any game to call the CleanupSprites() method so that sprites aren’t left hanging around in memory.

The last method in the GameEngine class pertaining to sprite management is the IsPointInSprite() method, which is used to see if a point lies within a sprite in the sprite list. This method is useful in situations in which you want to allow the user to click and somehow control a sprite. If the point lies within a sprite, the sprite is returned from the IsPointInSprite() method. Otherwise, NULL is returned, which indicates that the point doesn’t lie within any sprites.

Eliminating Flicker with Double Buffering

The sprite manager is now complete and ready to use within a program example. However, one bit of unfinished business needs to be addressed before pressing onward with an example. You might have noticed an annoying flicker in all the animated examples throughout the book thus far. This flicker is caused by the fact that the background image on the game screen is repainted before painting the animated graphics. In other words, animated graphics objects are erased and repainted each time they are moved. Because the erase and repaint process is taking place directly on the game screen, the image appears to flicker. To better understand the problem, imagine a movie in which a blank background is displayed quickly in between each frame containing actors that move. Although the film is cooking along at a fast enough pace to give the illusion of movement, you would still see a noticeable flicker because of the blank backgrounds.

The flicker problem associated with sprite animation can be solved using a technique known as double buffering. In double buffering, you perform all of your erasing and drawing on an offscreen drawing surface that isn’t visible to the user. After all the drawing is finished, the end result is painted straight to the game screen in one pass. Because no visible erasing is taking place, the end result is flicker-free animation. Figure 11.1 shows the difference between traditional single-buffer animation and double-buffer animation that eliminates flicker.

Double-buffer animation eliminates the annoying flicker associated with drawing directly to the game screen with a single buffer.

Figure 11.1. Double-buffer animation eliminates the annoying flicker associated with drawing directly to the game screen with a single buffer.

Note

Double-buffer animation eliminates the annoying flicker associated with drawing directly to the game screen with a single buffer.

A buffer is simply an area in memory to which you are drawing graphics. The buffer in traditional single-buffer animation is the game screen itself, whereas double-buffer animation adds an offscreen memory buffer to the equation.

Figure 11.1 reveals how an offscreen memory buffer is used to perform all the incremental animation drawing, with only the finished image being drawn to the game screen. This might sound like a tricky programming problem, but double-buffering is really not very hard to incorporate into your games. The first step is to add two global variables to keep track of the offscreen device context and bitmap that serve as the offscreen buffer. Following is an example of how to create these global variables:

HDC     _hOffscreenDC;
HBITMAP _hOffscreenBitmap;

With these variables in place, you need to create the offscreen device context and then use it to create an offscreen bitmap the same size as the game screen. The offscreen bitmap then needs to be selected into the offscreen device context, and you’re ready to go. The following code shows how these tasks are accomplished:

// Create the offscreen device context and bitmap
_hOffscreenDC = CreateCompatibleDC(GetDC(hWindow));
_hOffscreenBitmap = CreateCompatibleBitmap(GetDC(hWindow),
  _pGame->GetWidth(), _pGame->GetHeight());
SelectObject(_hOffscreenDC, _hOffscreenBitmap);

You now have an offscreen bitmap the same size as your game screen that is selected into an offscreen device context to which you can draw. The following code reveals how easy it is to use the offscreen device context and bitmap to add double-buffer support to the paint code in a game:

// Obtain a device context for repainting the game
HWND  hWindow = _pGame->GetWindow();
HDC   hDC = GetDC(hWindow);

// Paint the game to the offscreen device context
GamePaint(_hOffscreenDC);

// Blit the offscreen bitmap to the game screen
BitBlt(hDC, 0, 0, _pGame->GetWidth(), _pGame->GetHeight(),
  _hOffscreenDC, 0, 0, SRCCOPY);

// Cleanup
ReleaseDC(hWindow, hDC);

This code would fit in perfectly in a GameCycle() function. The familiar GamePaint() function is passed the offscreen device context, which means that all the game painting takes place offscreen. The resulting image is then painted, or blitted, to the game screen’s device context at once, which eliminates the possibility of flicker. Notice that this code is structured so that you don’t have to do anything special in the GamePaint() function.

It’s still important to clean up after yourself, and the following code shows how to clean up the offscreen bitmap and device context:

// Cleanup the offscreen device context and bitmap
DeleteObject(_hOffscreenBitmap);
DeleteDC(_hOffscreenDC);

Although the double-buffer code isn’t technically part of the sprite manager, it is nonetheless an important improvement to sprite animation, and a technique you should definitely use in all of your future sprite animation programs. The next sections build on what you’ve learned thus far in this hour to revamp the Fore program example from the previous hour to support the sprite manager and double-buffer animation.

Building the Fore 2 Program Example

If you recall from the previous hour, the Fore program example demonstrated how to use the Sprite class to create a few sprites and move them around on the game screen. You’re now going to enhance that program example a little by reworking it to support the new sprite management features built in to the game engine, as well as double-buffer animation. The new version of the Fore program is called Fore 2, and it serves as a great test bed for exploring the sprite features you’ve now added to the game engine.

Writing the Program Code

As with most programs created using the game engine, the best place to start with the code is the GameStart() function, which is used to initialize global variables and get everything in place. Listing 11.3 shows the code for the GameStart() and GameEnd() functions in the Fore 2 program.

Example 11.3. The GameStart() Function Initializes the Offscreen Buffer Variables and Adds Sprites to the Game Engine, Whereas the GameEnd() Function Cleans Up the Offscreen Buffer

 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:   _pForestBitmap = new Bitmap(hDC, IDB_FOREST, _hInstance);
15:   _pGolfBallBitmap = new Bitmap(hDC, IDB_GOLFBALL, _hInstance);
16:
17:   // Create the golf ball sprites
18:   RECT    rcBounds = { 0, 0, 600, 400 };
19:   Sprite* pSprite;
20:   pSprite = new Sprite(_pGolfBallBitmap, rcBounds, BA_WRAP);
21:   pSprite->SetVelocity(5, 3);
22:   _pGame->AddSprite(pSprite);
23:   pSprite = new Sprite(_pGolfBallBitmap, rcBounds, BA_WRAP);
24:   pSprite->SetVelocity(3, 2);
25:   _pGame->AddSprite(pSprite);
26:   rcBounds.left = 265; rcBounds.right = 500; rcBounds.bottom = 335;
27:   pSprite = new Sprite(_pGolfBallBitmap, rcBounds, BA_BOUNCE);
28:   pSprite->SetVelocity(-6, 5);
29:   _pGame->AddSprite(pSprite);
30:   rcBounds.right = 470;
31:   pSprite = new Sprite(_pGolfBallBitmap, rcBounds, BA_BOUNCE);
32:   pSprite->SetVelocity(7, -3);
33:   _pGame->AddSprite(pSprite);
34:
35:   // Set the initial drag info
36:   _pDragSprite = NULL;
37: }
38:
39: void GameEnd()
40: {
41:   // Cleanup the offscreen device context and bitmap
42:   DeleteObject(_hOffscreenBitmap);
43:   DeleteDC(_hOffscreenDC);
44:
45:   // Cleanup the bitmaps
46:   delete _pForestBitmap;
47:   delete _pGolfBallBitmap;
48:
49:   // Cleanup the sprites
50:   _pGame->CleanupSprites();
51:
52:   // Cleanup the game engine
53:   delete _pGame;
54: }

The first big change in the GameStart() function, as compared to its previous version, is the creation of the offscreen device context and bitmap (lines 7–10). The other change has to do with how the golf ball sprites are created. Rather than use a global array of sprites to keep track of the sprites, they are now just created and added to the game engine via the AddSprite() method (lines 22, 25, 29, and 33). Actually, there is an extra golf ball sprite in the Fore 2 program example, which is helpful in demonstrating the collision detection features now built in to the game engine.

You might notice that two of the sprites have their bounding rectangles set differently than the others. More specifically, the third sprite has its bounding rectangle diminished in size, which limits the area in which the sprite can travel (line 26). Similarly, the last sprite’s bounding rectangle is further reduced to limit its travel area even more (line 30). When you later test the program, you’ll see that these diminished bounding rectangles give the balls the effect of bouncing between trees in the forest background image.

The GameEnd() function is similar to the previous version except that it now cleans up the offscreen bitmap and device context that are required for double-buffer animation (lines 42 and 43).

One of the areas where the new sprite manager really simplifies things is in the GamePaint() function, which is shown in Listing 11.4.

Example 11.4. The GamePaint() Function Draws the Forest Background and All the Sprites in the Sprite List

 1: void GamePaint(HDC hDC)
 2: {
 3:   // Draw the background forest
 4:   _pForestBitmap->Draw(hDC, 0, 0);
 5:
 6:   // Draw the sprites
 7:   _pGame->DrawSprites(hDC);
 8: }

Notice in this code that the entire list of sprites is drawn using a single call to the DrawSprites() method in the game engine (line 7). This is a perfect example of how a little work in the game engine can really help make your game code easier to manage and understand.

Unlike the GamePaint() function, the GameCycle() function is a little more complex in the Fore 2 program than in its predecessor. However, as Listing 11.5 reveals, the new code consists solely of the familiar double-buffer code that you saw in the previous section.

Example 11.5. The GameCycle() Function Updates the Sprites in the Sprite List, and Then Draws Them to an Offscreen Memory Buffer Before Updating the Game Screen

 1: void GameCycle()
 2: {
 3:   // Update the sprites
 4:   _pGame->UpdateSprites();
 5:
 6:   // Obtain a device context for repainting the game
 7:   HWND  hWindow = _pGame->GetWindow();
 8:   HDC   hDC = GetDC(hWindow);
 9:
10:   // Paint the game to the offscreen device context
11:   GamePaint(_hOffscreenDC);
12:
13:   // Blit the offscreen bitmap to the game screen
14:   BitBlt(hDC, 0, 0, _pGame->GetWidth(), _pGame->GetHeight(),
15:     _hOffscreenDC, 0, 0, SRCCOPY);
16:
17:   // Cleanup
18:   ReleaseDC(hWindow, hDC);
19: }

The GameCycle() function first updates the sprites in the sprite list with a call to the game engine’s UpdateSprites() method (line 4). The remainder of the code in the function should look familiar to you because it is identical to the code you saw earlier when you learned about double-buffer animation. The GamePaint() method is called to paint the game graphics to the offscreen device context (line 11). The offscreen image is then blitted to the game screen’s device context to finish the painting (lines 14 and 15).

If you recall from the previous hour, the left mouse button can be used to click and drag a golf ball sprite around on the game screen. Listing 11.6 contains the code for the three mouse functions that make sprite dragging possible.

Example 11.6. The MouseButtonDown(), MouseButtonUp(), and MouseMove() Functions Use New Game Engine Sprite Manager Features to Simplify the Process of Dragging a Sprite Around the Game Screen

 1: void MouseButtonDown(int x, int y, BOOL bLeft)
 2: {
 3:   // See if a ball was clicked with the left mouse button
 4:   if (bLeft && (_pDragSprite == NULL))
 5:   {
 6:     if ((_pDragSprite = _pGame->IsPointInSprite(x, y)) != NULL)
 7:     {
 8:       // Capture the mouse
 9:       SetCapture(_pGame->GetWindow());
10:
11:       // Simulate a mouse move to get started
12:       MouseMove(x, y);
13:     }
14:   }
15: }
16:
17: void MouseButtonUp(int x, int y, BOOL bLeft)
18: {
19:   // Release the mouse
20:   ReleaseCapture();
21:
22:   // Stop dragging
23:   _pDragSprite = NULL;
24: }
25:
26: void MouseMove(int x, int y)
27: {
28:   if (_pDragSprite != NULL)
29:   {
30:     // Move the sprite to the mouse cursor position
31:     _pDragSprite->SetPosition(x - (_pDragSprite->GetWidth() / 2),
32:       y - (_pDragSprite->GetHeight() / 2));
33:
34:     // Force a repaint to redraw the sprites
35:     InvalidateRect(_pGame->GetWindow(), NULL, FALSE);
36:   }
37: }

The mouse functions in Fore 2 are simplified from their previous versions thanks to the new and improved game engine. For example, the MouseButtonDown() function now relies on the IsPointInSprite() method in the game engine to check and see if the mouse position is located within a sprite (line 6). The other two mouse functions are very similar to their previous counterparts, except that they now rely on a sprite pointer to keep track of the drag sprite, as opposed to an index into an array of sprites. For example, notice that when the mouse button is released, the _pDragSprite pointer is set to NULL (line 23). Similarly, the same pointer is used to set the position of the drag sprite in the MouseButtonUp() function (lines 31 and 32).

The last function in the Fore 2 program example is the SpriteCollision() function, which is called whenever two sprites collide with each other. Listing 11.7 contains the code for this function.

Example 11.7. The SpriteCollision() Function Swaps the Velocities of Sprites That Collide, Which Makes Them Appear to Bounce Off of Each Other

 1: BOOL SpriteCollision(Sprite* pSpriteHitter, Sprite* pSpriteHittee)
 2: {
 3:   // Swap the sprite velocities so that they appear to bounce
 4:   POINT ptSwapVelocity = pSpriteHitter->GetVelocity();
 5:   pSpriteHitter->SetVelocity(pSpriteHittee->GetVelocity());
 6:   pSpriteHittee->SetVelocity(ptSwapVelocity);
 7:   return TRUE;
 8: }

The SpriteCollision() function receives the two sprites that collided as its only arguments (line 1). The function handles the collision by swapping the velocities of the sprites (lines 4–6). This has the effect of making the sprites appear to bounce off of each other and reverse direction. Notice that the SpriteCollision() function returns TRUE at the end to indicate that the sprites should be restored to their old positions prior to the collision (line 7).

Testing the Finished Product

The improvements you made in the Fore 2 program example are somewhat subtle, but they are significant in terms of adding functionality to the game engine that is required to create real games. For example, it is critical that you be able to detect collisions between sprites and react accordingly. The collision detection support in the game engine now makes it very easy to tell when two sprites have collided, and then take appropriate action. Although it’s hard to show sprite collisions in a still image, Figure 11.2 shows the Fore 2 program example in action.

The golf ball sprites in the Fore 2 program move around and bounce off of each other thanks to the new and improved sprite management features in the game engine.

Figure 11.2. The golf ball sprites in the Fore 2 program move around and bounce off of each other thanks to the new and improved sprite management features in the game engine.

If you pay close attention to the sprites, you’ll notice that two of them appear to bounce between trees in the background. These two sprites are the ones whose bounding rectangles were reduced to limit their movement. You can see that bounding rectangles provide a simple yet effective way to limit the movement of sprites. Keep in mind that you can still click and drag any of the sprites with the left mouse button. Now that the sprites are sensitive to collisions, dragging them around with the mouse is considerably more interesting.

Summary

Sprites are undoubtedly a critical part of two-dimensional game programming because they allow you to create graphical objects that can move around independently of a background image. Not only that, but sprites can be designed so that they reside together in a system in which they can interact with one another. Most games represent a model of some kind of physical system, so a system of sprites becomes a good way of simulating a physical system in a game. This hour built on the sprite code that you developed in the previous hour by pulling sprites together into a system that is managed within the game engine. By actively managing the sprites in the game engine, you’re able to ensure that they are layered properly according to Z-order, as well handle collisions between them.

Moving into the next hour, you’ll quickly realize how important the new sprite features are to games. The next hour guides you through the development of a complete game called Henway, that is sort of a takeoff on the classic Frogger arcade game. You’ll be using your newfound sprite knowledge to the maximum as you build Henway, so get ready!

Q&A

Q1:

Why isn’t the sprite manager created as its own class similar to Sprite?

A1:

Although the sprite manager code has been created in a class of its own, its tight integration with the game engine made it simpler to just place the sprite management code directly in the engine. If the code resided in a separate class, you’d have to do a fair amount of work making sure that the game engine could communicate with the sprite manager, and vice versa. This is a situation in which it’s easier to forego a strict OOP approach of sticking everything in its own class, with the benefit being a more simplistic design.

Q2:

I’m still having trouble understanding how the AddSprite() function affects the order of sprites in the sprite list according to their Z-order. What gives?

A2:

It works like this: The Z-order of a sprite determines its depth on the screen, with higher Z-order values resulting in sprites that are more visible. In other words, a sprite with a Z-order of 3 would appear to be sitting on top of a sprite with a Z-order of 2. The practical way to achieve this effect is to draw the topmost sprite last. Or to put it another way, you draw all the sprites in order of increasing Z-order, which naturally means that the higher Z-order sprites appear to be on top of the others. The AddSprite() function enforces this system by making sure that the sprite list remains sorted by increasing Z-order as new sprites are added. The Z-order of the sprites is then automatically factored in when drawing the sprites because the sprite list is already sorted accordingly.

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 is the purpose of the vector class in the Standard Template Library?

2:

What is the purpose of the SA_KILL sprite action?

3:

What is double buffering, and why is it important?

Exercises

  1. Modify the SpriteCollision() function in the Fore 2 example program so that the velocities of the sprites are not only swapped, but also increased slightly with each collision. Then run the program and notice how the balls speed up with each new collision.

  2. Create a couple of new sprites in the Fore 2 program example that use an image other than the golf ball image. Pay close attention to how you set the Z-order of the new sprites, and then watch as they move with respect to the golf ball sprites. You should quickly be able to tell how useful Z-order can be in providing depth to games.

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

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