Creating the Sprite Class


Creating the Sprite Class

The Sprite class is designed to model a single sprite that uses the familiar Bitmap class to represent its appearance. Listing 10.1 contains the code for the Sprite class definition, which shows the overall design of the Sprite class, including its member variables and methods .

Listing 10.1 The Sprite Class Definition Shows How the Design of a Game Sprite Is Realized in Code
 1: class Sprite  2: {  3: protected:  4:   // Member Variables  5:   Bitmap*       m_pBitmap;  6:   RECT          m_rcPosition;  7:   POINT         m_ptVelocity;  8:   int           m_iZOrder;  9:   RECT          m_rcBounds; 10:   BOUNDSACTION  m_baBoundsAction; 11:   BOOL          m_bHidden; 12: 13: public: 14:   // Constructor(s)/Destructor 15:   Sprite(Bitmap* pBitmap); 16:   Sprite(Bitmap* pBitmap, RECT& rcBounds, 17:     BOUNDSACTION baBoundsAction = BA_STOP); 18:   Sprite(Bitmap* pBitmap, POINT ptPosition, POINT ptVelocity, int iZOrder, 19:     RECT& rcBounds, BOUNDSACTION baBoundsAction = BA_STOP); 20:   virtual ~Sprite(); 21: 22:   // General Methods 23:   virtual void  Update(); 24:   void          Draw(HDC hDC); 25:   BOOL          IsPointInside(int x, int y); 26: 27:   // Accessor Methods 28:   RECT&   GetPosition()             { return m_rcPosition; }; 29:   void    SetPosition(int x, int y); 30:   void    SetPosition(POINT ptPosition); 31:   void    SetPosition(RECT& rcPosition) 32:     { CopyRect(&m_rcPosition, &rcPosition); }; 33:   void    OffsetPosition(int x, int y); 34:   POINT   GetVelocity()             { return m_ptVelocity; }; 35:   void    SetVelocity(int x, int y); 36:   void    SetVelocity(POINT ptVelocity); 37:   BOOL    GetZOrder()               { return m_iZOrder; }; 38:   void    SetZOrder(int iZOrder)    { m_iZOrder = iZOrder; }; 39:   void    SetBounds(RECT& rcBounds) { CopyRect(&m_rcBounds, &rcBounds); }; 40:   void    SetBoundsAction(BOUNDSACTION ba) { m_baBoundsAction = ba; }; 41:   BOOL    IsHidden()                { return m_bHidden; }; 42:   void    SetHidden(BOOL bHidden)   { m_bHidden = bHidden; }; 43:   int     GetWidth()                { return m_pBitmap->GetWidth(); }; 44:   int     GetHeight()               { return m_pBitmap->GetHeight(); }; 45: }; 

You might notice that the member variables for the Sprite class (lines 5 “11) correspond one-to-one with the sprite properties you learned about in the previous section. The only real surprise in these variables is the use of the BOUNDSACTION data type (line 10), which is a custom data type that you learn about in a moment. This data type is used to describe the bounds action for the sprite.

The Sprite class offers several constructors that require differing amounts of information in order to create a sprite (lines 15 “19), as well as a destructor that you can use to clean up after the sprite (line 20). There are three general methods in the Sprite class that are extremely important when it comes to using the Sprite class. The first of these is Update() , which updates the sprite by applying its velocity to its position and carrying out any appropriate reactions to the sprite movement (line 23). Next , the Draw() method is responsible for drawing the sprite at its current position using the bitmap that was specified in one of the Sprite() constructors (line 24). Finally, the IsPointInside() method is used to see if a point is located within the sprite's position rectangle (line 25). This method basically performs a hit test , which is useful if you want to determine if the sprite has been clicked with the mouse.

The remaining methods in the Sprite class are accessor methods that get and set various properties of the sprite. Some of these methods come in multiple versions to make it more convenient to interact with sprites . For example, the SetPosition() methods allow you to set the position of a sprite using individual X and Y values, a point, or a rectangle (lines 29 “32). You might notice that most of the accessor methods include their code directly in the class definition, whereas a few of those do not. The accessor methods whose code isn't directly included next to the method definition are defined as inline methods , and their code appears in the Sprite.h header file below the Sprite class definition.

Earlier, I mentioned that a custom data type called BOUNDSACTION was used as the data type for the m_baBoundsAction member variable. This custom data type is defined in the Sprite.h header file as the following:

 typedef WORD        BOUNDSACTION; const BOUNDSACTION  BA_STOP   = 0,                     BA_WRAP   = 1,                     BA_BOUNCE = 2,                     BA_DIE    = 3; 

If you recall, the bounds actions described in the BOUNDSACTION data type correspond directly to those that were mentioned in the previous section when I first explained bounds actions and how they work. The idea here is that you use one of these constants to tell a sprite how it is to react when it runs into a boundary. If you're creating an Asteroids game, you'd want to use the BA_WRAP constant for the asteroids. On the other hand, a game like Breakout would rely on the BA_BOUNCE constant to allow the ball to bounce off the edges of the game screen. Regardless of which bounds action you choose for a sprite, it is entirely dependent on the bounding rectangle you set for the sprite. This rectangle can be as large as the game screen or as small as the sprite itself, although it wouldn't make much sense to bound a sprite with a rectangle the same size of the sprite.

Creating and Destroying the Sprite

You're probably ready to learn some more about how the Sprite class is actually put together. Listing 10.2 contains the code for the three Sprite() constructors, as well as the Sprite() destructor.

Listing 10.2 The Sprite::Sprite() Constructors and Destructor Are Used to Create and Clean up After Sprites
 1: Sprite::Sprite(Bitmap* pBitmap)  2: {  3:   // Initialize the member variables  4:   m_pBitmap = pBitmap;  5:   SetRect(&m_rcPosition, 0, 0, pBitmap->GetWidth(), pBitmap->GetHeight());  6:   m_ptVelocity.x = m_ptVelocity.y = 0;  7:   m_iZOrder = 0;  8:   SetRect(&m_rcBounds, 0, 0, 640, 480);  9:   m_baBoundsAction = BA_STOP; 10:   m_bHidden = FALSE; 11: } 12: 13: Sprite::Sprite(Bitmap* pBitmap, RECT& rcBounds, BOUNDSACTION baBoundsAction) 14: { 15:   // Calculate a random position 16:   int iXPos = rand() % (rcBounds.right - rcBounds.left); 17:   int iYPos = rand() % (rcBounds.bottom - rcBounds.top); 18: 19:   // Initialize the member variables 20:   m_pBitmap = pBitmap; 21:   SetRect(&m_rcPosition, iXPos, iYPos, iXPos + pBitmap->GetWidth(), 22:     iYPos + pBitmap->GetHeight()); 23:   m_ptVelocity.x = m_ptVelocity.y = 0; 24:   m_iZOrder = 0; 25:   CopyRect(&m_rcBounds, &rcBounds); 26:   m_baBoundsAction = baBoundsAction; 27:   m_bHidden = FALSE; 28: } 29: 30: Sprite::Sprite(Bitmap* pBitmap, POINT ptPosition, POINT ptVelocity, 31:     int iZOrder, RECT& rcBounds, BOUNDSACTION baBoundsAction) 32: { 33:   // Initialize the member variables 34:   m_pBitmap = pBitmap; 35:   SetRect(&m_rcPosition, ptPosition.x, ptPosition.y, pBitmap->GetWidth(), 36:     pBitmap->GetHeight()); 37:   m_ptVelocity = ptPosition; 38:   m_iZOrder = iZOrder; 39:   CopyRect(&m_rcBounds, &rcBounds); 40:   m_baBoundsAction = baBoundsAction; 41:   m_bHidden = FALSE; 42: } 43: 44: Sprite::~Sprite() 45: { 46: } 

The first Sprite() constructor accepts a single argument, a pointer to a Bitmap object, and uses default values for the remainder of the sprite properties (lines 1 “11). Although this constructor can work if you're in a hurry to create a sprite, you'll probably want to use a more detailed constructor to have more control over the sprite. The second Sprite() constructor adds a bounding rectangle and bounds action to the Bitmap pointer, and uses them to help further define the sprite (lines 13 “28). The interesting thing about this constructor is that it randomly positions the sprite within the bounding rectangle (lines 16, 17, 21, and 22). The third constructor is the most useful because it gives you the most control over creating a new sprite (lines 30 “42). The Sprite() destructor doesn't do anything, but it's there to provide a means of adding cleanup code later should you need it (lines 44 “46).

Updating the Sprite

There are only two methods in the Sprite class that you haven't seen the code for yet: Update() and Draw() . It turns out that these are the two most important methods in the class. Listing 10.3 contains the code for the Update() method, which is responsible for updating the sprite.

Listing 10.3 The Sprite::Update() Method Updates a Sprite by Changing Its Position Based on Its Velocity and Taking Action in Response to the Movement
 1: void Sprite::Update()  2: {  3:   // Update the position  4:   POINT ptNewPosition, ptSpriteSize, ptBoundsSize;  5:   ptNewPosition.x = m_rcPosition.left + m_ptVelocity.x;  6:   ptNewPosition.y = m_rcPosition.top + m_ptVelocity.y;  7:   ptSpriteSize.x = m_rcPosition.right - m_rcPosition.left;  8:   ptSpriteSize.y = m_rcPosition.bottom - m_rcPosition.top;  9:   ptBoundsSize.x = m_rcBounds.right - m_rcBounds.left; 10:   ptBoundsSize.y = m_rcBounds.bottom - m_rcBounds.top; 11: 12:   // Check the bounds 13:   // Wrap? 14:   if (m_baBoundsAction == BA_WRAP) 15:   { 16:     if ((ptNewPosition.x + ptSpriteSize.x) < m_rcBounds.left) 17:       ptNewPosition.x = m_rcBounds.right; 18:     else if (ptNewPosition.x > m_rcBounds.right) 19:       ptNewPosition.x = m_rcBounds.left - ptSpriteSize.x; 20:     if ((ptNewPosition.y + ptSpriteSize.y) < m_rcBounds.top) 21:       ptNewPosition.y = m_rcBounds.bottom; 22:     else if (ptNewPosition.y > m_rcBounds.bottom) 23:       ptNewPosition.y = m_rcBounds.top - ptSpriteSize.y; 24:   } 25:   // Bounce? 26:   else if (m_baBoundsAction == BA_BOUNCE) 27:   { 28:     BOOL bBounce = FALSE; 29:     POINT ptNewVelocity = m_ptVelocity; 30:     if (ptNewPosition.x < m_rcBounds.left) 31:     { 32:       bBounce = TRUE; 33:       ptNewPosition.x = m_rcBounds.left; 34:       ptNewVelocity.x = -ptNewVelocity.x; 35:     } 36:     else if ((ptNewPosition.x + ptSpriteSize.x) > m_rcBounds.right) 37:     { 38:       bBounce = TRUE; 39:       ptNewPosition.x = m_rcBounds.right - ptSpriteSize.x; 40:       ptNewVelocity.x = -ptNewVelocity.x; 41:     } 42:     if (ptNewPosition.y < m_rcBounds.top) 43:     { 44:       bBounce = TRUE; 45:       ptNewPosition.y = m_rcBounds.top; 46:       ptNewVelocity.y = -ptNewVelocity.y; 47:     } 48:     else if ((ptNewPosition.y + ptSpriteSize.y) > m_rcBounds.bottom) 49:     { 50:       bBounce = TRUE; 51:       ptNewPosition.y = m_rcBounds.bottom - ptSpriteSize.y; 52:       ptNewVelocity.y = -ptNewVelocity.y; 53:     } 54:     if (bBounce) 55:       SetVelocity(ptNewVelocity); 56:   } 57:   // Stop (default) 58:   else 59:   { 60:     if (ptNewPosition.x  < m_rcBounds.left  61:       ptNewPosition.x > (m_rcBounds.right - ptSpriteSize.x)) 62:     { 63:       ptNewPosition.x = max(m_rcBounds.left, min(ptNewPosition.x, 64:         m_rcBounds.right - ptSpriteSize.x)); 65:       SetVelocity(0, 0); 66:     } 67:     if (ptNewPosition.y  < m_rcBounds.top  68:       ptNewPosition.y > (m_rcBounds.bottom - ptSpriteSize.y)) 69:     { 70:       ptNewPosition.y = max(m_rcBounds.top, min(ptNewPosition.y, 71:         m_rcBounds.bottom - ptSpriteSize.y)); 72:       SetVelocity(0, 0); 73:     } 74:   } 75:   SetPosition(ptNewPosition); 76: } 

This method is probably considerably longer than you expected it to be, but on closer inspection you'll realize that it is doing several important things. The primary purpose of the Update() method is to use the velocity of the sprite to alter its position, which has the effect of moving the sprite. However, simply changing the position of the sprite isn't good enough because you have to take into consideration what happens if the sprite runs into a boundary. If you recall, every sprite has a bounding rectangle that determines the area in which the sprite can move. A sprite also has a bounds action that determines what happens to the sprite when it runs into a boundary. The Update() method has to check for a boundary and then take the appropriate response based on the sprite's bounds action.

The Update() method begins by making some temporary calculations involving the new position, the size of the sprite, and the size of the boundary (lines 4 “10). The rest of the method handles each kind of bounds action, beginning with BA_WRAP . To handle the BA_WRAP bounds action, the sprite is simply moved to the opposite side of the bounding rectangle (lines 14 “24), which gives the effect of the sprite wrapping off one side and on to the other. The BA_BOUNCE action has to look a little closer at which boundary the sprite is crossing because it must correctly reverse the sprite's velocity in order to yield a bouncing effect (lines 26 “56). The final bounds action handled in the Update() method is BA_STOP , which is actually unnamed in this case because it is the default bounds action. This bounds action ensures that the sprite doesn't cross over the boundary, while setting the sprite's velocity to zero (lines 59 “74).

Throughout all the bounds action handling code, the new sprite position is calculated and stored in a temporary variable of type POINT , ptNewPosition . At the end of the Update() method, this variable is used to actually set the new position of the sprite (line 75).

graphics/book.gif

If you're the overly observant type, you might recall that earlier in the hour the Sprite class was designed to support an additional bounds action, BA_DIE , which causes the sprite to be destroyed when it encounters a boundary. Although this bounds action is technically available for the Sprite class to use, it isn't possible to support the action without some additional code in the game engine to manage a system of sprites. You don't develop a sprite manager for the game engine until the next hour, so you won't address the BA_DIE bounds action until then. Fortunately, there is plenty of fun to be had with the three other bounds actions, as you'll soon see.


Drawing the Sprite

The remaining method in the Sprite class is the Draw() method, which is shown in Listing 10.4.

Listing 10.4 The Sprite::Draw() Method Draws a Sprite by Using the Sprite's Bitmap and Current Position
 1: void Sprite::Draw(HDC hDC)  2: {  3:   // Draw the sprite if it isn't hidden  4:   if (m_pBitmap != NULL && !m_bHidden)  5:     m_pBitmap->Draw(hDC, m_rcPosition.left, m_rcPosition.top, TRUE);  6: } 

If the Update() method surprised you by having too much code, hopefully the Draw() method surprises you by having so little. Because the Bitmap class includes its own Draw() method, there isn't much for the Sprite::Draw() method to do. It first checks to make sure that the Bitmap pointer is okay, along with making sure that the sprite isn't hidden (line 4). If all is well, the Draw() method calls on the Bitmap class with the m_rcPosition member variable used to convey the sprite's position for drawing the bitmap (line 5). The last argument to the Bitmap::Draw() method is a Boolean that determines whether the sprite's bitmap should be drawn with transparency, which in this case is TRUE . So, all sprites are assumed to use transparency.



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