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.

Software Engineering and Computer Games
Software Engineering and Computer Games
Year: 2002
Pages: 272

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net