8.3 Critter methodsIn order to run a game, we repeatedly call six methods for each critter, cycling through the calls over and over. The cGame::step method orchestrates the calls. Figure 8.4 isn't any particular kind of official UML diagram, it's simply an informal way of showing the order in which a critter object is cycled through its main method calls, with time flowing in the clockwise direction. Figure 8.4. Critter methods called by the cGame step method The Update , Feelforce , and Feellistener methodsWe give the cCritter a basic update(CPopView *pactiveview, Real dt) method. The argument isn't often used; a bit more about it appears below. The basic update method feels the forces affecting the critter, and changes the acceleration of the critter accordingly . In addition, the basic update checks if the critter should die of old age. void cCritter::update(CPopView *pactiveview, Real dt) { feelforce(); if(_usefixedlifetime && _age > _fixedlifetime) dieOfOldAge(); /* I don't call die() because I like to use die for when a critter dies of unnatural causes, like getting shot. It's more likely that I override die() to do something dramatic than that I override dieOfOldAge(). */ } The pactiveview arguments aren't used by any of our critters in the standard Pop Framework files; they're in place simply for possible use. Some students have in fact written two-dimensional games in which the update feeds the pactiveview into the method COLORREF cCritter::sniff(const cVector &snifflocation , CPopView *pactiveview) . The purpose of this is to let a critter adjust its behavior according to the colors of the nearby pixels as drawn in the active view; we've designed car-racing games in this way, for instance, by using the sniff method to let a critter know when it had driven off the track. As we already discussed in Chapter 7: Simulating Physics, the feelforce method sums up the forces acting on the critter and applies Newton's Law to compute the acceleration. void cCritter::feelforce() { cVector forcesum; //Default constructor (0,0) for (int i=0; i<_forcearray.GetSize(); i++) forcesum += _forcearray.GetAt(i)->force(this); _acceleration = forcesum/mass(); /* From Newton's Law: Force = Mass * Acceleration. */ } Recall that the base class cForce::force returns a zero vector, but we have 'physical' child classes cForceGravity , cForceDrag , and cForceVortex which return non-trivial values. We also have 'behavioral' force classes like cForceObjectSeek , cForceClassEvade , and cForceEvadeBullet void cCritter::feellistener(Real dt) { _plistener->listen(dt, this); /* We pass the pointer "this" to the listener so that it can change the fields of this calling cCritter as required. The caller critter's pgame() holds the cController object that stores all of the keys and mouse actions you need to process. */ } Taken together, the sequence of actions involving the update , feelforce , and feellistener methods can be summarized as follows .
The Move methodThe cCritter::move method has a dt argument because we want the motion to adapt itself according to the speed of the processor running the program. A fast processor will pass very small dt to the move method, and we will want the critters to move only a slight amount with each update. A slow processor will pass larger dt timesteps to the move method, and in that case we need for the critters to move a larger amount with each update. What we basically want from our move(dt) method is these two lines. _velocity += dt * _acceleration; _position += dt*_velocity. But, as we mentioned in the last section, our move(dt) has to do a bit more.
In terms of the special cCritter field names , this can be put a bit more precisely as follows.
As usual, for the fully complete and accurate version, look at the actual code in critter.cpp . Although it isn't really necessary, we happen to have implemented our cCritter::move , and some other critter-moving methods such as clamp and moveTo , so that they return the outcode. But the real use of the outcode is as the internal cCritter field int _outcode . Let's say a few words about the meaning of the outcode. The word 'outcode' comes from computer graphics. The outcode value of the critter is set to reflect the relationship between the border box of the world and the last position the critter moved to (prior to having this position clamped or wrapped). In two dimensions the outcode would distinguish among nine positions relative to a rectangle: inside the rectangle, to its right, to its top right, to its top, and so on. The idea is that we imagine extending the edges of the rectangle into infinite lines, and these lines cut space into nine regions . This is shown in Figure 8.5. Figure 8.5. The nine outcode zones in two dimensions Relative to a box in three dimensions, an outcode can distinguish among 27 possible regions: think of a 3 x 3 x 3 Rubik's cube of space regions built up around the central box. Rather than making up 27 different outcode names, it's more useful to OR together bitflags specifying a location's region relative to each axis. The values we use for our outcodes are defined in realbox.h as follows. (These happen to be implemented as define values rather than static int constants.) #define BOX_INSIDE 0 #define BOX_LOX 1 #define BOX_HIX 2 #define BOX_LOY 4 #define BOX_HIY 8 #define BOX_LOZ 16 #define BOX_HIZ 32 Thus in the plane, a critter located to the 'northwest' of a box would have an outcode of BOX_LOX BOX_HIY . And to perform some action dosomething() only if a critter had touched the lower edge of a box, we could put a line like this into an overridden version of the critter's update method. if(_outcode & BOX_LOX) dosomething(); Indeed, looking back at our Space Invaders Exercise 3.10 in Chapter 3: The Pop Framework, recall that we suggested a condition of just this form for use in the critter's update to detect if the critter had touched the bottom edge of the screen during its last move . The Draw methodIn order to draw a critter we need to have a pointer to a cGraphics object. Also we may have some drawflags to indicate some special aspects of how we want to draw the critter; generally , for instance, we draw a circle around the player critter, and we draw our critters as 'hollow' if they have been recently damaged. So the cCritter::draw code looks essentially like the following, though the actual code you'll find in critter.cpp is a little more complicated. void cCritter::draw(cGraphics *pgraphics, int drawflags) { if (recentlyDamaged()) drawflags = CPopView::DF_WIREFRAME pgraphics->pushMatrix(); pgraphics->multMatrix(_attitude); _psprite->draw(pgraphics, drawflags); pgraphics->popMatrix(); } The draw code is an example of the Template Method pattern. We always want to multiply in the attitude matrix, doing the necessary set-up and clean-up to the pgraphics matrix stack. The part of the call that we override is separated out into the virtual cSprite::draw method. We almost don't need to make cCritter::draw a virtual function, but the cCritterArmed::draw does override and extend the cCritter::draw to draw a short line segment to represent the gun. The Animate methodLet's say a bit more about the critter's _attitude matrix. This specifies how we are to orient the sprite image that represents the critter. The place where the critter updates the _attitude is in its cCritter::updateAttitude call, which is called by the cCritter::animate . void cCritter::animate(Real dt) { updateAttitude(dt); _psprite->animate(dt, this); } If _attitudetomotionlock field is TRUE , the updateAttitude method matches the _attitude matrix to the motion matrix given by the tangent, normal, binormal, and position vectors. This is a good default behavior that makes the critters look lively. If _attitudetomotionlock is FALSE , we allow for the possibility that the critter is spinning. void cCritter::updateAttitude(Real dt) { _attitude.setLastColumn(_position); //always update position. if (_attitudetomotionlock) copyMotionMatrixToAttitudeMatrix(); else //_attitudetomotionlock is FALSE rotateAttitude(dt*_spin); } As we'll see in Chapter 9: Sprites, the reason we pass this to the cSprite::animate() call is that the sprite may want to change itself depending on the direction or the health of its owner critter. Randomizing and mutation methodsAs well as the randomizePosition and randomizeVelocity methods, we can also change a critter by calling a cCritter::mutate(int mutationflags, Real mutationstrength) method. The way this works is that we can feed in various combinations of the static MF_ mutation flags, some of which are defined in critter.h , some in sprite.h and some in spritepolygon.h . The cCritter::mutate method changes a few critter values and then makes a call to the cSprite::mutate method. For randomizing purposes, these methods use the singleton cRandomizer::pinstance() object. The Die and Damage methodsThe default cCritter::die method is implemented in-line simply as virtual void die(){delete_me();} . This tells the owner game to delete the pointer and remove it from the cBiota array when the current round of critter updates is done. The reason we wait a bit is that it can cause trouble if you start adding and deleting critters to an array that you're in the process of updating. Some critters override the die or the damage method to make a noise with a call of the form playSound("Bonk") . Both the cCritter and cGame classes have a playSound method. The string you feed into a playSound call needs to be defined in quotes as the ID of the relevant resource, for instance as 'BONK'. The names of resources are not case sensitive. The standard cCritter::damage(int hitstrength) code reduces the _health by hitstrength , and if the _health is less than or equal to zero, the critter makes a call to die() . The Collide methodCollision is a tricky matter, and we'll give a more detailed discussion of it in Chapter 11: Collisions. Critters collide in pairs. Each critter has the virtual BOOL cCritter::collide(cCritter *pother) method whose default behavior is to perform an elastic collision between the caller critter and the pother critter, changing their positions and velocities in a manner that would be physically natural if the critters were spheres. We can override a critter's collide method to include a reaction to the critter that it's colliding with, possibly killing one of the critters, adding a score to one, or the like. Sometimes we may have two cCritter *pcritteri, *pcritterj that belong to cCritter child classes that have different overrides of the collide method. In this case it makes a difference whether you call pcritteri->collide(pcritterj) or pcritteri->collide(pcritterj) . We generally don't want to call both collide methods as (a) this would waste computational time and (b) the collide methods are designed to have a symmetric effect on the critters, so it would be physically incorrect to call collide twice for one particular collision. As we discuss in Chapter 11: Collisions, we give the critters an int _collidepriority field and a virtual int cCritter::collidesWith(cCritter *pcritterother) method to resolve the question of which critter gets to control a given collision. ![]() |