9.2 The ''cSprite'' class

Now let's look at what goes inside a cSprite. A sprite does not need to know the name of its owner cCritter. This is the way it should be, as a sprite is simply some geometry in space, possibly textured with a bitmap. This makes life easier as maintaining a cCritter * pointer inside cSprite while maintaining an 'inverse' cSprite * pointer inside cCritter would be a bit of a hassle, particularly when it came to writing the destructors for these objects.

A sprite is something that a critter uses to draw a picture of itself. We will think of the size of a critter as being the visual size of its sprite. So a sprite will have a Real _radius field. Our decision was to have the _radius belong to the sprite rather than the critter, by the way, because we want the radius to represent the visual radius that we see on the screen, and we'd like to have anything visual belong to the sprite.

The effective radius of a sprite may be affected by a scaling matrix, or by the fact that the sprite is a composite of several sprites, so it's not always going to be the case that the virtual Real cSprite::radius() method returns the same value as _radius.

Different sprites will override radius() in different ways. For purposes of collisions, a critter will regard its own radius() as being its _psprite->radius().

The sprite also has a cMatrix _spriteattitude variable which is by default the identity matrix. This matrix is used in addition to the cCritter member cMatrix _attitude. We'll say more about the sprite attitude in the following sections.

The sprite Draw method

We'll give the cSprite a draw method with the same arguments as the cCritter::draw. The cSprite::draw manipulates the graphics matrices and calls a secondary helper method cSprite::imagedraw.

Our graphics pipeline is set up so that before drawing the sprite of a critter, the pipeline gets the critter's _attitude which moves the zero vector to the critter's current position. In addition the _attitude transformation rotates the sprite's spatial 'attitude' to match that of the critter. We only need to multiply a non-trivial _spriteattitude for cases where the sprite is to be positioned other than in the most natural way.

We implement the 'graphics pipeline' as a cGraphics object which maintains two cMatrix members. One of these matrices is called the projection matrix, and the other is called the modelview matrix. At the time when the CPopView::OnDraw calls on the cGraphics object to draw your sprite onto the screen, the modelview matrix MV will typically have the form MV = V' * Mc * Ms, where Ms is the _spriteattitude, Mc is the critter's _attitude, and V' is the inverse of the _attitude matrix of the cCritterViewer which views the scene. (See Chapter 24: 2D and 3D Graphics for a bit more about this.) A given vertex u of a sprite polygon will be drawn as being at the point u' = P * MV * u, where P is the projection matrix. In the case of a composite sprite the MV may incorporate subsidiary matrices for the individual sprite pieces and take on a form like V' * Mc * Ms * Msa, with Msa representing the location of a component of the sprite relative to the spirite as a whole. (Look for instance at the code for cSpriteBubble::setAccentPoly() in spritebubble.cpp.)

In order to right-multiply a matrix into the modelview matrix, we can use the cGraphics::multMatrix method as indicated in the sequence diagram of Figure 9.2. Note that in order to preserve the leading bits of the matrix for use by other critters and sprites, we use pushMatrix and popMatrix calls. The push call saves a copy of the current state of the modelview matrix in a stack, and the latter call copies the saved state back out of the stack.

Figure 9.2. Sequence diagram of the draw cascade


In terms of our equation MV = V' * Mc * Ms, when we start at the top of Figure 9.2, MV is simply V'. The first pushMatrix call saves this value of MV, and the first call to multMatrix sets MV = V' * _attitude. The second pushMatrix call saves this 'critter matrix' value, and the second multMatrix call sets MV = V' * _attitude * _spriteattitude. The two succesive popMatrix calls restore MV back to the simple V' state.

As we mentioned in Chapter 8: Critters, a critter's call to draw(pgraphics, drawflags) uses a Template Method pattern to do the following.

  • Push (that is, save) the graphics pipeline's current modelview matrix.

  • Multiply the critter _attitude times the graphics pipeline's modelview matrix.

  • Call _psprite->draw with the same arguments.

  • Pop (that is, restore) the graphics pipeline's current modelview matrix.

The cSprite::draw method uses the same kind of Template Method pattern, again doing some standard things with matrices and passing the actual drawing off to a subsidiary method, this time the cSprite imagedraw.

void cSprite::draw(cGraphics *pgraphics, int drawflags) 
    imagedraw(pgraphics, drawflags); 
    /* After the draw, tell the sprite that its current geometry has 
        now been drawn once. */ 
    setNewgeometryflag(FALSE); /* This is for use by the 
        cGraphicsOpenGL for knowing when it may need to change any 
        display list id being used for the sprites.*/ 

Table 9.1. How we draw the different kinds of sprite.


imagedraw behavior


Default: draw a hollow circle and radius


Draw a polygon


Draw a bitmap in a rectangle


Draw the sprite for the current time


Draw the sprite for the current direction


Draw a circle decorated with a rectangle


Draw a circle decorated with a pie slice

In plain English, this is the following.

  • Push (that is, save) the graphics pipeline's current modelview matrix.

  • Multiply the sprite _spriteattitude times the graphics pipeline's modelview matrix.

  • Call _psprite->imagedraw with the same arguments.

  • Pop (that is, restore) the graphics pipeline's current modelview matrix.

The cSprite child class imagedraw methods make calls to special kinds of cGraphics methods. For example

void cPolygon::imagedraw(cGraphics *pgraphics, int drawflags) 
    pgraphics->drawpolygon(this, drawflags); 

The cSpriteIcon::imagedraw calls pgraphics->drawbitmap(this, drawflags). The individual cGraphics child class can tell from the pointer argument pgraphics what kind of graphics it is. How the graphics class draws a polygon or a bitmap is up to the individual cGraphics child class. This is an example of the Bridge pattern; the cGraphics child classes have different implementations of the key drawing methods such as drawpolygon and drawbitmap.

The behaviors that we see when drawing the different kinds of sprites are shown in Table 9.1.

The Animate method

During every update of the game, each critter calls a cCritter::animate(dt) method that does two things.

  • Make an updateAttitude(dt) call to

    1. match the critter's _attitude to the critter's current motion matrix if the critter's _attitudetomotionlock is TRUE, or, otherwise

    2. rotate the critter's _attitude by dt*_spin or

    3. leave the _attitude alone if _spin is zero.

  • Call a _psprite->animate(dt, this).

The default cSprite::animate(Real dt, cCritter* powner) doesn't do anything. But the cSprite::animate can be overridden to do various kinds of things. We might look at the powner->recentlyDamaged() value and set a sprite accordingly (see Exercise 9.10). Or you could use dt to increase and decrease the radius of the sprite to give a 'breathing' effect. If we have a polygon-based sprite, we might use dt to move some of the vertices of the polygon so as to make the image flex, perhaps opening and closing its 'mouth' (see Exercise 9.7).

When we use a bitmap based sprite in the cGraphicsMFC, we need to actually change the bitmap being used for different directions (because unlike cGraphicsOpenGL, cGraphicsMFC doesn't rotate bitmaps). And in any graphics implementation, you will need to flip through differing bitmaps if you want an animation effect for the sprite.

In these situations we use the cSpriteShowOneChild composite sprite and let the animate method set the _showindex used to determine the currently active component sprite.

The cSpriteLoop::animate method ages a time counter and adjusts the _showindex accordingly, while the cSpriteDirectional::animate adjusts the _showindex sprite according to the current powner->tangent().

    Part I: Software Engineering and Computer Games
    Part II: Software Engineering and Computer Games Reference