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.

Listing 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.

Listing 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.

graphics/book.gif

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.

graphics/book.gif

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.



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