To help organize this discussion, let's start with a UML class diagram of some of the main classes involved in the Pop Framework. We'll give a more detailed explanation of how to 'read' UML class diagrams in Section 3.5. For this initial diagram, we use three conventions.
First, we represent a class by the class name inside a rectangle. Second, we represent the class relationship of composition by a line with a diamond at one end. The composition relationship means that a class object of the type at the diamond end owns or has as members class objects of the type at the other end. You can think of the diamond as a 'socket' where we 'plug in' one or more instances of the class at the other end of the composition line. We express the composition relationship by the phrase 'has a' (see Figure 3.2).
Figure 3.2. Composition. Read as 'a cCritter has a cSprite '
Third, we put a star at the end of a composition line to indicate that the 'owner' class may have more than one instance of the other class. We express this relationship by the phrase 'has a number of'. See Figure 3.3.
Figure 3.3. Multiple composition. Read as 'a cGame has a number of cCritters '
Okay, so now here's a class diagram (Figure 3.4) of some of the main classes involved in the Pop Framework.
Figure 3.4. Class diagram for Pop Framework classes
The most central class is the cCritter class. cCritter objects are our game pieces: players, enemies, bullets, furniture, and even the camera through which we look at the world. The word 'critter' is a colloquial Wild-West variation on the word 'creature,' chosen for no better reason than that it's fun to say. We often use the word 'critter' to stand for ' cCritter object'.
We have quite a number of child classes derived from cCritter , specifying different kinds of critters. These classes include cCritterArmed , cCritterBullet , cCritterArmedPlayer , cCritterArmedRobot , cCritterWall , cCritterViewer , etc. (These child classes are not shown in Figure 3.4.)
A unifying notion behind the critters is that they are implemented in such a way that their motions obey a reasonable simulation of physical laws. Why should our game objects move like physical objects? In order for a game to engage the user's attention it needs to feel in some way realistic. You want the user to feel immersed within the world of the game. Given how accustomed we are to the laws of physics, a game whose motions approximate physics is going to be easier to relate to. Keep in mind that we are going to allow ourselves to be fairly arbitrary about the kinds of interactions and 'force fields' that we put into our worlds , so the use of some basic physically-inspired laws of motion is not going to be a drastic limitation. We'll talk about the physics of cCritter objects in Chapter 7: Simulating Physics.
One of the principles of OO is to not make one class do too much. In line with this principle, we let a separate cSprite class be responsible for a critter's appearance.
The most important method of the cSprite class is its draw method, which is overridden in various ways for the different cSprite child classes. The child classes include cPolygon , cSpriteIcon , cSpriteDirectional , cSpriteLoop , cSpriteCircle , cSpriteBubble , etc. We often speak of ' cSprite objects' simply as sprites .
Each cCritter will have a pointer to a cSprite object. We say that a critter delegates the task of drawing to its sprite. Delegation is a very useful technique in OO. Rather than having a class be responsible for a given task, you use composition to give it a member class that handles the task.
One of the advantages of the delegation approach to drawing critters is that after we develop a critter's behavior, we can change its appearance without having to create a new class. It would be tedious , for instance, to develop a cCritterArmedRobot and to then have to derive off cCritterArmedRobotWithBitmapSprite and a cCritterArmedRobotWithPolygonSprite . Since we've delegated the drawing task to a cSprite member, we define a single cCritterArmedRobot class and then, according to the needs of the game we're writing, we put either a cPolygon* or a cSpriteIcon* into the _psprite field of our cCritterArmedRobot objects.
A computer game, or other kind of simulated world, will contain a number of critters, each with its own sprite. We have a cGame class which holds a special array of critters. The cGame class has several important duties . A game initializes the critters of its game world. A game carries out repeated updates of the critter simulations, and makes calls to display the updated critters on the screen. A game keeps track of the critters' status and displays information about the status of the game. The game method that updates the world is called step(dt) ; it takes a real number argument dt that represents how big a slice of time is to be simulated in this update.
We attain resolution independence and the possibility of simulating physics by having all of our critter and game data stored in terms of real numbers . The game exists in a two- or three-dimensional mathematical plane or space. We have a class called cVector to specify the points or vectors in the world. cVector is equipped with a wealth of methods and overloaded operators. There is a related cMatrix class that's heavily used for three-dimensional graphics. As class members of this type are so all-pervasive we don't include them in Figure 3.4.
In line with simulating physics, we allow for each cCritter to be influenced by any number of cForce objects, or forces . Rather than making a force an essential part of a critter's implementation, we delegate out the forces, so that we can 'plug in' whatever forces we like to each critter. Examples of our child class forces include cForceGravity , cForceDrag , cForceObjectSpringRod , cForceObjectSeek , cForceEvadeBullet , etc.
We also delegate out the task of listening to the user's input from mouse and keyboard. At each update, each critter's cListener member, called simply a listener, is given access to the current mouse and key state, and is allowed to change the critter's motion or other states. The default listener does nothing, usually we attach meaningful listeners to only two of our critters: firstly the critter that the cGame recognizes as the 'player' to represent the user on the screen, and secondly the cCritterViewer object that acts as a camera to determine the active window's point of view. Listener child classes include cListenerArrow , cListenerScooter , cListenerCursor , cListenerViewerRide , etc.
We also use some MFC class files that were defined automatically by the Visual Studio AppWizard when the original Pop application was created. Over time, of course, the files for these classes have been edited so as to override and alter the behaviors of the base classes.
The CPopApp is an application object or simply the running instance of the program. It has an overridden OnIdle method that the system calls whenever the program has no other tasks to do. We use the OnIdle call as the 'pump' to drive our animation; our CPopApp::OnIdle uses an instance of our cPerformanceTimer class to find the time dt elapsed since our previous update and then sets off a cascade of calls that lead to invoking the cGame step(dt) method.
The CPopDoc document holds the data associated with your windows . The document serves to hold the data about the game you are running.
The CPopView is a view that controls how your data is displayed in an onscreen window and also does the initial processing on user input with mouse and keyboard. We'll say more about the Document and View pattern in Chapter 5: Software Design Patterns.
The CPopView delegates the details of drawing graphics to a cGraphics class object. The cGraphics class embodies a kind of software pattern known as the Bridge, which means that it can work as a stand-in for such widely varying kinds of graphics implementations as standard Windows graphics and OpenGL. These are embodied in the cGraphics child classes cGraphicsMFC and cGraphicsOpenGL .