从头开始实现一款大型的RPG游戏Iceman,第三部分是介绍游戏需要实现的功能。
What Do You Have to Do?
You must create a number of different classes to implement the Iceman game.
Your classes must work properly with our provided classes, and you must not
modify our classes or our source files in any way to get your classes to work
properly. Here are the specific classes that you must create:
- You must create a class called StudentWorld that is responsible for keeping track of your game world (including the oil field) and all of its actors/objects (e.g., the Iceman, Protesters, Boulders, Ice, etc.) that are inside the oil field.
- You must create a class to represent the Iceman in the game.
- You must create classes for Regular Protesters, Hardcore Protesters, Squirts (that the Iceman shoots), Water, Sonar Kits, Gold Nuggets, Ice, Boulders, and Barrels of oil, as well as any additional base classes (e.g., a Goodie base class that’s common to all pick-uppable items like Water, Gold Nuggets, etc., if you need one) that are required to implement the game.
You Have to Create the StudentWorld Class
Your StudentWorld class is responsible for orchestrating virtually all game
play - it keeps track of the whole game world (the Ice of the oil field, and
all of its inhabitants such as Protesters, the Iceman, Boulders, Goodies,
etc.). It is responsible for initializing the game world at the start of the
game, asking all of your game’s actors to do something during each tick of the
game, and destroying all of the actors in the game world when the user loses a
life or when actors disappear (e.g., a Regular Protester leaves the oil field
after being sufficiently annoyed by being repeatedly squirted).
Your StudentWorld class must be derived from our GameWorld class (found in
GameWorld.h) and must implement at least these three methods (which are
defined as pure virtual in our GameWorld class):
virtual int init() = 0;
virtual int move() = 0;
virtual void cleanUp() = 0;
—|—
The code that you write must never call any of these three functions. Instead,
our provided game framework will call these functions for you. So you have to
implement them correctly, but you won’t ever call them yourself in your code.
When a new level starts (e.g., at the start of a game, or when the player
completes a level and advances to the next level), our game framework will
call the init() method that you defined in your StudentWorld class. You don’t
call this function; instead, our provided framework code calls it for you.
Your init() method is responsible for creating the current level’s oil field
and populating it with Ice, Boulders, Barrels of Oil, and Gold Nuggets (we’ll
show you how below), and constructing a virtual representation of the current
level in your StudentWorld class, using one or more data structures that you
come up with. This function must return the value GWSTATUS_CONTINUE_GAME
(defined in GameConstants.h).
The init() method is automatically called by our provided code either (a) when
the game first starts, (b) when the player completes the current level and
advances to a new level (that needs to be initialized), or (c) when the player
loses a life (but has more lives left) and the game is ready to restart at the
current level.
Once a new level has been initialized with a call to your init() method, our
game framework will repeatedly call your StudentWorld’s move() method, at a
rate of roughly 10-20 times per second. Each time your move() method is
called, it must run a single tick of the game. This means that it is
responsible for asking each of the game’s actors (e.g., the Iceman, each
Regular Protester or Hardcore Protester, Boulders, etc.) to try to do
something: e.g., move themselves and/or perform their specified behavior.
Finally, this method is responsible for disposing of (i.e., deleting) actors
(e.g., a Squirt from the Iceman’s squirt gun that has run its course, a
Regular Protester who has left the oil field, a Boulder that has fallen and
crashed into Ice below, etc.) that need to disappear during a given tick. For
example, if a Boulder has completed its fall and disintegrated in the Ice
below, then its state should be set to “dead,” and the after all of the actors
in the game get a chance to do something during the tick, the move() method
should remove that Boulder from the game world (by deleting its object and
removing any reference to the object from the StudentWorld’s data structures).
Your move() method will automatically be called once during each tick of the
game by our provided game framework. You will never call the move() method
yourself.
The cleanup() method is called by our framework when the player loses a life
(e.g., the Iceman’s hit-points reach zero due to being shouted at by one or
more Protesters), or the player successfully completes the current level. The
cleanup() method is responsible for freeing all actors (e.g., all Regular
Protester objects, all Hardcore Protester objects, all Ice and Boulder
objects, the Iceman object, all Goodie objects (like Water, Gold Nuggets,
Barrels of oil), Squirt objects, etc.) that are currently active in the game.
This includes all actors created during either the init() method or introduced
during subsequent game ticks (e.g., a Hardcore Protester that was added to the
oil field during the middle of a level, or a Squirt of water shot by the
Iceman just before they complete the level) that have not yet been removed
from the game.
You may add as many other public or private methods and private member
variables to your StudentWorld class as you like (in addition to the above
three methods, which you must implement).
Your StudentWorld class must be derived from our GameWorld class. Our
GameWorld class provides the following methods for your use:
unsigned int getLives() const;
void decLives();
void incLives();
unsigned int getScore() const;
unsigned int getLevel() const;
void increaseScore(unsigned int howMuch);
void setGameStatText(string text);
bool getKey(int& value);
void playSound(int soundID);
—|—
- getLives() can be used to determine how many lives the player has left.
- decLives() reduces the number of player lives by one.
- incLives() increases the number of player lives by one.
- getScore() can be used to determine the player’s current score.
- getLevel() can be used to determine the player’s current level number.
increaseScore() is used by your StudentWorld class (or you other classes) to
increase the user’s score when the Iceman irritates Protesters with a Squirt,
picks up a Barrel or a Goodie of some sort, or bonks a Protester with a
Boulder, etc. When your code calls this method, you must specify how many
points the player gets (e.g., 100 points for irritating a Regular Protester to
the point where it gives up). This means that the game score is controlled by
our GameWorld object - you must not maintain your own score member variable in
your own class(es).
The setGameStatText() method is used to specify what text is displayed at the
top of the game screen, e.g.:
Lvl: 52 Lives: 3 Hlth: 80% Wtr: 20 Gld: 3 Oil Left: 2 Sonar: 1 Scr: 321000
You’ll pass in a string to this function that specifies the proper stat
values.
getKey() can be used to determine if the user has hit a key on the keyboard to
move the player or to fire. This method returns true if the user hit a key
during the current tick, and false otherwise (if the user did not hit any key
during this tick). The only argument to this method is a variable that will be
filled in with the key that was pressed by the user (if any key was pressed).
If the player does hit a key, the argument will be set to one of the following
values (constants defined in GameConstants.h):
KEY_PRESS_LEFT
KEY_PRESS_RIGHT
KEY_PRESS_UP
KEY_PRESS_DOWN
KEY_PRESS_SPACE
KEY_PRESS_ESCAPE
KEY_PRESS_TAB
‘z’
‘Z’
The playSound() method can be used to play a sound effect when an important
event happens during the game (e.g., a Regular Protester gives up due to being
squirted, or the Iceman picks up a Barrel of oil). You can find constants
(e.g., SOUND_PROTESTER_GIVE_UP) that describe what noise to make inside of the
GameConstants.h file. Here’s how the playSound() method might be used:
// if a Regular Protester reaches zero hit-points and dies
// then make a dying sound
if (theProtesterHasZeroHitPoints())
GameController::getInstance().playSound(SOUND_PROTESTER_GIVE_UP);
—|—
init() Details
Your StudentWorld’s init() method must:
- A. Initialize the data structures used to keep track of your game’s virtual world
- B. Construct a new oil field that meets the requirements stated in the section below (filled with Ice, Barrels of oil, Boulders, Gold Nuggets, etc.)
- C. Allocate and insert a valid Iceman object into the game world at the proper location
Your init() method must construct a representation of your virtual world and
store this in your StudentWorld object. It is required that you keep track of
all of the game objects (e.g., actors like Regular Protesters, Gold Nuggets,
Barrels of oil, Sonar Kits , Boulders, etc.) with the exception of Ice objects
and the Iceman object in a single STL collection like a list or vector. To do
so, we recommend using a vector of pointers to your game objects, or a list of
pointers to your game objects.
If you like, your StudentWorld class may keep a separate pointer to the Iceman
rather than keeping a pointer to the Iceman object in this collection along
with the other game objects.
Similarly, you may store pointers to all Ice objects in a different data
structure than the list/vector used for your other game actors (i.e., those
objects that actually do something during each tick) if you like. Hint:
Keeping all of your Ice objects in a separate 2-D array of Ice pointers will
speed things up.
You must not call the init() method yourself. Instead, this method will be
called by our framework code when it’s time for a new game to start (or when
the player completes a level, or needs to restart a level).
Contents of Each Oil Field
First, you must completely fill rows 0 through 59 of the oil field with Ice
objects, with the exception of a vertical mine shaft in the middle of the
field. Your Ice class, which is used to create these Ice objects, must be
derived in some way from our GraphObject class, and have an imageID of
IMID_ICE. A Ice object is the simplest type of game object in Iceman. All it
does is display itself to the screen - it doesn’t move or perform any other
actions on its own. You’ll find more details on the requirements for the Ice
object in its section below.
As mentioned above, a single tunnel, 4 squares wide (occupying columns 30-33
of the oil field), and 56 squares deep (occupying rows 4-59) must lead from
the surface of the mine down into its depths, and must be devoid of any Ice
objects.
The Iceman must start the game at location x=30, y=60, just atop the tunnel,
at the start of each level (and after the Iceman loses a life and restarts a
level).
You must distribute the following game objects randomly in the oil field:
B Boulders in each level, where:
int B = min(current_level_number / 2 + 2, 9)
—|—
G Gold Nuggets in each level, where:
int G = max(5-current_level_number / 2, 2)
—|—
L Barrels of oil in each level, where:
int L = min(2 + current_level_number, 21)
—|—
The starting level # is level 0, so level 0 would have 2 Boulders, 5 Nuggets
and 2 Barrels of oil. Or, for example, level 2 would have 3 Boulders, 4
Nuggets and 4 Barrels of oil.
No distributed game object may be within a radius (Euclidian distance) of 6
squares of any other distributed game object. For example, if a Boulder were
distributed to x=1,y=2, then a Nugget could not be distributed to x=6,y=4
because the two would be 5.39 squares away (less than or equal to 6 squares
away). However the same Nugget could be distributed to x=6,y=6 because this
would be 6.4 squares away (more than 6.0 squares away). Nuggets and Oil
Barrels must be distributed between x=0,y=0 and x=60,y=56 inclusive, meaning
that the lower-left corner of any such object must fall within this rectangle.
Boulders must be distributed between x=0,y=20 and x=60,y=56, inclusive (so
they have room to fall).
All distributed Gold Nuggets must start in a state that is pickup-able by the
Iceman, but not by Protesters. All distributed Gold Nuggets must start out in
a permanent state.
All distributed Gold Nuggets and Barrels of oil must start out in an invisible
state (not displayed on the screen). They will become visible when the Iceman
either gets near them (this is detailed within the specs for Nuggets and
Barrels) or if the Iceman uses a sonar charge to scan the nearby Ice around
him.
There must not be any Ice overlapping the 4x4 square region of each Boulder,
so you’ll need to clear this Ice out when you place your Boulders within the
oil field (or place your Boulders first, then avoid placing Ice objects where
the Boulders are located). The other items must have the area under their 4x4
image completely filled with Ice (in other words, these items must not be
distributed at the surface of the oil field or within the mine shaft).
Once your init() method has distributed all of the Ice, Iceman, and game
objects throughout the oil field, it should return so our game framework can
start the game.
move() Details
The move() method must perform the following activities:
- It must update the status text on the top of the screen with the latest information (e.g., the user’s current score, the remaining bonus score for the level, etc.).
- It must ask all of the actors that are currently active in the game world to do something (e.g., ask a Regular Protester to move itself, ask a Boulder to see if it needs to fall down because Ice beneath it was dug away, give the Iceman a chance to move up, down, left or right, etc.).
* A. If an actor does something that causes the Iceman to give up, then the move() method should immediately return GWSTATUS_PLAYER_DIED.
* B. If the Iceman collects all of the Barrels of oil on the level (completing the current level), then the move() method should immediately play a sound of SOUND_FINISHED_LEVEL and then return a value of GWSTATUS_FINISHED_LEVEL. - It must then delete any actors that need to be removed from the game during this tick and remove them from your STL container that tracks them. This includes, for example:
* A Protester that has run to the upper-right-hand corner of the oil field after being sufficiently annoyed (by being squirted by a Squirt or hit by a Boulder) and is ready to “leave” the oil field
* A Boulder that has fallen down a shaft and disintegrated upon hitting the bottom (or another Boulder)
* A Gold Nugget that has been picked up by the Iceman or a Protester and is therefore no longer in the oil field
* A Water Pool that has dried up after a period of time.
* A Squirt from the Iceman’s squirt gun once it’s reached the maximum distance it can travel.
* Etc.
The move() method must return one of three different values when it returns at
the end of each tick (all are defined in GameConstants.h):
GWSTATUS_PLAYER_DIED
GWSTATUS_CONTINUE_GAME
GWSTATUS_FINISHED_LEVEL
The first return value indicates that the player died during the current tick,
and instructs our provided framework code to tell the user the bad news and
restart the level if the player has more lives left. If your move() method
returns this value, then our framework will call your cleanup() method to
destroy the level, then call your init() method to reinitialize the level from
scratch. Assuming the player has more lives left, they will be prompted to
continue their game, and our framework will then begin calling your move()
method over and over, once per tick, to let the user play the level again.
The second return value indicates that the tick completed without the player
dying BUT the player has not yet completed the current level. Therefore the
game play should continue normally for the time being. In this case, the
framework will advance to the next tick and call your move() method again.
The final return value indicates that the player has completed the current
level (that is, gathered all of the Barrels of oil on the level). If your
move() method returns this value, then the current level is over, and our
framework will call your cleanup() method to destroy the level, advance to the
next level, then call your init() method to prepare that level for play, etc
IMPORTANT NOTE: The “skeleton” code that we provide to you is hard-coded to
return a GWSTATUS_PLAYER_DIED status value from our dummy version of the
move() method. Unless you change this value to GWSTATUS_CONTINUE_GAME your
game will not display anything on the screen! So if your screen just shows up
black once the user starts playing, you’ll know why!
Here’s pseudocode for how the move() method might be implemented:
int StudentWorld::move() {
// Update the Game Status Line
updateDisplayText(); // update the score/lives/level text at screen top
// The term “Actors” refers to all Protesters, the player, Goodies,
// Boulders, Barrels of oil, Holes, Squirts, the Exit, etc.
// Give each Actor a chance to do something
for each of the actors in the game world
{
if (actor[i] is still active/alive)
{
// ask each actor to do something (e.g. move)
tellThisActorToDoSomething(actor[i]);
if (theplayerDiedDuringThisTick() == true)
return GWSTATUS_PLAYER_DIED;
if (theplayerCompletedTheCurrentLevel() == true)
{
return GWSTATUS_FINISHED_LEVEL;
}
}
}
// Remove newly-dead actors after each tick
removeDeadGameObjects(); // delete dead game objects
// return the proper result
if (theplayerDiedDuringThisTick() == true)
return GWSTATUS_PLAYER_DIED;
// If the player has collected all of the Barrels on the level, then
// return the result that the player finished the level
if (theplayerCompletedTheCurrentLevel() == true)
{
playFinishedLevelSound();
return GWSTATUS_FINISHED_LEVEL;
}
// the player hasn’t completed the current level and hasn’t died
// let them continue playing the current level
return GWSTATUS_CONTINUE_GAME;
}
—|—
Give Each Actor a Chance to Do Something
During each tick of the game each active actor must have an opportunity to do
something (e.g., move around, shoot, etc.). Actors include the Iceman, Regular
Protesters, Hardcore Protesters, Boulders, Gold Nuggets, Barrels of oil,
Water, Squirts from the Iceman’s squirt gun, and Sonar Kits.
Your move() method must enumerate each active actor in the oil field (i.e.,
held by your StudentWorld object) and ask it to do something by calling a
method in the actor’s object named doSomething(). In each actor’s
doSomething() method, the object will have a chance to perform some activity
based on the nature of the actor and its current state: e.g., a Regular
Protester might move one step up, the Iceman might shoot a Squirt of water, a
Boulder may fall down one square, etc.
To help you with testing, if you press the f key during the course of the
game, our game controller will stop calling move() every tick; it will call
move() only when you hit a key (except the r key). Freezing the activity this
way gives you time to examine the screen, and stepping one move at a time when
you’re ready helps you see if your actors are moving properly. To resume
regular game play, press the r key.
Add New Actors During Each Tick
During each tick of the game in your move() method, you may need to add new
Protesters (Regular or Hardcore) and/or Goodies (Water Pools or Sonar Kits) to
the oil field. You must use the following approach to decide whether to add
these new actors to the oil field:
- A new Protester (Regular or Hardcore) may only be added to the oil field after at least T ticks have passed since the last Protester of any type was added, where:
int T = max(25, 200 - current_level_number)
- The target number P of Protesters that should be on the oil field is equal to:
int P = min(15, 2 + current_level_number * 1.5)
However, based on #1 above, you can only add a new Protester to the oil field every T ticks, so the actual number of Protesters on the oil field at any particular time may be less than the target number P. - The first Protester must be added to the oil field during the very first tick of each level (and any replays of the level).
- Assuming the appropriate number of ticks T has elapsed since the last Protester was added to the oil field, AND the current number of Protesters on the oil field is less than P, then you must add a new Protester to the oil field during the current tick. All Protesters must start at location x=60,y=60 on the screen. The odds of the Protester being a Hard Core Protester (vs. a Regular Protester) must be determined with this formula:
int probabilityOfHardcore = min(90, current_level_number * 10 + 30)
- There is a 1 in G chance that a new Water Pool or Sonar Kit Goodie will be added to the oil field during any particular tick, where:
int G = current_level_number * 25 + 300
Assuming a new Goodie should be added, there is a 1/5 chance that you should add a new Sonar Kit, and a 4/5 chance you should add a Water Goodie.
Each new Sonar Kit must be added at x=0, y=60 on the screen.
Each new Water Goodie must be added to a random ice-less spot in the oil
field. Water may only be added to a location if the entire 4x4 grid at that
location is free of Ice.
Remove Dead Actors after Each Tick
At the end of each tick your move() method must determine which of your actors
are no longer alive, remove them from your STL container of active actors, and
delete their objects (so you don’t have a memory leak). For example, once a
Barrel is picked up by the Iceman during a tick, it should be marked as “not
active.” After giving all of the actors a chance to move during the current
tick, your move() method would then discover this inactive Barrel (as well as
any other objects that have become inactive during this tick) and remove its
object pointer from your StudentWorld’s container of active objects. Finally,
your move() method should delete the object (using the C++ delete command) to
free up room in memory for future actors that will be introduced later in the
game. (Hint: Each of your actors could have a member variable indicating
whether or not it is still active/alive!)
Updating the Display Text
Your move() method must update the game statistics at the top of the screen
during every tick by calling the setGameStatText() method that we provide in
our GameWorld class. You could do this by calling a function like the one
below from your StudentWorld’s move() method:
void setDisplayText()
{
int level = getCurrentGameLevel();
int lives = getNumLivesLeft();
int health = getCurrentHealth();
int squirts = getSquirtsLeftInSquirtGun();
int gold = getPlayerGoldCount();
int barrelsLeft = getNumberOfBarrelsRemainingToBePickedUp();
int sonar = getPlayerSonarChargeCount(); int score = getCurrentScore();
// Next, create a string from your statistics, of the form:
// Lvl: 52 Lives: 3 Hlth: 80% Wtr: 20 Gld: 3 Oil Left: 2 Sonar: 1 Scr: 321000
string s = someFunctionYouUseToFormatThingsNicely(level, lives, health, squirts, gold, barrelsLeft, sonar, score);
// Finally, update the display text at the top of the screen with your
// newly created stats setGameStatText(s);
// calls our provided GameWorld::setGameStatText
}
—|—
Your status line must meet the following requirements:
- Each field must be exactly as wide as shown in the example above:
* a. The Lvl field must be 2 digits long, with leading spaces (e.g., “1”, where _ represents a space).
* b. The Lives field should be 1 digit long (e.g., “2”).
* c. The Hlth field should be 3 digits long and display the player’s health percentage (not its hit-points!), with leading spaces, and be followed by a percent sign (e.g., “_70%”).
* d. The Wtr field should be 2 digits long, with a leading space as required (e.g., “ 7”).
* e. The Gld field should be 2 digits long, with a leading space as required (e.g., “_ 3”).
* f. The Oil Left field should be 2 digits long, with a leading space as required (e.g., “_ 4”).
* g. The Sonar field should be 2 digits long, with a leading space as required (e.g., “_ 2”).
* h. The Scr must be exactly 6 digits long, with leading zeros (e.g., 003124). - Each statistic must be separated from the last statistic by two spaces. For example, between the “000100” of the score and the “L” in “Level” there must be exactly two spaces.
cleanUp() Details
When your cleanUp() method is called by our game framework, it means that the
Iceman lost a life (e.g., his hit-points/annoyance tolerance reached zero due
to being shouted at or being bonked by a Boulder), or has completed the
current level. In this case, every actor in the entire oil field (the Iceman
and every Protester, Goodies like Nuggets, Sonar Kits and Water, Barrels of
oil, Boulders, Ice, etc.) must be deleted and removed from your StudentWorld’s
container(s) of active objects, resulting in an empty oil field. If the player
has more lives left, our provided code will subsequently call your init()
method to create a new oil field and the level will then continue from scratch
with a brand new set of actors (including a newly-generated Iceman!).
You must not call the cleanUp() method yourself when the player’s hit points
go to zero. Instead, this method will be called by our code.