Working with Sprites


A surface is simply a two dimensional array of pixels. This isn't enough information to create a complicated screen that has layered backgrounds and multiple elements moving around and animating. You need a construct that contains references to a surface that contains every animation frame, knows its current location on the screen, and can be sorted.

A Basic Sprite Class

The sprite class is the foundation of every 2D computer game, and certainly many 3D games. At a minimum, a sprite contains a Z-order for sorting, a position, a special offset called a hotspot, a surface, a width and height, a frame number to identify the current animation frame, and the total number of frames that the surface holds. Here's a model sprite class you can use right out of the box or alter to work for your needs:

 class Sprite { protected:    // Position and Pixel data    //--------------------    LPDIRECTDRAWSURFACE7 m_Surface;  // the surface bits    CPoint m_Position, m_Hotspot;    // subtract HS from pos to get origin    int m_ZOrder;                    // the sort order of the sprite    int m_Width, m_Height;           // dimensions of one frame    int m_CurrentFrame, m_NumFrames; // current frame and total frames    int m_Alpha;                     // range 0x0-0xFF, 0xFF is totally opaque    bool m_HasColorKey;              // set to true if the sprite has a color key    bool m_IsVisible;                // set to true if you want the sprite to draw    // Members that control animation    //--------------------    bool m_IsPaused;         // set to true if the animation has been paused    bool m_LoopingAnim;      // set to true if the animation loops    int m_ElapsedTime;       // the measure of total elapsed time in ms    int m_MSPerFrame         // ms per frame (1000 / desired frames per second) public:    Sprite();    void Update(const int deltaMS);    HRESULT Draw(LPDIRECTDRAWSURFACE7 dest, const CRect& screenRect)    virtual HRESULT Restore();    // the method that sets the current frame    void SetFrame() { m_CurrentFrame = desiredFrame % m_NumFrames; }    // the method that retrieves the current frame    int GetFrame() const { return m_CurrentFrame; }    // the method that returns the number of frames in this animation    int GetFrameCount() const        { return m_TotalFrames; } }; Sprite::Sprite() {    m_Surface = NULL;    m_Position = m_Hotspot = CPoint(0,0);    m_Width = m_Height = 0;    m_CurrentFrame = m_NumFrames = 0;    m_Alpha = ALPHA_LEVEL_OPAQUE;           // 0xFF is totally opaque    m_HasColorKey = TRUE;    m_IsPaused = FALSE;    m_LoopingAnim = FALSE;    m_ElapsedTime = 0;    m_MSPerFrame = 0; }; 

Sprites can also control how they draw and how they animate, but before you learn how that works we'll go over the simple stuff. As you read the next few sections, you'll find out exactly how this Sprite class works.

Sorting Order and Position

Sprites are usually stored in ordered lists so that the screen they are attached to can easily iterate through the list of sprites and draw them. This order is call the Z-order. It is the value used to sort the sprite into the sprite list.

Figure 6.8, shows three sprites. The black rectangle sorts on top of the other sprites because it is drawn last. When the black rectangle sprite is defined, the programmer assigns a Z-order such that it is inserted in the sprite list at the very end. The Z-order determines where sprites are inserted into the list, and therefore the order in which they will draw.

click to expand
Figure 6.8: Z-Order Sprites.

In most games, a list of constants are used to define different Z-orders. Background layers have the lowest Z-order, assuming the list is ordered from back to front. Different layers are given "higher Zs" so that they always sort on top of the background and other sprites that have lower Zs. Sprites, like tooltips, get the highest Zs, so they'll be guaranteed to be drawn on top of everything.

Best Practice

It's a good idea to define a number of different Z layers such as ZORDER_BACKGROUND, ZORDER_FOREGROUND, ZORDER_TOOLTIP, and sort other sprites by using offsets from these layers. For example, if you wanted a special layer of sprites to sort just underneath the tooltips you could set their Z-order at (ZORDER_TOOLTIP-1). This lets you easily redefine the sort order of different layers of similar sprites. Two sprites that have exactly the same Z will sort in the order in which they appear in the sprite list.

The position of the sprite is expressed in pixel coordinates where (0,0) is the upper left-hand corner of the screen and coordinates increase as you move right and down. Not every sprite is positioned relative to its upper left-hand corner. Instead, sprites use a special offset called a hotspot (see Figure 6.9).

click to expand
Figure 6.9: A Hand Pointer's Hot Spot Is at the Tip of the Finger.

Some shapes, such as a tip of a finger in a mouse pointer, have a natural target position other than the top left-hand corner of the surface. Sprites store this offset internally so programmers can position the sprite exactly. The origin of the sprite, its (0,0) pixel, is always drawn at the stored position minus the hotspot offset.

The Sprite class stores these values here:

 CPoint m_Position, m_Hotspot;         // subtract HS from pos to get origin 

Assuming you store the hotspot as a positive pixel value, you'll draw the sprite at a screen location calculated by subtracting the hotspot value from the current position.

Drawing and Animation

A sprite animates by drawing slightly different versions of itself in a smooth sequence. A single surface can store all the animation frames, with a few caveats. Most of the problems come from attempting to store large animations; either from a large dimension or a long sequence of frames. More problems come from storing one of these large frame sequences in video memory.

The best organization for a multi-frame animation sequence in a single surface is a filmstrip (see Figure 6.10). This method guarantees that all the pixels in a single frame are located in contiguous memory addresses. If the animation frames were stored side-by-side, the first "row" of pixels would contain the first row of each frame of the animation, virtually guaranteeing cache misses when the CPU copies any frame of the sprite to the screen. This is why sprites also store their width and height: It is the width and height of one frame, not the width and height of the surface. The frame number is the current frame of the animation—the first frame is always frame zero. The width, height, and frame can be used to find the inclusive rectangle of the current frame within the larger surface.

click to expand
Figure 6.10: Store Multi-Frame Animations in a Filmstrip.

In the sprite class definition I presented, I defined methods to get and set the current frame number, as well as get the total number of frames in the surface. SetFrame() is smart enough to wrap around using a modulo operation. This is convenient for coding simple animations that always loop around to the beginning by calling SetFrame(GetFrame()+1).

You can see how all this works together in the code that a sprite uses to draw itself to a destination surface:

 HRESULT Sprite::Draw(LPDIRECTDRAWSURFACE7 dest, const CRect& screenRect) {    if ( ! m_IsVisible )      return DD_OK;    HRESULT hr;    // Convert to point to screen coordinates    CPoint actualPos( m_Position - m_Hotspot );    actualPos.Offset( screenRect.TopLeft() );    // Source and dest rects    CRect sourceRect( actualPos,    CPoint( actualPos.x + m_Width, actualPos.y + m_Height ) ); CRect destRect = screenRect; // Modify destination rect - make it valid if ( ! destRect.IntersectRect( sourceRect, destRect ) )    return DD_OK; // Find the real source rectangle - from the DX surface CPoint srcOffset = ( destRect.TopLeft() - actualPos ); // Convert to image coordinates sourceRect.left = srcOffset.x; sourceRect.top = srcOffset.y; sourceRect.right = sourceRect.left + min( destRect.Width(), m_Width ); sourceRect.bottom = sourceRect.top + min( destRect.Height(), m_Height ); // Adjust for the frame sourceRect.top += ( m_CurrentFrame * m_Height ); sourceRect.bottom += ( m_CurrentFrame * m_Height ); //Blit from Bitmap Surface to the Offscreen surface for ( ;; ) {      //Blit to the appropriate display      if (m_Alpha!=0xFF)      {           hr = CopyAlpha(                          dest, m_Surface, m_Alpha,                          destRect.TopLeft(), sourceRect, m_HasColorKey);    }    else    {           hr = Copy(dest, m_Surface,                          destRect.TopLeft(), sourceRect, m_HasColorKey);    }    if( SUCCEEDED(hr) )           break;    if ( hr == DDERR_SURFACELOST )    {           if ( FAILED( hr = Restore() ) )                  return hr;    else    {                  continue;        }       } if( hr != DDERR_WASSTILLDRAWING )        {             return hr;      } } return DD_OK; } 

Here's what's happening in this code: The "homework" section at the top checks to see if the sprite is visible. The next section of code, which deals with pixel coordinates and rectangles, looks a little confusing, but it's not as bad as it looks.

If you remember the discussion about screens, you'll recall that a dialog box is a screen, positioned at some positive offset from the background screen's origin. The first two lines of code take the sprite's position, subtract the hot spot, and add the screen's top left corner. If the screen isn't a dialog box the top left point will be (0,0). Then we start calculating the clipping rectangle, if the destination of the sprite would place the right hand or lower portion of the sprite off the screen.

The final adjustment for finding the right animation frame pixels is just before the for loop, where the top and bottom coordinates of the source rectangle are moved relative to the current frame number and the height of an individual frame. The for loop attempts to copy the source frame to the destination surface until it succeeds, or an error other than DDERR_WASSTILLDRAWING is received. The two copy functions assume that the sprite has a color key. If the sprite's surface was lost, it can attempt a restore in the middle of the loop.

Now you need to know how to set the frame counter. You could set it to any legal value and just leave it there; not every sprite animates all the time. Buttons on the user interface usually animate only when they are in a highlighted or rollover state. Another example would be a sprite that shows increasing levels of damage, like the head shot in Doom. Every time damage increases, so does the current frame of the sprite.

Sprite animation is different. An animating sprite changes frames constantly, usually looping back to the beginning and playing again as long as the sprite is in view. A trivial solution would simply involve advancing the frame counter each time the sprite was updated by the game logic, but that's not a very wise choice.

Gotcha

Most artists will design animations with a particular framerate in mind. You should always check with the artist to see what the framerate is for two reasons. First, you want the animation to look as if the artist designed it. It will look wrong if it's played back too fast or too slow. Second, some artists will send you an animation with an insane framerate. I've seen 60fps animations coming from artists who simply didn't check their rendering settings. Most animations can be set at 15fps with little or no degradation in look and feel.

Different computers run through the draw code at vastly different rates. If you used the trivial solution a fast computer would run through the animation frames instantly, where a slow computer could take forever to run through the same animation. Even more importantly, some animations are accompanied by sound effects which always finish in the same amount of time on all machines. How do you synchronize these things?

You need a sprite that runs through its animation in a given amount of time. Most programmers and artists think in terms of FPS, or frames per second. Animation code is written in terms of milliseconds per frame because the algorithm is easier. Milliseconds per frame is simple to calculate from either a given FPS or a given animation time and total animation frames:

 msPerFrame = 1000 / framesPerSecond; msPerFrame = ( totalSeconds * 1000 ) / totalAnimationFrames; 

An update function is all you need to animate the sprite. You'll learn how to set up the architecture to call this function in Chapter 7.

For now, you can assume that this function will get called frequently, with the input parameter set to the number of milliseconds since the last time the method was called:

 void Sprite::Update(const int deltaMS) {    if (m_IsPaused)    {      return;    }    m_ElapsedTime += deltaMS;    // Only call SetFrame() if we have to.    // We're guaranteed to have to advance at least one frame...    if (m_ElapsedTime >= mMSperFrame)    {      DWORD const numFramesToAdvance = (m_ElapsedTime / mMSPerFrame);      m_ElapsedTime -= (numFramesToAdvance * mMSPerFrame);      int desiredFrame = GetFrame() + numFramesToAdvance;      //Check if we're looping...      if ((false==mLoop) && (desiredFrame >= GetFrameCount()))      {        desiredFrame = GetFrameCount() - 1;        //Stay on last frame...      }      //Now advance our frame properly...      SetFrame(desiredFrame);    } } 

The first thing you'll notice is a simple predicate that checks to see if the sprite is actually animating. If the animation is paused, the method returns. The second predicate checks to see if enough time has elapsed to change the current frame. The code inside the second predicate calculates the next frame number given the elapsed time. If the animation does not loop, there's a special case that sets the current frame to the last frame of the animation.

Initializing Sprites

You've got most of a working sprite class with the code you've already seen, but you still lack an important part. How does a sprite get initialized? Initialization needs to be very specific for your game. It depends completely on how you store the data that goes along with the surface bits. You might create surfaces and initialize them from graphics files like JPGs, BMPs, or GIFs, but these formats don't hold all the additional data that a sprite needs to function: position, hotspot, frame information, animation data, and everything else. Something has to associate a single graphics file with all this extra data.

One possible solution is to store this data in an XML file. Every sprite definition can have a unique identifier, a graphic source file, and all the extra bits and pieces that make up a sprite. That would certainly work and it would be a snap to edit, but before you get too excited I should tell you that that solution isn't the best choice. You'll learn more about this solution in Chapter 8, but it makes sense to give you a little peek.

Most games store all their game data in resource files. A resource file stores any kind of game data and associates this data with a unique identifier. A resource manager, or perhaps a better description would be resource cache, would be responsible for taking those identifiers and handing back a pointer to a game object:

 Sprite *kaboom = ResourceCache::Use(SPRITE_KABOOM); 

This implies that the resource cache is smart enough to create a sprite, or anything else for that matter. The block of data that is read to create a sprite resource contains all the extra information to properly initialize the member variables of the sprite, followed by the surface bits.

One question you might have at this point is how in the hell that information gets saved to the resource file. It clearly didn't use the random junk on your hard drive. The resource file is created and maintained with a special builder tool that you'll end up writing yourself. That XML solution I mentioned before might be looking a little better to you right about now, given the inertia of writing an entire tool. Trust me, get to the end of the discussion on resource files before you issue a final judgement; there's a good reason I'm sending you down this road.

Restore()

Restoring is simply the process of throwing out the current surface and reinitializing it, probably by reloading the graphic resource through whatever initialization scheme you've cooked up. This might not always be the case, which is why the Restore() method is virtual. Some sprites don't have a resource file based beginning. A good example might be a sprite that is drawn with an algorithm instead of a resource. This might be a fractal pattern. An even better example is a text sprite, something that you'll initialize by drawing a font to a surface when it is created.

In these cases, you'll want to create classes that overload Restore(). When crazy game players change their game display options or reset their desktop settings, you can iterate through every sprite and reinitialize them.

Dirty Rectangle Drawing

Drawing complicated screens is slow, even for fast machines. There are two primary culprits: pixel overdraw and fonts. Fonts are so slow that it would take a liquid nitrogen cooled CPU to get any framerate if you ever attempted to draw them every frame. Even without fonts, a multilayered sprite screen with tons of pixel overdraw will still bring a flamethrower to its knees, especially if you can't fit everything into video memory. Dirty rectangle drawing refers to something very familiar to programmers unlucky enough to spend any amount of time with Win32's GDI. It refers to redrawing only the portion of the screen that changes, and leaving everything else alone.

If your game uses a 3D engine you can stop reading right now because you can't use dirty rectangle drawing. Actually I lied. Ultima IX's software rasterizer used dirty rectangles, and it was only possible because the screen was drawn without perspective. Everything on the screen appeared just like the old sprite-based Ultima games, which meant that things nearer the viewer scrolled across the screen as the grass under the Avatar's feet. Hardware acceleration did away with all that nastiness. So follow my original advice and don't bother using a dirty rectangle scheme to speed up your 3D game.

A simple dirty rectangle scheme creates a list of rectangles that will be drawn when the screen updates. It might not be completely obvious, but you can't use a flipping screen surface with dirty rectangles. This is because it would be onerous to keep track of which screen areas are dirty from frame to frame. The smallest and simplest flipping chain has a front buffer and a back buffer. A dirty rectangle list would have to be kept for each screen separately, which would also tend to cover more pixels since each buffer only gets updated once every other frame. A non-flipping back buffer is much easier to manage.

Let's assume that your game uses something similar to the sprite class. During the game's update loop, changes to any sprites will be reflected in a managed list of rectangles that describe the areas of the screen that have been touched:

  • Sprite creation/deletion: Add the sprite's screen area to the list.

  • Sprite frame change: Add the sprite's screen area to the list.

  • Sprite movement: Add a set of rectangles that describe the minimum surface area of the old and new screen areas.

You'll end up with a list of rectangles that describe the areas of the screen that have been "dirtied." If your game's list of rectangles is sparse, you can simply run through your sprite list and draw them if they intersect with any of the rectangles. Most games aren't this lucky. The list of rectangles probably has significant overlap and could use some preprocessing to create a list of sprites and rectangles that minimize overdraw. Lucky for you Michael Abrash solved this problem in the Zen of Graphics Programming. Go buy his book and use his solution. I did.




Game Coding Complete
Game Coding Complete
ISBN: 1932111751
EAN: 2147483647
Year: 2003
Pages: 139

Similar book on Amazon

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