When you get into tweaking one particular game mode, it saves time to have the Pop program start up in the game mode that you want to play with. The way to control this is to edit the CPopDoc constructor in popdoc.cpp. Simply comment in exactly the one setGameClass line corresponding to the game you want to play. If you make a new game class, add a line for it. For the following exercises, have your startup game be cGameStub.
CPopDoc::CPopDoc():
_pgame(NULL)
{
/* Choose the type of game you want at startup by commenting in ONE setGameClass line.
The setGameClass sets brandnewgameflag to TRUE. */
// setGameClass(RUNTIME_CLASS(cGameSpacewar));
// setGameClass(RUNTIME_CLASS(cGameAirhockey));
// setGameClass(RUNTIME_CLASS(cGameBallworld));
// setGameClass(RUNTIME_CLASS(cGameDambuilder));
// setGameClass(RUNTIME_CLASS(cGamePickNPop));
// setGameClass(RUNTIME_CLASS(cGameWorms));
setGameClass(RUNTIME_CLASS(cGameStub));
// setGameClass(RUNTIME_CLASS(cGameStub3D));
// setGameClass(RUNTIME_CLASS(cGameDefender3D));
}
If you do this exercise and the game still starts up in the original Spacewar Game mode, it’s very likely that you edited the wrong copy of popdoc.cpp. One of the gotchas of Visual Studio is that when you use the File | Open command, the file selection dialog doesn’t make it clear which directory you are in. Visual Studio has a certain persistence of state, and if you open a file in DirectoryA, the next time you open a file the dialog is likely to search in DirectoryA again, even if you are now working on a project in DirectoryB. One often has multiple copies of the Pop Framework code on one’s disk, and it is easy to be editing a file in the wrong directory.
A sure sign that you’re editing the wrong files is if (a) your program always compiles and runs with no warnings or error messages and (b) the appearance of the executable looks the same after each “build.”
How to avoid this gotcha? If you see signs of (a) and (b), close all your files, close your project, reopen your project in your desired directory, and then do a File | Open, only this time use the dialog to back a step or two up the directory tree to find out what directory you’re realy in, and then go back down into the correct directory.
We edit some of the cGameStub methods in the gamestub.cpp file to change the appearance of the game world.
(a) Our goal here is to make a simple Space Invaders game. Let’s not use the cCritterStubRival at all, let’s just have dumb non-shooting cCritterSpaceInvadersProp falling down on us. We can do this by changing the two static critter count numbers that are used in the cGameStub constructor to initialize _rivalcount and _seedcount. The statics are defined right before the cGameStub::cGameStub() constructor. Change the lines to read:
int cGameStub::DEFAULTSEEDCOUNT = 8;
int cGameStub::DEFAULTRIVALCOUNT = 0;
(b) Let’s make our world tall and thin. We can do this by changing a line in the cGamestub::cGamestub constructor. Take the line _border.set(60.0, 40.0), and change it to _border.set(20.0, 40.0).
(c) We don’t want to start out zoomed in on the world. Find the code for the void cGameStub::initializeViewpoint(cCritterViewer *pviewer) method and comment out two lines.
//pviewer->zoom(4.0);
and
//pviewer->zoom(2.0);
(d) We don’t want the view to move with the player, so find the void cGameStub::initializeView(CPopView *pview) code and comment out a line.
//pview->pviewpointcritter()->setTrackplayer(TRUE);
(a) Before changing the constructor, change the value of a static variable used in the constructor. At the start of the gamestub.cpp file, change the PLAYERHEALTH line to this.
int cGameStub::PLAYERHEALTH = 3;
Now we’re going to add some code to the end of the cCritterStubPlayer::cCritterStubPlayer(cGame *pownergame) code in gamestub.cpp.
(b) We want to use the arrow keys to move our player. Add this line to the end of the constructor. Alternately you could replace the existing setListener line with this line.
setListener(new cListenerArrow());
(c) To make the arrow key motion a little peppier, give the player a higher maximum speed (which is the speed the arrow moves it at). Add this line.
setMaxspeed(30.0); // Careful not to write setMaxSpeed
(c) Limit the player to moving back and forth along the bottom of the screen. This means we want to change the player’s cRealBox _movebox field.
We do this in the player’s constructor. Our framework is set up so that in this code block you can assume that the player’s _movebox has already been set to match the game’s _border box. We now want to use setMoveBox to change the _movebox.
The setMoveBox call takes a cRealBox as argument. The cRealBox constructor we use here takes cVector specifying two opposite corners as arguments, the lower left front corner and the upper right back corner. Add this block of code to the end of the constructor code. What we’re doing here is to move the “high corner” down almost to the bottom of the _border box.
/* At this point the player's _movebox matches the _border it got from pownergame.
Now we want to make the _movebox just be the bottom edge of the world. */
setMoveBox(cRealBox(
_movebox.locorner(),
_movebox.hicorner() - //Move high corner almost to the bottom of world.
(_movebox.ysize()-2*radius())* cVector::YAXIS
));
(d) Another aspect of a Space Invaders game is that the player’s gun always points straight up. We’ll make this change in the cCritterStubPlayer constructor. Change the old cCritterStubPlayer constructor by adding these lines to the bottom of it.
setAttitudeToMotionLock(FALSE);
/* Note that for this line to have its proper effect, you need
to edit the method void cListenerArrow::listen(Real dt, cCritter *pcritter)
inside the listener.cpp file. What you have to do is to change the last
line of that method to have an if condition, so the line reads as follows:
if (pcritter->attitudetomotionlock()) // Need this condition to allow
//a "space invaders" type shooter that always points up
pcritter->copyMotionMatrixToAttitudeMatrix();
*/
setAttitudeTangent(cVector::YAXIS); //Call this AFTER turning off the lock
setAimVector(cVector::YAXIS);
(e) In order for (d) to have the desired effect, you need to make a correction to the listener.cpp file that you have. In this file you have to edit the method void cListenerArrow::listen(Real dt, cCritter *pcritter). What you have to do is to change the last line of that method to have an if condition. Before your change, the line reads
pcritter->copyMotionMatrixToAttitudeMatrix();
After your change, the line should read as follows:
if (pcritter->attitudetomotionlock())
pcritter->copyMotionMatrixToAttitudeMatrix();
Now we make some changes to the bottom of the cCritterStubProp::cCritterStubProp(cGame *pownergame) code in gamestub.cpp.
(a) Let’s have the props automatically be positioned up near the top of the world. Since the prop constructor uses a cGame argument, its baseclass constructor will have set its _movebox to match the game’s _border. To move the critters up to the top of the world, add these lines.
randomizePosition(cRealBox(
_movebox.locorner() +
(_movebox.ysize() - 2*radius()) * cVector::YAXIS,
_movebox.hicorner()
));
(b) Let’s put a force of gravity on the Prop critters. Usually when you have gravity, it’s a good idea to put in some “air friction” as well. Add these lines to the end of the constructor code.
addForce(new cForceGravity());
addForce(new cForceDrag());
(c) Let’s soup up the game by allowing the critters to fall a bit faster. Add this line. You might find the value 8.0 to be a shade too low or high.
setMaxspeed(8.0); //Careful not to write setMaxSpeed
(d) Now let’s try having the Props run away from the bullets. Try adding a line like this. You may not like the effect of this, so its optional.
addForce(new cForceClassEvade(4.0, 1.0, RUNTIME_CLASS(cCritterStubPlayerBullet)));
Each critter has an int _outcode field that is an OR combination of bit flags telling you which, if any, edge of its cRealBox _movebox the critter touched during its last move. The bit flags, which are defined in the realbox.h file, have simple names like BOX_LOY. We will use the _outcode to take action when a prop hits the bottom or the top of the screen.
When one of the prop critters hits the bottom of the screen, we want to kill off the critter and reduce the player’s health by calling its damage method.
If you have called setWrapflag(cCritter::WRAP), then the props might get to the bottom by going around the top. When our props run away from bullets they might sometimes do this. It would unfairly punish the player if let the props get away with that, as then they would be in a position to cross back and game might think they landed on the bottom. Therefore if a prop hits the top of the world we kill it off without charging the player a damage point.
We do all this by changing the cCritterStubProp update method to look like this.
void cCritterStubProp::update(CPopView *pactiveview)
{
cCritter::update(pactiveview); //Always call this first
if (_outcode & BOX_LOY) //Landing damages me
{
pplayer()->damage(1);
die();
}
if (_outcode & BOX_HIY) //So they don't sneak around over the top.
die();
}
Let’s eliminate the feature of cGameStub which rewards the player for bumping into a Prop critter. This means you should comment out this line from within the lines from the cCritterStubPlayer::collide(cCritter *pcritter) code
// setHealth(health() + 1);
(a) In the gamestub.cpp file, try giving yourself prop-seeking missiles for your bullets. Change the code of the cCritterStubPlayer::shoot() as follows.
That is, give your bullets a cForceObjectSeek so they turn into smart missiles that hunt down whichever critter was closest to the line you aimed along. If you think it makes the game too easy or too hard, leave it out or perhaps use a smaller value for the argument 50.0 passed to the cForceObjectSeek constructor.
cCritterBullet* cCritterStubPlayer::shoot()
{
cCritterBullet *pbullet = cCritterArmedPlayer::shoot();
cCritter* paimtarget = pgame()->pbiota()->pickClosest(cLine(position(), aimvector()), this);
/* Find the critter closest to your aiming line. Including this as the second argument
means to exclude yourself from consideration as the closest critter. */
pbullet->addForce(new cForceObjectSeek(paimtarget, 50.0));
return pbullet;
}
(b)You may now find the game is now hard to play because your bullets die at the screen edges and sometimes do this before hitting a prop. Fix this by adding this line to the cCritterStubPlayerBullet::cCritterStubPlayerBullet() constructor.
_dieatedges = FALSE;
(c) A downside of (b) is that you’ll now notice that some silly bullets bounce off the top and get confused and bumble around on the bottom of the world. To fix this, you have to overload the void cCritterStubPlayerBullet::update(CPopView *pactiveview) as follows.
Add this line to the prototype in gamestub.h
virtual void update(CPopView *pactiveview);
Add this code to gamestub.cpp.
void cCritterStubPlayerBullet::update(CPopView *pactiveview)
{
cCritterBullet::update(pactiveview); //Always call base update first
if (_outcode & BOX_LOY || _outcode & BOX_HIY) //Landing damages me
die();
}
(a) To make this more of a game, it’s better to have the action be non-stop. As it presently stands, you can kill off all the attackers. We fix it so each time you kill an attacker, some new ones come in. We do this by adding this code to the bottom of the cGameStub::adjustGameParameters code.
int propcrittercount = pbiota()->count(RUNTIME_CLASS(cCritterStubProp));
if (propcrittercount < _seedcount)
new cCritterStubProp(this); //The constructor automatically adds the critter to the game.
Note that if you’ve killed, say, three props all at once, it will take the game three steps of calling adjustGameParameters to restore the full prop count. This is fine, as visually it’s just as well not to change the game too rapidly.
(b) What about making the game get harder as you play it? This step is optional.
You could keep track of the cGame::score(), and each time this gets larger than some increment size, make the game harder in some way, perhaps by increasing the size of _seedcount.
Or you could add a cCritterStubRival whenever the score passes a certain size, similar to how the Spacewar game adds cCritterUFO. To make this effective you might need to tweak the cCritterStubRival methods a bit.
(20% for Mechanics) Hand in the following: (1) A sheet of paper describing the controls of your game and listing any Special Features you added. (2) Two floppy disks: one disk with a Release Build of the executable in the root directory, and another disk with clean, minimal-sized, buildable source code. Label the disks with your name and with a word to indicate if this is the EXE or the SOURCE disk. You will probably need to WinZip your source to fit it onto a floppy. If the *.zip is larger than 1 Meg you probably haven’t cleaned your source directory properly (or you’ve included a lot of extra big sounds and bitmap files). If your cleaned and zipped source won’t fit on a floppy, check with the professor about alternate disk formats. Emailing your homework to the professor as a gigundo attachment is forbidden! (3) Put disks and paper in a two-pocket folder with your name on it. (4) Put your name on the program caption bar. To change the caption bar, see section 23.9 of this book.
(40% for Basics) Carry out the steps outlined in the series of Space Invaders exercises just above. You don’t necessarily need to use all the exact same parameter values suggested. Get the program working so that the critters are neither too hard nor too easy to hit, the game itself should be neither too hard or too easy. You may need to tweak some params to get it right.
(20% for Improvements) Possibilities: Change the background, use different kinds of sprites, add code to make the game get progressively harder, change the code so that the enemies jiggle back and forth like in the traditional Space Invaders rather than running away from bullets. Add sound effects. Have the enemies change appearance when you hit them before disappearing, maybe have them shatter or show a cSpriteIcon or cSpriteLoop explosion bitmap for a few seconds. Looking at the gamespacewar.cpp or the gamedefender3d.cpp file may provide inspiration.