17.2 The PickNPop implementation

A lot of the work of designing software goes into improving the way that the program looks onscreen. Software engineering is a little like theater, or like stage-magic. Your goal is to give the user the illusion that your program is a very solid, tangible kind of thing. Getting everything in place requires solid design and a lot of tweaking.

One thing differentiating PickNPop from Spacewar and Airhockey is that we chose to make the _border of the world have a non-zero z size so that the shapes can pass above and below each other when we show the game in the OpenGL 3D mode.

Making the score come out even

Though not all games must have a numerical score, if you have one, then it should be easy to understand. On the one hand you might require that your game events have simple, round-number score values assigned to them. On the other hand you might require that your maximum possible game score total be a round easy number like 100, 1000, or even 1,000,000. If you are able to control the number of things that can happen in your game, then you can satisfy both conditions. If not, then you have to settle for one of the conditions: round-number values or round-number maximum score.

In PickNPop, we allow for varying sizes of worlds, and, since the game might still be developed further, we allow for recompiling the program with different values of JEWEL_PERCENT. So it's not possible both to have round-number values and to have a round-number max score.

Our decision here was to go for the round-number maximum score. In the CPopDoc::seedBubbles(int gametype, int count) method we figure out how many jewels and peanuts to make, and then we figure out how much they should be worth, and finally we calculate a _scorecorrection value that we add in at the game's end to make it possible for the user's score to exactly equal the nice round number MAX_SCORE.

In cGamePickNPop::seedCritters() we compute the peanutstoadd peanuts and jewelstoadd jewels needed, and then bury the jewels 'under' the peanuts by adding them in second. The default behavior of cBiota is to draw the earlier array members after the later array members. When using the two-dimensional cGraphicsMFC, this causes a 'painter's algorithm' effect of having the later-listed critters appear behind the earlier-listed ones. When using the three-dimensional cGraphicsOpenGL, the critters are actually sorted according to the z-value of their _position values. The cheap and dirty cGame::zStackCritters() call gives the critters different z-values, again arranging them so the earlier-listed critters have larger z-values than the later-listed critters' z-values and end up appearing on top in the default view from up on the positive side of the z-axis.

void cGamePickNPop::seedCritters() 
{ 
    /* First we'll set the _bubble array to have room for count 
        bubbles. Then we'll add jewels and peanuts, randomizing their 
        radii, positions, and colors as we go along. In the case of 
        PGT_3D, we go back and change the radii at the end. */ 
    int i; 
    int jewelstoadd, peanutstoadd; 
    Real jewelprobability = cGamePickNPop::JEWEL_WEIGHT; 
    int jewelvalue(0), peanutvalue(0); 
    cCritter *pcritternew; 
    /* I use the jewelprobability to decide how many jewels and how 
        many peanuts to have. These are the jewelstoadd and 
        peanutstoadd numbers. We think of randomly drawing from this 
        supply and adding them into the game. I want my standard game 
        score to be MAX_SCORE, with JEWEL_GAME_WEIGHT portion of the 
        score coming from the jewels and the rest and from the 
        peanuts. The scores have to be integers, so it may be that the 
        total isn't quite MAX_SCORE, so I will give the rest to the 
        user as game-end bonus. */ 
//----------Get the counts and the scorevalues ready----------
    jewelstoadd = int(jewelprobability * _seedcount); 
    peanutstoadd = _seedcount ? jewelstoadd; 
    jewelvalue = 
int(_maxscore*cGamePickNPop::JEWEL_GAME_SCORE_WEIGHT)/ 
    (jewelstoadd?jewelstoadd:1); 
    peanutvalue = (_maxscore ? 
        jewelvalue*jewelstoadd)/(peanutstoadd?peanutstoadd:1); 
    _scorecorrection = _maxscore ? (jewelstoadd*jewelvalue + 
        peanutstoadd*peanutvalue); 
        /* We'll add this in at the end, so that user's maximum 
            score is the same as the targeted _maxscore). */ 
//--------------------Renew the _bubble contents ----------
    _pbiota->purgeNonPlayerNonWallCritters(); 
        // Need to delete any from last round 
        /* Regarding the stacking, it's worth mentioning that 
            cBiota::draw draws the critters in reverse order, last 
            index to first, so the first-added members appear on top 
            in 2D. We want the peanuts "on top", so we add them first. 
            Of course in 3D, the zStackCritters is going to take care 
            of this irregardless of what order the critters are drawn. */ 
    for(i=0; i<peanutstoadd; i++) 
    { 
        pcritternew = new cCritterPeanut(this); /* White bubble that 
            we call a "Peanut", can't move out of _packingbox */ 
        pcritternew->setValue(peanutvalue); 
    } 
    for (i=0; i<jewelstoadd; i++) 
    { /* Make a pcritternew and then add it into _bubble at the 
        bottom of loop. */ 
        pcritternew = new cCritterJewel(this); /* Colored bubble 
            that we call a "Jewel", can move all over within 
            _border.*/ 
        pcritternew->setValue(jewelvalue); 
    } 
    zStackCritters(); 
} 

The world rectangles

In PickNPop we want to try and fit our game as nicely as possible into our window. We give the CDocument a cGraphicRealBox _packingbox and _targetbox field. These are to be rectangles that fit nicely inside the _border. Rather than setting their values with brute numbers, we set their values as proportions of the _border. The cRealBox::innerBox function returns a cRealBox slightly inside the caller box. And we give them some nice colors and edges.

Converting a critter

One of the parts of the code the author initially had trouble with was in the cCritterJewel method where we react to moving the critter inside the _targetbox. Here we have to replace one class of object by a different class of object, while still having the object be in some ways the 'same.' It turns out that you can't do this with something so simple as a type-cast of the sort you'd use to turn an int into a float. Class instances carry too much baggage for that. What we do instead is to create a brand-new object which copies the desired properties of the object that you wanted to 'cast.' We do this by means of a cCritterUnpackedJewel copy constructor.

void cCritterJewel::update(CPopView *pactiveview) 
{ 
    cGamePickNPop *pgamepnp = NULL; 
//(1) Apply force if turned on. 
    cCritter::update(pactiveview); //Always call this. 
    cVector safevelocity(_velocity); /* To be safe, don't let any z 
        get into velocity. */ 
    safevelocity.setZ(0.0); 
    setVelocity(safevelocity); 
//(2) Check if in targetbox, and if so, replace yourself with a good 
//  jewel. 
    if (pgame()->IsKindOf(RUNTIME_CLASS(cGamePickNPop))) 
    /* We need to do the cast to access the targetbox field, and to 
        be safe we check that the cast will work. */ 
        pgamepnp = (cGamePickNPop*)(pgame()); 
    else 
        return; 
    cRealBox effectivebox = pgamepnp-
>targetbox().innerBox(cGamePickNPop::JEWELBOXTOLERANCE*radius()); 
    if (!effectivebox.inside(_position)) 
        return; 
    //Reaction to being inside _targetbox. 
    playSound("Ding"); 
    cCritterUnpackedJewel *pcritternew = 
        new cCritterUnpackedJewel(this); //Copy constructor 
    pcritternew->setMoveBox(pgamepnp->targetbox()); 
    pcritternew->setDragBox(pgamepnp->targetbox()); 
    delete_me(); /* Just tell cBiota to just remove the old critter. 
        Don't use the overridden cCritterJewel::die to make a noise 
        and subtract _value from score.*/ 
    pcritternew->add_me(_pownerbiota); //Tell cBiota add new 
        critter. 
    pgamepnp->pplayer()->addScore(_value); 
} 

The delete_me makes a service request to the _pownerbiota cBiota object. The add_me makes a service request as well, but since pcritternew isn't yet a member of _pownerbiota, we need to pass this pointer into the add_me method.



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