C++代写:ICS214 Iceman Part3

从头开始实现一款大型的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:

  1. 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.

  2. You must create a class to represent the Iceman in the game.

  3. 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):

1
2
3
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:

1
2
3
4
5
6
7
8
9
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:

1
2
3
4
5
// 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:

1
int B = min(current_level_number / 2 + 2, 9)

G Gold Nuggets in each level, where:

1
int G = max(5-current_level_number / 2, 2)

L Barrels of oil in each level, where:

1
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:

  1. 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.).

  2. 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.

  3. 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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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:

  1. 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)

  2. 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.

  3. The first Protester must be added to the oil field during the very first tick of each level (and any replays of the level).

  4. 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)

  5. 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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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:

  1. 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).
  2. 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.