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,
The sprite class is the foundation of every 2D computer game, and
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
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
Figure 6.8:
Z-Order Sprites.
In most games, a list of constants are used to define different Z-orders. Background
| 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
Figure 6.9:
A Hand Pointer's Hot Spot Is at the Tip of the Finger.
Some
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.
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
Figure 6.10:
Store Multi-Frame Animations in a Filmstrip.
In the sprite class definition I presented, I defined
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
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
|
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.
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.
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
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
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
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
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
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
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
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
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