10.3 The virtual methods of cGame


10.3 The virtual methods of cGame

Most of the coding you do involving the cGame class is going to involve extending the cGame constructor and overriding a few special methods called seedCritters , initializeView , adjustGameParameters , and statusMessage .

Here are the main things determined by these methods.

cGame::cGame

  • The size of the game world.

  • What colors to use for the edges and background of the world.

  • What bitmaps, if any, to use for your backgrounds.

  • Whether the world is wrapped or has edges.

  • What class of player critter you'll use.

  • What 'permanent' critters you'll use.

cGame::seedCritters

  • What 'temporary' critters you have.

  • Arrangement of the critters in the world.

cGame::initializeView

  • The background image, if any, to use.

  • The cursor tool to start with.

  • Start in zoomed-in mode?

cGame::initializeViewpoint

  • Where to place the viewer critter relative to the world and the player.

cGame::adjustGameParameters

  • How the game is to change during play.

  • When the game is over.

cGame::statusMessage

  • What to write in the status bar line.

The cGame constructor

In a recent build, the default cGame constructor looks, in part, like this.

 cGame::cGame():  _seedcount(COUNTSTART),  _gameover(TRUE),  _maxscore(MAXSCORE),  _scorecorrection(0),  _wrapflag(cCritter::WRAP),  _cursorpos(0.0, 0.0),  _autoplay(0),  _level(1),  _newgame(TRUE)  {  //Allocate the pointer variables except for the player.      _pbiota = new cBiota(this);      _pcollider = new cCollider();      _pcontroller = new cController(); /* This is a structure used to          store key and mouse info. */      _plightingmodel = new cLightingModel(); /* Can be used in 3D          games to specify the lights. */  //Set the border size.      _border.set(14.4, 9.6, 0.0); /* A flat rectangle that happens to          seem good. */  //Set the border colors.      _border.pcolorstyle()->setFillColor(cColorStyle::CN_WHITE);      _border.pcolorstyle()->setLineColor(cColorStyle::CN_YELLOW);      _border.pcolorstyle()->setLineWidthWeight(0.01);  //Set the background bitmap AFTER setting the size of the _border      setBackgroundBitmap(IDB_BACKGROUND); /* Sets the          _pbackgroundbitmap field. */  //Initialize the player AFTER setting the size of the _border.      setPlayer(new cCritterPlayer(this)); /* Use the setPlayer accessor          rather than setting _pplayer by hand. */  } 

When you derive a game such as cGameSpacewar as a child of cGame , you write a cGameSpacewar:: cGameSpacewar constructor. Keep in mind that a child class constructor works by first calling the parent class constructor, and by then calling its own code. In other words, all of the code and initializations of the cGame constructor will have taken place by the time you get inside the first left bracket { of your cGameSpacewar constructor.

The cGameSpacewar needs to make the following changes (among others) to what the default constructor does.

  • Change the dimensions of the _border to a square.

  • Change the fillcolor of the _border to black. (This serves as the game's background color when no background bitmap is being shown.)

  • Change the player by constructing a new cCritterArmedPlayerSpacewar and using the cGame::setPlayer mutator method to install it into the _pplayer field.

Here's the three lines you'd add to the cGameSpacewar constructor to do this.

 _border.set(20.0, 20.0);  _border.pcolorstyle()->setFillColor(cColorStyle::CN_BLACK);  setPlayer(new cCritterArmedPlayerSpacewar); 

We do allow the possibility for having a game in which the _pplayer is ' offscreen .' This means the player is not a member of the _pbiota array of the critters being moved and displayed. This is what you might do it if you were designing, say, a pinball game, a game in which the player is not represented as visible object on the screen. We do this in the PickNPop game for example. To add an offscreen player, use a line like the following.

 setPlayer(new cCritter(), FALSE); /* Put dummy player offscreen, not      in pbiota. */ 

Seeding the game

Once the constructor has been called and the view initialized , the cGame still needs to populate its _pbiota with more critters than just the _pplayer . This is what the cGame::seedCritters method does. And when we want to restart or reset a game, we call the seedCritters method again.

By the way, you can also create critters inside your overridden cGame constructor. The general rule of thumb is that any critter that you expect to have around for the whole game you can create inside the constructor. Any critters that will come and go should be defined in seedCritters . These include the critters that you would need to reseed when you reset the game or move to a new level.

Some examples.

  • In the Spacewar game, the player gets added in the constructor, the asteroids get added in the seedCritters call, and the UFOs are added one at a time by the adjustGameParameters call.

  • In Airhockey, the player, the puck, the goals, and the rival player are all added in the constructor. Nothing is added in the seedCritters call.

  • In Ballworld, the player and the basket are added in the constructor. The balls are added in seedCritters .

  • In Dambuilder, the player and the walls are added in the constructor. The other critters are added in seedCritters .

There are three different situations in which seedCritters is called.

Firstly , when you start the program or use the Game menu to select a new game type, the new game's seedCritters is called by the cPopDoc::setGameClass(CRuntimeClass *pruntimeclass) method. The argument to the setGameClass method is the ' name ' of the game class we want. Thus, for instance, the CPopDoc constructor has a call to

 setGameClass(RUNTIME_CLASS(cGameSpacewar)). 

It's probably a good idea to pause here and mention that the CRuntimeClass holds a string with the name of the class, the size in bytes of the class objects, and information about the class's parent class, if any. There is more discussion of this in Chapter 22: Topics in C++. Now back to the discussion of the first way in which seedCritters can be called.

The purpose of the setGameClass call is to:

  • construct a new game object of the required type and put it into the _pgame field of the CPopDoc ;

  • seed the new game;

  • tell the documents' views to adjust their display for the new game.

Here's how the setGameClass code looks.

 void CPopDoc::setGameClass(CRuntimeClass *pruntimeclass)  {      /* Create a new pointer with the MFC CreateObject method. Even          though we cast the new game into a cGame* pointer for the          return, it "really" remains whatever kind of child class is          described by the pruntimeclass variable, and will use the          child class' overrides of any virtual methods. */      delete _pgame; /* It's OK to delete a NULL, as happens at          startup. */      _pgame = (cGame*)(pruntimeclass->CreateObject());      _pgame->seedCritters();      UpdateAllViews(NULL, CPopDoc::VIEWHINT_STARTGAME, 0);  } 

The only way we ever construct a cGame is via the setGameClass method which always calls seedCritters right after the constructor. This means that a game class constructor should not make a call to seedCritters . If you call it yourself in the constructor, you'll actually be calling it twice, which can waste time or, worse , have the effect of giving you too many critters. The reason we separate out the seedCritters from the constructor is because you want to initialize the permanent members of your game in the constructor and only initialize the temporary members in seedCritters .

The second situation where the Pop Framework calls seedCritters is when you press Enter to start a new game, either because you want a fresh start or because the current game session has ended. Pressing Enter generates a call to the cGame::reset method which calls seedCritters .

On the subject of the reset method, we should mention that this call also resets the player's health and score to their starting values and returns the game's _level parameter to the starting value of 1. See Exercise 10.2 for an example of how to use the _level .

The third way to have a call for seedCritters is that a game may automatically call seedCritters from within its adjustGameParameters method. A game might do this to keep the number of onscreen critters from getting too low.

As an example, here's the seedCritters call from the Spacewar game.

 void cGameSpacewar::seedCritters()  {          /* Get rid of any asteroids and bullets, but if there are              UFOs, leave them alone. */      _pbiota->purgeCritters(RUNTIME_CLASS(cCritterBullet));      _pbiota->purgeCritters(RUNTIME_CLASS(cCritterAsteroid));      for (int i=0; i < _seedcount; i++)          new cCritterAsteroid(this);  } 

The purgeCritters calls are used to get rid of leftover critters you might want. The thing is, we allow the user to call for a restart at any time during game play. So it may be that there's some critters we need to get rid of. In the Spacewar game, the adjustGameParameters call may also call on seedCritters , in the event that it's time for a fresh wave of asteroids.

Inside the seedCritters method, we often use one or more loops to create new critters. We feed the current cGame * argument into the cCritter constructors as the this pointer. When you pass a game pointer to a critter constructor, the critter is automatically added into the game's _pbiota array, and the critter is then able to use its pgame() accessor to get information about the game, in particular, to get information about the size of the game world and whether its edges wrap. The critter constructors assign sprites to the critters, position them within the game world, and initialize their velocities.

How the game adjusts itself

The next method to discuss is cGame::adjustGameParameters . This method gets called once per game update (from within the cGame::step method). We don't necessarily make this method do much work. The default is for it to do nothing. The cGameStub::adjustGameParameters is fairly general.

 void cGameStub::adjustGameParameters()  {  // (1) End the game if the player is dead-------------------------     if (!health() && !_gameover)          //Player's been killed and game's not over.      {          _gameover = TRUE;          pplayer()->addScore(_scorecorrection);              // So user can reach _maxscore          playSound("Tada");          return;      }  // (2) Perhaps reseed the screen if rivals and props are gone.------     int othercrittercount = pbiota()->count(RUNTIME_CLASS(cCritter))           pbiota()->count(RUNTIME_CLASS(cCritterBullet))  1;          /* Number of critters minus bullets minus player equals other              critters. */      if (!othercrittercount) //Player is alone with bullets          seedCritters();  // (3) Maybe check some other conditions. --------------------------- } 

See the description of the Spacewar game in Chapter 14: 2D Shooting Games for an example of a more complicated adjustGameParameters method.

Initializing the view

Depending on which game you're playing, there are all sorts of things you might want to adjust about your view. Does it use a background bitmap? Does it use the 2D cGraphicsMFC or the 3D cGraphicsOpenGL ? What kind of cursor tool do you have? From what viewpoint do you look at the world? Is it zoomed in? And so on.

One initial point to make is how we think of our x -, y - and z -axes. We use the same default orientation in both our 2D and 3D computer games. We think of the x -axis as running from left to right across the screen, and we think of the y -axis as running vertically from bottom to top. And we often think of the origin of the axes as starting at the center of the screen. The z -axis is thought of as pointing out from the screen. Normally by default we would be looking down at the world from somewhere out on the positive z -axis, say at a point with coordinates (0.0, 0.0, 5,0), looking down at the origin point (0.0, 0.0, 0.0). Depending on the game, we may want to adjust which point we look at the world from , and which point of the world we are looking at .

The game itself is data that lives inside a document. When you start up a new game, how does the document manage to reach out and make changes to the view? Actually it's the other way around. The view reaches out and finds its document, gets the game out of the document, and then asks the game to initialize the view.

What prompts the view to do this? A CPopView::OnUpdate call with the integer code CPopDoc::VIEWHINT_STARTGAME in the OnUpdate call's lHint argument.

When you first start up the Pop program a direct CPopView::OnUpdate call is made by the CPopView::OnCreate method. And when the Pop program is running and you have a view already in place and you use the Game menu to select a new kind of game, the CPopDoc::setGameClass method document passes the static integer code CPopDoc::VIEWHINT_STARTGAME as the lHint to an UpdateAllViews call. As we discussed in both Chapter 5: Software Design Patterns and Chapter 6: Animation, the CPopDoc::UpdateAllViews(CView* pSender, int lHint, CObject* pHint) method generates calls to the CPopView::OnUpdate(CView* pSender, int lHint, CObject* pHint) method for each open view, passing on the same arguments.

However we call it, the relevant block of the CPopView::OnUpdate code looks like the following.

 if (lHint == CPopDoc::VIEWHINT_STARTGAME)  {      pgame()->initializeView(this);      pgame()->initializeViewpoint(_pviewpointcritter);      pgraphics()->installLightingModel(pgame()->plightingmodel());      //And now go on and call Invalidate to show the game...  } 

In these lines the CPopView uses its pgame() accessor to reach out and get a pointer to its game from its owner document. (The MFC framework provides a CView::GetDocument() method via which a view can always get a pointer to its owner document.) Once a Pop Framework view has a pointer to its owner game, the view asks the game to initialize it in three different ways.

  • Initialize the view's settings with initializeView .

  • Initialize the location and direction of the viewpoint with initializeViewpoint .

  • Initialize the lighting model used by the view's graphics with installLightingModel .

At this point you might wonder why initializeView and initializeViewpoint are separate methods. Why have separate view and viewer initialization methods? The Pop Framework does it this way so the games will behave smoothly as you use the menu to switch on and off the various View menu options. If we were only writing one game with one kind of view this wouldn't be necessary; the complexity is a result of the code being usable as a flexible framework to build a variety of changeable games.

Here is the code in the base class cGame version of initializeView . As well as the standard calls, you'll notice a number of possible additional calls. The comments explain them pretty clearly.

 void cGame::initializeView(CPopView *pview)  {      pview->setCursor(((CPopApp*)::AfxGetApp())->_hCursorArrow);      pview->setUseBackgroundBitmap(FALSE);          //Default doesn't use bitmap background      pview->setUseSolidBackground(TRUE);          //Use a solid rect background.      pview->setGraphicsClass(RUNTIME_CLASS(cGraphicsMFC));      pview->pviewpointcritter()->setTrackplayer(TRUE);          //Do not track player.  } 

When you override initializeView , it's a good idea to put a call to the base class cGame::initializeView(pview) in the new method, lest you leave out some default call that the base class initializeView makes. A complicating factor in developing the Pop Framework demo program has been that the user is free to use menus to change the game type or the graphics mode or various other things without actually changing the identity of the view. So unless you are careful to reset everything in initializeView , there may be some left-over settings from the last game that you don't want.

Here's an example listing some ways you might override the method for an imaginary cGameSomeChild class.

 void cGameSomeChild::initializeView(CPopView *pview)  {      cGame::initializeView(pview); //Always call the baseclass method.      //Some possible additional calls:      pview->setUseSolidBackground(FALSE);          //For no background at all, faster in 3D.      pview->setCursor(((CPopApp*)::AfxGetApp())->_hCursorPlay);          /* To use the crosshair cursor for shooting with mouse              clicks. */      pview->pviewpointcritter()->setTrackplayer(TRUE);          /* To scroll after the player critter if it moves off              screen. This can be confusing, but is useful if you plan              to use a zoomed in view. */      pview->setGraphicsClass(RUNTIME_CLASS(cGraphicsOpenGL));          //For 3D graphics      pview->pviewpointcritter()->setListener(new cListenerViewerRide());          //To ride the player; this only works in 3D.  } 

Now let's talk about the initializeViewpoint(cCritterViewer *_pviewpointcritter) method.

Initializing the viewpoint critter

Let's set the scene by printing the default cGame::initializeViewpoint code.

 void cGame::initializeViewpoint(cCritterViewer *pviewer)  {          /* The two args to setViewpoint are (directiontoviewer,              lookatpoint). Note that directiontoviewer points FROM the              origin TOWARDS the viewer. */      if (pviewer->is3D())          pviewer->setViewpoint(cVector3(0.0, -1.0, 2.0),              _border.center());              //Direction to viewer is down a bit      else //2D case.          pviewer->setViewpoint(cVector::ZAXIS, _border.center());  } 

To get the point of this, you need to understand that each view of your game has an associated 'viewpoint critter.' More precisely, the CPopView class has a cCritterViewer *_pviewpointcritter member as well as a cGraphics *_pgraphics member. The viewpoint critter and the graphics object work together in the CPopView::OnDraw method, which includes these three steps.

  • Use the viewpoint critter's zoom and perspective settings to set the projection method used by the graphics object.

  • Use the viewpoint critter's position and orientation to set the view matrix used by graphics.

  • Use the graphics to draw the world and the critters as seen by the viewpoint critter.

Clearly the view we see is going to depend upon the direction the viewpoint critter is looking in. A convenient way to set position and orient to the viewpoint critter is to use our cCritterViewer::setViewpoint(cVector toviewer, cVector lookat point) method, where we can think of toviewer and lookatpoint as illustrated in Figure 10.2.

Figure 10.2. Setting the viewpoint

graphics/10fig02.gif

In three dimensions you might ask how far out along the toviewer vector do we move the viewpoint critter? The setViewpoint call will position the viewpoint critter itself just far enough away from the world so that every corner of the world's _border box is visible.

In other words, a call to cCritterViewer::setViewpoint(cVector toviewer, cVector lookatpoint) computes an appropriate seethewholeworld_distance for the current zoom setting and then has the caller viewpoint critter execute lines to the following effect.

 moveTo(lookatpoint + seethewholeworld * toviewer);  lookAt(lookatpoint); 

If you would prefer to see a smaller part of the world, you follow the setViewpoint call with a call to the cCritterViewer::zoom(real zoomfactor) method.

In three dimensions we can think of the zoom as being a field of view angle that ranges from wide-angle to telephoto . With a call like zoom(2.0) , the critter will use a telephoto effect to see only half the world, or if we call zoom(0.5) , it will use a wide-angle effect to see a space twice as big as the world's border.

In two-dimensional worlds , all of this is simpler. The toviewer direction is always just the z -axis, as in a two-dimensional world you can always just imagine the viewer as hovering over the world staring straight down at it. But even in a flat world, we do need to think about which point we are to hover over, that is, the lookatpoint still matters. And in two dimensions we also need to think about how much we want to zoom in on “ or away from “ the world. The analogy to telephoto and wide-angle lenses doesn't make as much sense for the two-dimensional worlds; here it's easier to just think of a call like zoom(2.0) as making things look bigger and zoom(0.5) as making them look smaller.

In designing your game, rather than agonizing over the exact quantitative meaning of the zoomfactor numbers , it's easier to just experiment with a few till you get the look that works the best. Here's how we use zoom in the long, thin world of the Ballworld game for instance. At startup the player is positioned not at the _border.center() but rather near the _border.locorner() , that is, the lower left-hand corner of the long, thin game world. We want to position ourselves right over the critter, looking down at it, and we want to zoom in a bit rather than trying to fit the whole long, thin world onto the screen.

 void cGameBallworld::initializeViewpoint(cCritterViewer *pviewer)  {      if (!pviewer->is3D()) //2D case      {          pviewer->setViewpoint(cVector::ZAXIS, pplayer()->position());          pviewer->zoom(1.5);      }      else          //Do something slightly different in the 3D case...  } 

By the way, how does the Pop Framework know when CPopView::is3D is TRUE ? It looks at the kind of cGraphics currently being used by the view. If the graphics is cGraphicsOpenGL (or maybe, one of these days, cGraphicsDirectX ) the view is 3D, and if it's cGraphicsMFC , the view isn't 3D. The dimensionality of the view is independent of the dimensionality of the game's _border box. Even though our Ballworld game is in a two-dimensional world, we can still fancy it up with a three-dimensional view. When we go to a three-dimensional view we actually enhance the sprites and give many of them a cosmetic z -axis thickness determined by the cSprite::_prismdz factor mentioned in Chapter 9: Sprites.

We can do fancy things with the viewpoint when we start thinking about three-dimensional views. Here's something we do for the three-dimensional viewpoint in the Airhockey game, so as to bring the viewer around behind the player's goal.

 void cGameAirhockey::initializeViewpoint(cCritterViewer *pviewer)  {      if (pviewer->is3D())      {          pviewer->setViewpoint(cVector3(-2.0, 0.0, 1.0),              _border.center());              //These args are the (directiontoviewer, lookatpoint);          pviewer->roll(PI/2.0);              //rolls the viewer to right orientation.      }      else //2D case just copies base class.          pviewer->setViewpoint(cVector::ZAXIS, _border.center());  } 

Still on the subject of three-dimensional views, there's an installLightingModel a call to initialize a new 3D graphics with the game's plightingmodel() . But, at this point, we aren't doing much with this model other than using it to turn the lighting calculations on or off in cGraphicsOpenGL . By default the lighting calculations are on in all the games except for PickNPop, simply because our default lights don't happen to look good on those particular shapes . Oddly enough, OpenGL graphics may run faster when you turn on the additional calculations of a lighting model! Perhaps this is because the OpenGL hardware on graphics cards is optimized to run best with lighting on.

The status message

The last method of the cGame which we'll discuss here is a method it has for generating a string to put into the status bar at the bottom of your Pop window. The active CPopView window repeatedly undergoes an MFC framework call named OnUpdate , and we have this method in turn use the active game's status message by a somewhat arcane line of the form cMainFrame ->SetMessageText(pDoc->pgame()->statusMessage()).

It's common to speak of methods that return useful objects as 'factory methods.' The statusMessage is a kind of factory method that creates a CString object. The default cGame behavior tailors the message to report the score and health of the player critter, the number of critters onscreen, and the number of cycles per second that the Pop program is currently running at. Recall that the Pop Framework is designed in such a way that it will never run at faster than the refresh rate of the graphics card, so if we're near that, we just say 'Near Max.'

Thanks to the richness of the MFC CString methods, it's pretty easy to make the status bar string. Here's a recent version of the default statusMessage code. Clearly this is something you want to override to give the most useful and relevant information for your particular game.

 CString cGame::statusMessage()  {      CString cStrStatusBar;      int nUpdatesPerSecond;      CString cStrUpdatespersecond;      CString cStrHealth;      CString cStrCount;      CString cStrScore;      CString cStrCollisionCount;      if (!gamepaused())      {          nUpdatesPerSecond = int(((CPopApp*)::AfxGetApp())->              _timer.updatesPerSecond());          if (!nUpdatesPerSecond)              cStrUpdatespersecond.Format("Less than one update per second.");          else              cStrUpdatespersecond.Format("Updates per second: %d.",                  nUpdatesPerSecond);          if (((CPopApp*)::AfxGetApp())->_timer.runningNearMaxSpeed())              cStrUpdatespersecond += " (Near Max)";      }      else              cStrUpdatespersecond.Format("Animation is paused.");      cStrScore.Format("Score: %d.", score());      cStrHealth.Format("Health: %d.", health());      int crittercount = _pbiota->count(RUNTIME_CLASS(cCritter));      int bulletcount = _pbiota->count(RUNTIME_CLASS(cCritterBullet));      crittercount -= bulletcount;      if (visibleplayer()) /*Subtract off 1 for player as well. */          crittercount -= 1;      cStrCount.Format("Other Critters: %d.", crittercount);      cStrStatusBar = cStrScore + " " + cStrHealth + " " + cStrCount +          " " + cStrUpdatespersecond;      return cStrStatusBar;  } 

In line with our usual policy of not keeping the same information in two different places, we use a cBiota::count method to walk through the active critter array and count up the kinds of objects on the spot “ this is in place of trying to maintain an integer that holds the count value in it. It costs a (very small) bit of speed to walk through the array once to recompute the number each time you need it, but the simplification to your code seems worth it.

The randomSprite factory method

Let's conclude by mentioning a factory method that cGame has. A factory method constructs an object of a certain kind and returns a copy of it or, more commonly, a pointer to it.

 cSprite* randomSprite(int spritetypeindex);/* A factory method to      return one of the various kinds of sprites. */ 

The kind of sprite that randomSprite returns depends on the argument you give it. These are statics defined as follows .

 const int cGame::ST_SPRITETYPENOTUSED = -1;      //Indicates you will put in sprites by hand.  const int cGame::ST_SIMPLEPOLYGONS = 0;      //Simple triangles, squares, pentagons.  const int cGame::ST_FANCYPOLYGONS = 1;      //Diverse regular and star polygons.  const int cGame::ST_ASTEROIDPOLYGONS = 2;      //Polypolygons that have polypolygons at their tips.  const int cGame::ST_POLYPOLYGONS = 3;      //Polypolygons that have polygons at their tips.  const int cGame::ST_BITMAPS = 4; //cSpriteIcon bitmaps.  const int cGame::ST_BUBBLES = 5; //balls of various kinds.  const int cGame::ST_TRIPLEPOLYPOLYGONS = 6;      //Polypolygons that have polypolygons at their tips. 

When a cCritter child needs a random sprite of a certain type, we can get one with the randomSprite factory method like this.

 cCritterAsteroid::cCritterAsteroid(cGame *pownergame)  {      if (pownergame)          setSprite(pownergame->randomSprite(cGame::ST_ASTEROIDPOLYGONS));      //Etcetera  } 

The randomSprite is a fairly generic method that hardly needs any members of the cGame class, but you might sometime want to override it. The randomSprite explicitly looks at another cGame member only when called with the cGame::ST_BITMAPS argument. In this case, the cGame::randomSprite chooses a resource ID for a bitmap from a cGame member array _bitmapIDarray . The _bitmapIDarray gets initialized in the cGame constructor, by the way. You can check the game.cpp file for details.

Also see Exercise 14.4: A Graphic Theme for Your Game for an example of how we might want to change bitmapIDarray in a game constructor override.



Software Engineering and Computer Games
Software Engineering and Computer Games
ISBN: B00406LVDU
EAN: N/A
Year: 2002
Pages: 272

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