Our critters will have a large number of primitive fields relating to their internal state, to the game they participate in, and to their motion. These primitive fields will mostly be int, Real, or cVector objects. Recall that Real is a type we typedef in realnumber.h to be float, although it could be changed to double.
Mixed in with the primitives, our cCritter objects will have pointer fields that hold references to a few other classes. The best way to get a quick idea of the class members is to look at the full cCritter prototype listing in the last section of this chapter.
In brief, the cCritter fields fall into these groupings.
State fields, such as the Real _age and int _health.
Game fields, such as the int _score, and the cBiota * _pownerbiota.
Position fields, such as the cVector _position.
Velocity fields, such as the Real _speed and the cVector _velocity and _tangent.
Acceleration, mass and force fields, such as the Real _acceleration and _density, and a _forcearray of cForce * objects.
Listener fields, such as the cListener * _plistener.
Attitude and display fields, such as the cMatrix _attitude, and the cSprite *_psprite.
In the coming pages, we'll see, step by step, how we to build up a cCritter class whose instances can serve as the all-purpose inhabitants of our computer games.
It's useful for every critter to know its age in seconds. How to measure this age? In keeping with our discussion in Chapter 6: Animation, we'll use real elapsed time for a critter's _age. Ten and a half seconds after the start of the game, all the critters should have an age of 10.5, and so on. When a critter is constructed, its age is set to 0.0, and we update the age within the code for the critter move(Real dt) method with a line like
_age += dt;
Another key state field is a critter's integer _health. By default the _health starts out at 1, and if the critter is damaged, for instance by a bullet, its health will drop. In the standard cCritter::update method, we have the critter die and get deleted if its health drops to 0 or below. We can also allow the possibility of giving a critter a Real _fixedlifetime and forcing it to die once its age passes this value.
These considerations lead to these fields.
protected: Real _age; /* Measure in seconds of time simulated, start at 0.0 when constructed. */ BOOL _usefixedlifetime; /* If TRUE, then die when _age > _fixedlifetime. */ Real _fixedlifetime; /* Max lifetime in seconds, applies only if _usefixedlifetime. */ int _health; /* Lose by being hit and taking damage(). Usually die when _health is 0. */
Since our critters are going to be part of a game, we're going to have some game-related fields as well. For one thing, a critter needs an integer _score field to track how well it's doing. So that a critter can 'see' the other critters in the game, we give it a pointer to a special kind of an array called a cBiota. In any given game, all of the active critters are stored in a common cBiota object. We'll say a bit more about this class in the next subsection.
In our repeated listings of the cCritter fields, we'll carry along some of the fields already mentioned, but not all of them lest our page gets too cluttered. Remember that the full listing can be found at the end of this chapter.
protected: Real _age; int _health; cBiota *_pownerbiota; int _score;
We want our critters to simulate a reasonable kind of motion like we discussed in Chapter 7: Simulating Physics. To start with, in the light of that chapter's discussion, it's clear that we want to have a vector _position and a vector _velocity.
The cVector class is defined in vectortransformation.h, with a switch that lets us make it either a two-dimensional or a three-dimensional vector throughout the program. Our current choice is to have all of our vectors be three-dimensional, so that really cVector stands for the class cVector3. In the case of the flat, two-dimensional games, the third vector component isn't really necessary, but carrying it along adds to generality and turns out to impose only a negligible penalty on speed.
In any case, to start with we'll give the cCritter two more fields. As mentioned above, we'll only relist the most important of the fields already mentioned.
protected: Real _age; int _health; cVector _position; cVector _velocity;
As we discussed in Chapter 7: Simulating Physics, the normal way that objects move can be approximated by repeated updates like this.
_position += dt * _velocity.
As we mentioned in Chapter 6: Animation, it's a good idea to let the time step dt be computed (by a cTimer object belonging to our CPopApp ) to represent the actual time between program updates.
The cCritter has a move(dt) method for moving its position, with the assumption that dt is a real number measuring the time since the last update. Because we are going to construct this method rather carefully to embody the physical laws that apply to all of our objects, we are going to make it a non-virtual method that we can't override.
protected: Real _age; int _health; cVector _position; cVector _velocity; public: int move(Real dt);
Whenever you run a simulation with moving objects, you have to worry about the objects moving off towards infinity and about the possibility of them speeding up and going unnaturally fast. To keep a cCritter from wandering off and getting lost, we'll need to give it a cRealBox _movebox to stay inside. The cRealBox class is a utility class of ours for holding real-valued rectangles or 3D boxes (as opposed to the MFC CRect which is for integer-valued rectangles).
A cRealBox is created by a constructor that takes two or three arguments. If there is no explicit third argument, it's assumed to be 0.0. The dimensions of a cRealBox are chosen so that it's centered on the origin (0.0, 0.0, 0.0), which is also known as cVector::ZEROVECTOR. This is illustrated in Figure 8.2.
Our cGame has a cRealBox _border that keeps the objects inside it. By default a critter has a _movebox that matches the _border of the game its added into. This setting happens because we normally give the cCritter constructor a cGame *pownergame argument.
When a critter hits a wall, we can do various kinds of things. We might just do something like _movebox.clamp(_position) to simply keep it inside the box, where the clamp function just forces a position to be inside the box. Or we might do something more subtle: we could make the _position 'bounce' off the walls of the _movebox like a rubber ball or, perhaps, let it 'wrap' from one edge of the box to the other. Conceivably we might want the critter to wrap across some walls but bounce off others. We'll use an int _wrapflag to decide which of the possible kinds of actions it does.
protected: Real _age; int _health; cVector _position; cRealBox _movebox; int _wrapflag; cVector _velocity;
To keep our cCritter class object from rushing around too rapidly, we'll give it a _maxspeed that bounds the magnitude of its _velocity. Since you're going to be computing this magnitude, it's convenient to keep it around as a Real _speed variable, and while you're at it, it's useful to maintain a unit-length vector cVector _tangent. We'll require that at all times _velocity = _speed * _tangent. You need to be a little careful with your mutators so as not to allow someone to change one of these three fields and not the other two: this is a classic example of a situation where you would not want your fields to be public, for otherwise someone might ignorantly change the _speed or the _tangent field without making the corresponding change to the _velocity.
protected: Real _age; int _health; cVector _position; cRealBox _movebox; int _wrapflag; cVector _velocity; Real _speed; Real _maxspeed; cVector _tangent;
By now our move method has become more a three-step process.
Set _speed and _tangent to match the latest _velocity. If _speed > _maxspeed, reduce _speed, and change the _velocity to match.
_position += dt * _velocity.
Make sure _position is not outside of _movebox.
More complications arise when we put our critters into three-dimensional worlds. As well as tracking as the _tangent the direction the critter is moving in, we align a _normal with the direction the critter was most recently accelerating or turning in, and compute a _binormal perpendicular to _tangent and _normal (that is, we let _binormal be the vector cross product _tangent * _normal ).
protected: Real _age; int _health; cVector _position; cRealBox _movebox; int _wrapflag; cVector _velocity; Real _speed; Real _maxspeed; cVector _tangent; cVector _normal; cVector _binormal;
We'll also maintain a four column cMatrix object called _attitude. By default a critter will keep the four columns of _attitude equal to, respectively, the _tangent, _normal, _binormal, and _position. As it turns out, if we feed an _attitude like this into the graphics pipeline used in our display process, the critter will appear to be rotated so as to match the motion, using a bird-like or fish-like kind of way of holding its body. That is, we imagine that a critter's visual representation has three principal directions similar to, say, the long axis of a whale, the horizontal line of its flukes and the vertical line of its spout. And if we match the _attitude to the _tangent, _normal, _binormal, and _position, the 'whale' will 'heel over' in a natural kind of way when it makes a turn.
There are, however, situations where we want a critter's visible attitude not to match the motion; for instance, if our critter is a fighter that turns this way and that to shoot a gun. Here we have the option of freeing up the _attitude by setting an _attitudetomotionlock field to a FALSE value. In this kind of situation, we'd use some other method for setting the _attitude, possibly controlling it with user key input, or possibly letting the critter tumble at some rate about an axis, with the spin rate and spin axis encapsulated inside a cSpin _spin field.
protected: Real _age; int _health; cVector _position; cVector _tangent; cVector _normal; cVector _binormal; cMatrix _attitude; BOOL _attitudetomotionlock; cSpin _spin; cVector _acceleration;
(Remember that for these illustrative listings of the cCritter fields, we don't keep showing every single field we've mentioned so far. A complete list of the cCritter fields appears in the code printed at the end of the chapter.)
Given our plan to have critters move like objects, we have an _acceleration vector as well. Leaving out the lines about checking against the _movebox and the _maxspeed, we would get something like this for our move(dt) method, just as described in Chapter 7: Simulating Physics.
_velocity += dt * _acceleration; _position += dt*_velocity.
Of course if a critter is to do anything interesting, its motion should change over time. We can alter our motion in four ways: (a) use forces acting on the critter to change the velocity or acceleration, (b) make changes to the critter's position, velocity and/or acceleration based on user input, (c) use a collide method to bounce critters off each other, and (d) override the cCritter::update() method to make other changes to the velocity and acceleration, possibly related to the critter's age.
The details of how we carry out (a) and (b) depend on some class reference members in the cCritter class. Let's start a new subsection in which to discuss these kinds of members.
The associated classes are these: one owner cBiota*, one display-delegate cSprite*, and one listening-strategy cListener* per critter. In addition, there is at most one target cCritter*, and any number of cForce* force-strategy objects. This is shown in Figure 8.3.
The cBiota class is an array-like container class based upon the MFC CArray template. cBiota acts as a helper class for the cGame class. Each cGame has a cBiota member that holds pointers to the active critters of the game.
We give each cCritter a cBiota *_pownerbiota pointer which points to the array-like cBiota object that contains it. This 'back reference' provides a means for the critter to 'see' all the other critters in the simulation ? by walking through the array of all the members of the _pownerbiota object.
The cSprite * _psprite member specifies the critter's appearance on the screen. To make our code more modular, we don't want to tie ourselves to any one particular way of representing a cCritter. We'll work with several kinds of cSprite objects, the disk-like cSpriteBubble objects, the polygonal cPolygon objects, and the bitmap-based cSpriteIcon objects. The cSprite has a draw method that is called by the cCritter:: draw method. Note that before calling _psprite->draw, the cCritter::draw sends the current _attitude matrix into the graphics pipeline. We'll say more about draw below.
If critters hard-coded their display implementation, we'd be facing a combinatorial explosion of all possible critters times all possible sprites. Giving cCritter a cSprite * member is an example of the object-oriented technique of delegation. More information about sprites appears in Chapter 9: Sprites.
The cListener* _plistener is another example of the delegation technique; more precisely it's an example of the Strategy pattern. Later, we're going to introduce a cController class which will hold current information about which keys or mouse buttons are being pressed. And we'd like critters to have the ability to 'listen' to this information. Most critters will ignore user input, so the default listening behavior will be to do nothing. Typically there will be at least one critter that represents the player and which responds to user input. And we might sometimes want more than one critter to be listening to user input. We might, for instance, want to write a two-player game. Or we might want a pinball game with two flipper critters that respond to user input. So we do need to have a listen method for every critter, and we want different critters to be able to listen in different ways. Even when we have only one player listening, we might want to choose between having the player be controlled like a PacMan that moves with arrow keys or having the player be controlled like a car or like a spaceship. Rather than calling a cCritter::listen method, we have a cCritter::feellistener method that calls _plistener->listen.
The CTypedPtrArray<CObArray, cForce*> _forcearray holds any number of force strategy objects that the critter accesses with a feelforce call made by the default cCritter::update method. The CTypedPtrArray is a variation on the MFC CArray template. As we discussed in Chapter 7: Simulating Physics, the feelforce makes calls to _forcearray[i]->force(this).
For the maximum of flexibility we allow for the critters to be subject to a variety of forces. To avoid a combinatorial explosion of classes, we don't specifically define gravity-influenced critters, whirlpool-influenced critters, lighter-than-air critters, and so on. Instead we use the Strategy pattern; that is, we take the notion of a force, and split it off into a separate class called cForce. The main method of cForce is a cVector force(cCritter *pcritter). The cForce::force method computes a vector force for any critter with its concomitant location and velocity. The value of the returned force vector may be based on the critter's position, velocity, or other factors, and it's used to change the critter's acceleration. We also allow the possibility of a force directly changing a critter's position, velocity or acceleration.
We give each critter a _forcearray of cForce * pointers to force objects. As we already discussed in Chapter 7: Simulating Physics, we have a cCritter::feelforce() method which turns around Newton's law: F = ma to have a = F/m.
_acceleration = (Sum over i of _forcearray[i]->force(this)) / mass()
We're going to estimate our critter masses by regarding them as three-dimensional spheres. That is, we'll maintain the equality _mass = _density * radius()^3. (Strictly speaking this is the formula for a cube's mass, but we can think of the necessary 4/3 * PI multiplier for spherical mass as being part of the _density parameter.) Even though this is a two-dimensional simulation, the dynamics of bouncing looks better if you give things the masses of three-dimensional objects. Think in terms of balls rolling around on a pool table.
The radius() of a critter is going to be something that we get from the appearance of the critter, that is, radius() will get its value from the critter's cSprite *_psprite member, to which a critter delegates its display methods. That is, the cCritter::radius() simply returns _psprite->radius().
The cCritter *_ptarget member can be used when we want a critter to 'keep an eye' on one particular other critter. One example is the cCritterArmedRobot child critter class, which automatically aims and shoots at its _ptarget. Another example occurs in the Airhockey game, where each of the two cCritterHockeyGoal objects sets its _ptarget to the critter that's trying to knock the puck into that goal. This way the goal knows to whom to award a score point when the puck goes inside it.
Having a cCritter * member of the cCritter class imposes a certain burden on us regarding destructors. That is, if a critter gets deleted somewhere in the game, any critter that has a _ptarget reference to the dead critter needs to be notified. The cCritter class has a virtual fixPointerRefs method that a critter calls in its destructor. The mission of fixPointerRefs is to go out and tell any other critters in the game to drop any references to the critter now being destroyed. Depending on how heavily referenced a given kind of critter might be by other critters, you may need to overload the fixPointerRefs in various ways.
Dropping a few fields from our growing list and adding in these new ones, we get something like this. Once again, if you want to see the full listing, it's at the end of the chapter.
protected: Real _age; int _health; cBiota *_pownerbiota; cVector _position; cVector _velocity; cMatrix _attitude; cSprite *_psprite; cVector _acceleration; CTypedPtrArray<CObArray, cForce*> _forcearray; Real _mass; cListener *_plistener; cCritter *_ptarget;