Tuesday, October 27, 2009

17.2 The PickNPop implementation



[ Team LiB ]










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.






    [ Team LiB ]



    No comments:

    Post a Comment