Exercises

Exercise 9.1: Making cSprite::draw non-virtual

If you don't explicitly label a method as 'virtual' in the base class, then the child class overrides will ignore it. Open up the Pop project file and remove the word 'virtual' from the line virtual void draw(cGraphics *pgraphics, int drawflags); at the bottom of the sprite.h file. Build and run. You'll see default base-class imagedraw methods for all the sprites. They'll be sprites that are drawn as hollow circles.

Exercise 9.2: What happens if you don't initialize the sprites?

By default every critter gets a base class sprite. If you forget to initialize the sprites you'll see the same default sprite as in the last exercise. You can test this by going into the gamestub.cpp file and commenting out the sprite initialization in the cCritterStubProp constructor.

Exercise 9.3: Making your own polygons

Sometimes you'll want to give a critter a sprite that has some particular polygonal shape that you like. In this exercise you'll make the player in the Game stub game be shaped like a slender rocket-like pentagon instead of like a slender triangle.

Here's an example of how we set a polygonal sprite shape taken from cCritterBullet::initialize method inside the critterarmed.cpp. The purpose of the code is to create a slender isosceles triangle and make this be the sprite for the cCritterBullet making the initialize call.

cPolygon *ppolygon = new cPolygon(3); 
        /* Now make it a thin isosceles triangle, with the apex 
            at the 0th vertex. All that matters at first is the 
            tatops of the numbers, as we will use setRadius to 
            make the thing the right size, and center it on the 
            origin. */ 
    ppolygon->setVertex(0, cVector(3.0, 0.0)); 
    ppolygon->setVertex(1, cVector(0.0, 1.0)); 
    ppolygon->setVertex(2, cVector(0.0, -1.0)); 
    ppolygon->setRadius(cCritter::BULLETRADIUS); /* Call setRadius 
        after adding all the vertices! */ 
    ppolygon->setFillColor(cColorStyle::CN_YELLOW); 
    setSprite(ppolygon); 

The idea is that if you want a polygon with N vertices, you create a new polygon with a new operator. Then give the new polygon as many vertices as you need with a call like setRegularPolygon(N, cVector(0.0, 0.0), 1.0, 0.0), where only the first argument really matters. Then you go down the list of vertices and set them one by one, starting with 0 and ending with N ? 1. Then you use a setRadius call to set the polygon shape to whatever size you like. (The setRadius call has the side effect of centering the polygon on the origin, so you may need to change the _spriteattitude if you want to move the object back to some other location. If you have your vertices just where you want, you don't necessarily need to call setRadius at all. When in doubt, call it, though.)

Now how do you figure out what numbers to use in the setVertex lines? Draw a little grid for yourself ? or use some graph paper ? and draw a picture of the shape you want. Write the coordinates of each point on your sheet of paper (if you try and do this in your head you're likely to mess it up). Then use these numbers in the setVertex calls. Make sure that the first vertex you put in ? that is, the 0th vertex ? is where you'd like the sprite to point when it moves. In the case of the rocket, this would be the pointed tip. And only put the starting vertex in once because you don't need to close the polygon back up by putting the starting vertex in twice. The cPolygon will take care of that on its own.

Your picture doesn't necessarily need to be centered on the origin; the setRadius call will take care of that, automatically storing the polygon in origin-centered form. (You can test this claim by putting the digits 10 in front of the x-coordinates in the code above to shift everything 100 units to the right.) The size of the number also doesn't matter, this is taken care of by the setRadius call. (You can test this by putting the digit 0 after each of the x and y coordinates to make the triangle ten times as big.) All that matters is the relative positions and ratios of the points you choose.

Now make your player be shaped like a biting mouth. Once that works, try making it be shaped like a fish. And then try a shape of your own invention. Remember that the polygon will move with the 0th vertex in the lead, so pick this one towards the front.

Exercise 9.4: Fish-shaped polygons

Suppose we'd like to have a lot of fish. What you can do is make a child class cSpriteFish : public cPolygon. Define the class in a file called polygonshapes.h. And then make a polygonshapes.cpp class that implements a constructor to make the thing be a polygon in the shape of a fish. The class doesn't have any additional data members, so its constructor is the only new piece of code you have to add. If we used a crude ten-point fish (see Figure 9.5), the cSpriteFish constructor would call setRegularPolygon(10, ...) and then make ten calls to setVertex. Don't forget to put in the DECLARE_SERIAL and IMPLEMENT_SERIAL macros; you can copy the way its done in the bubble.h and bubble.cpp files.

Figure 9.5. A fish with vertex numbers

graphics/09fig05.gif

Make the class and test it in the PickNPop game by making the peanuts look like fish. So now it'll be an undersea treasure game! All you have to do is go into the gamepicknpop.cpp and change the single line of the cCritterPeanut::cCritterPeanut() constructor to read setSprite(newcSpriteFish()). Remember that for this to compile, you'll have to add an #include "polygonshapes.h" to the polygonshapes.cpp file, and the very first thing in the file has to be an #include "stdafx.h".

Exercise 9.5: Composite polygon shapes

Looking back at our fish problem, shouldn't a fish have an eye? So maybe you better add an extra cVector _vectoreyedot and cSpriteCircle *_pointeyedot to the cSpriteFish definition and let the fish inherit from cSpriteComposite.

Think of some more shapes we might need and make more child classes that draw them. A cSpriteRocket, a cSpriteBird, a cSpriteFootball, and a cSpriteUFO might all be useful. Some of these would be better represented by several polygons ? for a cSpriteRocket, for instance, you might want to have the rocket's fuselage (or fish's body) be a different color from its fins. Use the cSpriteComposite to have several polygons involved. Check the code in spritebubble.cpp for inspiration.

Exercise 9.6: Other kinds of polypolygons

You can use any kind of sprite you like for the tipshape of a polygon; in fact you could nest and get polypolypolygons. Try to view some of these. You might look at the cGame code for generating random polypolygons for inspiration.

We've experimented with both the two-level and the three-level polypolygons, which are polypolygons whose tips are polygons whose tips are polygons. The runspeed gets pretty low with these guys, but they're interesting to look at once you tweak the various numbers good values.

Exercise 9.7: Flexing polygons

Make a cPolygonFlex child class with a drift method to make the vertices move when animate is called. Try and make a fish whose mouth opens and closes.

Exercise 9.8: Directional vs loop sprites

The Worms game is an example of how to use these multi sprites. By default the player uses a cSpriteLoop. Go into the cCritterWormsPlayer::cCritterWormsPlayer() constructor in gameworms.cpp and look at how this works. To see a directional sprite, go to the top of the file and comment out the line #define PLAYERSPRITELOOP. Once you see how both the kinds of sprite work, you might try changing the cCritterWormsPlayer constructor code to use something different for the different individual sprites. See how the loop and the directional sprite look with, say, four bitmaps.

Exercise 9.9: A loop sprite of polygons

If I wanted a critter's constructor code to have a ppolygons animated sprite that would cycle from triangle through hexagon, I could do it like this.

cSpriteLoop ppolygons = new cSpriteLoop(); 
ppolygons->add(new cPolygon(3)); 
ppolygons->add(new cPolygon(4)); 
ppolygons->add(new cPolygon(5)); 
ppolygons->add(new cPolygon(6)); 
setSprite(ppolygons); //Called by the cCritter whose constructor 
    contains this code. 

Try giving the Prop critters a sprite like this in the cCritterStubProp constructor in gamestub.cpp.

Exercise 9.10: A HappySad sprite

Suppose that you want to alternate between two kinds of sprites, depending on whether or not the critter was recently damaged.

Derive a cSpriteHappySad from the cSpriteShowOneChild sprite, and overload its animate method like this:

viod cSpriteHappySad::animate(Real dt, cCritter *powner) 
{ 
    if (!powner->recentlyDamaged()) 
        setShowIndex(0); 
    else 
        setShowIndex(1); 
    if (showindex != cBiota::NOINDEX) 
            childspriteptr[ showindex]->animate(dt, powner); 
} 

When you use a cSpriteHappySad, you have to be sure to add at least two child sprites to it, the first added will be the 'happy' sprite, the second addded will be the 'sad' or 'recently damaged' sprite.

Note that we currently have a default method of showing a critter's damage by drawing polygons in wireframe mode. For this exercise, turn off this behavior by commenting out the #define SHOWDAMAGE line at the head of critter.cpp. (Also note that, in any case, the wireframe mode doesn't affect bitmap sprites.)

Now try giving the player in, say, the Worms game a cSpriteHappySad. You can use the current loop sprite (as it currently is) when the player is healthy, and add in a bitmap sprite for when the player is recently damaged.

Exercise 9.11: Three-dimensional sprites

The glshapes.* files in the Pop Framework provide some standard 'glut' methods for drawing three-dimensional shapes. 'Glut' stands for 'OpenGL Toolkit.' Although the glut library includes a lot of useful high-level OpenGL methods, we only incorporate this one single glut file into the Pop Framework at present, in part because glut is based on a non-Windows framework that makes parts of it incompatible with MFC.

The Gamestub3D showing OpenGL sphere, teapot, torus, and some polyhedra. The cog-like shapes are cPolyPolygon

graphics/09icon04.gif

The glshapes files implement the following function calls, where GLdouble means the same as double, and GLint means int.

glutSolidSphere(GLdouble radius, GLint slices, GLint stacks); 
glutSolidCone(GLdouble base, GLdouble height, GLint slices, GLint 
    stacks); 
glutSolidTorus(GLdouble innerRadius, GLdouble outerRadius, GLint 
    sides, GLint rings); 

glutSolidTetrahedron(void); 
glutSolidCube(GLdouble size); 
glutSolidOctahedron(void); 
glutSolidDodecahedron(void); 
glutSolidIcosahedron(void); 

glutSolidTeapot(GLdouble scale); 

For each 'Solid' function there is an analogous 'Wire' function, for instance, there is a glutWireSphere(GLdouble radius, GLint slices, GLint stacks);

The 'teapot,' by the way, is a standard test shape beloved of computer graphics programmers, and built up by using 'Bezier patches.' It's sometimes called the 'teapotahedron,' and is jokingly viewed as the sixth Platonic solid!

The slices and stacks parameters used for the circular shapes can be thought of as the number of north?south longitude and east?west latitude lines, respectively. That is, a sphere is drawn as a vertical pile of stacks many slices-sided polygons. You need values of at least 12 or so to make these shapes smooth-looking.

What you should do for this problem is to implement some or all of the classes cSpriteSphere , cSpriteTeapot , cSpriteTorus , cSpriteCube, and so on. First do one, and get it debugged, and then try a few more. You can try testing them out as sprites used by the critters in cGameDefender3D.

Each of the classes should have a constructor that takes a Real radius argument with a default value of 1.0. The circular sprites have additional int slices and int stacks arguments with default values of, say 12, though these defaults ought to be statics. And the torus and cone each have an additional Real parameter: the torus should also have a Real innerradius argument and the cone also needs a Real height argument.

To draw the sprites, you could go one of two ways: many classes or one class.

Many classes

You could have each class emulate the behavior of cPolygon and cSpriteIcon and define, say,

void cSpriteSphere::imagedraw(cGraphics *pgraphics, int drawflags) 
{ 
    pgraphics->drawsphere(this, drawflags); 
} 

And you'd have to add a new drawsphere method to cGraphics, giving it a void or trivial implementation for cGraphicsMFC and giving it an implementation in cGraphicsOpenGL that calls glutSolidSphere or glutWireSphere depending on whether psphere->filled() is TRUE.

virtual void drawsphere(cSpriteSphere *psphere, int drawflags ) 

You'd need to override imagedraw and implement a differently-argumented variant of cGraphics::drawsomething for each of the nine classes, which is a little boring to do.

One class

You could get by with slightly less typing by having all of these new classes inherit from a catch-all cSprite3D class. We could prototype cSprite3D something like this.

class cSprite3D : public cSprite 
{ 
protected: 
    int _slices, _stacks; 
    int _shapecode; 
    Real _extraparam; 
public: 
    cSprite3D(int type = cSprite3D::SPHERE, Real radius = 1.0, 
        Real extraparam = 0.0, int slices = cSprite3D::SLICES, 
        int stacks = cSprite3D::STACKS); 
    virtual void imagedraw(cGraphics *pgraphics, int drawflags) 
        {pgraphics->draw3Dshape(this, drawflags);} 
}; 

And then you'd only need to prototype and code a single cGraphics method.

virtual void draw3Dshape(cSprite3D *pshape, int drawflags ); 

The cGraphicsMFC version of draw3Dshape can just draw a circle, while the cGraphicsOpenGL implementation will hold a big switch on pshape->shapecode(). Is the cost of the switch something worth worrying about?

No. Although we didn't raise this point earlier, the cSprite::draw method is constructed so that it will avoid the switch after the first call to a given draw method for a cSprite3D by using display lists.

You will need to do some work to implement a correct radius() method for these sprites, and to have the value returned by radius() match the radius argument that you feed in ? and to match the visual appearance. If possible make radius() match the number in the _radius field, it may be that cSprite3D needs to maintain a supplemental Real _glutradius for the parameter that you actually feed into the glut call in cGraphicsOpenGL::draw(..), or for the parameter that you perhaps use in a scaling matrix. Compare our use of a Real _visualradius in the cSpriteIcon code.

The reason that radius is an issue is because you compute the distance from a cube's center to its corner one way, but you compute the distance from a tetrahedron or a cone's center to its furthest point another way. We care about the radius because in order for our cheap and dirty collision code to look right, the 'radius' of a sprite needs to match the radius of the smallest sphere that encloses it.



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