This particular page contains the first part of a chapter that presents a relatively elaborate example of the use of class inheritance, polymorphism, dynamic binding etc. The example is a "Moria" ("Rogue") style game.
This chapter is another that presents just a single example program illustrating how to apply features of the C++ language. In this case, the focus is on class inheritance and the use of polymorphism. The application program is a simple game.
In the days before "Doom", "Marathon", and "Dark Forces", people had to be content with more limited games! The basic game ideas were actually rather similar. The player had to "explore a maze", "find items", and avoid being eliminated by the "monsters" that inhabited the maze. However, details of graphics and modes of interaction were more limited than the modern games. A maze would be represented as an array of characters on the screen (walls represented by '#' marks, staircases between levels by characters like '<' and so forth). Single character commands allowed the player to move the maze-explorer around within the boundaries of the maze. "Rogue" and "Moria" are typical examples of such games; you may find that you have copies on old floppies somewhere.
You aren't quite ready to put Lucas Arts out of business by writing a replacement for "Dark Forces", but if you have got this far you can write your own version of Moria.
The example code given here is simplified with lots of scope for elaboration. It employs a single level maze (or "dungeon"). A map of the dungeon is always displayed, in full, on the screen. The screen size limits the size of the map; having the complete map displayed simplifies play. (Possible elaborations include showing only those parts of the map already explored and "scrolling" maps that are too large to fit on a screen.) The map and other data are displayed using the same system features as used in the "cursor graphics" examples from Chapter 12.
The map, details of the items to be found, and the monsters that are to be avoided (or destroyed) are taken from text file input. Again, this is a simplification. Games like Moria usually generate new maps for every game played.
The playing mechanism is limited. The user is prompted for a command. After a user command has been processed, all the "monsters" get a chance to "run". A "Monster:: Run()" function captures the essence of "monsterdom" i.e. a desire to eliminate the human player.
Naturally, such a game program requires numerous obvious objects - the "monsters", the "items" that must be collected, the "player" object, the dungeon object itself. In addition there will have to be various forms of "window" object used to display other information. Since there will be many "monsters" and many "items", standard collection classes will be needed.
There are two separate class hierarchies as well as a number of independent classes. One limited class hierarchy defines "windows". There is a basic "window" class for display of data with a couple of specializations such as a window used to output numeric data (e.g. player's "health") and a window to get input. (These "window" classes are further elaborated in the next chapter.)
There is also a hierarchy of "things found in the dungeon". Some, like the "items" that must be collected, are passive, they sit waiting until an action is performed on them. Others, like the player and the monsters, are active. At each cycle of the game, they perform some action.
Naturally, there are many different kinds of monster. Each different kind (subclass) employs a slightly different strategy when trying to achieve their common objective of "eliminating the player". This is where the polymorphism comes in. The "dungeon" object will work with a collection of monsters. When it is the monsters' turn, the code has a loop that lets each monster "run" (Monster *m; ...; m->Run();). The pointer m is polymorphic - pointing to different forms of monster at different times. Each different form has its own interpretation of Run().
The "dungeon" game program is to:
This "preliminaries" section explores a few aspects of the program that seem pretty much fixed by the specification. The objective is to fill out some details and get a few pointers to things that should be considered more thoroughly.
For example the specification implies the existence of "class Dungeon", "class Player", "class Monster", a class for "collectable items" and so forth. We might as well jot down a few initial ideas about these classes, making a first attempt to answer the perennial questions "What does class X do? What do instances of class X own?". Only the most important responsibilities will get identified at this stage; more detailed consideration of specific aspects of the program will result in further responsibilities being added. Detailed analysis later on will also show that some of the classes are interrelated, forming parts of a class hierarchy.
Other issues that should get taken up at this preliminary stage are things like the input files and the form of the main program. Again, they are pretty much defined by the specification, but it is possible to elaborate a little.
We can start with the easy parts - like the main() function! This is obviously going to have the basic form "create the principal object, tell it to run":
int main() { Dungeon *d; d = new Dungeon; Prompt user for name of file and read in name ... d->Load(aName); int status = d->Run(); Terminate(status); return 0; }
The principal object is the "Dungeon" object itself. This has to load data from a file and run the game. When the game ends, a message of congratulations or commis-erations should be printed. The Dungeon::Run() function can return a win/lose flag that can be used to select an appropriate message that is then output by some simple Terminate() function.
The files are to be text files, created using some standard editor. They had better specify the size of the map. It would be simplest if the map itself were represented by the '#' and ' ' characters that will get displayed. If the map is too large, the top-left portion should be used.
Following the map, the file should contain the data necessary to define the player, the collectable items, and the monsters. The program should check that these data define exactly one player object and at least one collectable item. The program can simply terminate if data are invalid. (It would help if an error message printed before termination could include some details from the line of input where something invalid was found.)
Collectable items and other objects can be represented in the file using a character code to identify type, followed by whatever number of integers are needed to initialize an object of the specified type. A sentinel character, e.g. 'q', can mark the end of the file.
A plausible form for an input file would be:
width and height (e.g 70 20) several (20) lines of (70) characters, e.g. ##################### ... ... ########### # # # ... ... # # # ######## # # ... ... ####### # dungeon items h 30 18 ... human (i.e. player), coords, other data w 2 2 10 ... wandering monster, coords, ... ... $ 26 6 0 30 0 collectable item, coords, values q end markAny additional details can be resolved once the objects have been better characterized.
Consideration of the main() function identified two behaviours required of the Dungeon object: loading a file, and running the game.
The Dungeon::Load() function will be something along the following lines:
Dungeon::Load Open file with name given Load map Load other data Dungeon:: load map read size loop reading lines of characters that define the rows of the map Dungeon:: load other data read type character while character != 'q' create object of appropriate type tell it to read its own data if it is a monster, add to monster collection if it is a collectable, add to collectable items collection if player, note it (make sure no existing player) check that the data defined some items to collect
The Dungeon::Run() function could have the following general form:
Dungeon::Run() Finalize setting up of displays Draw initial state while(player "alive") player "run" if(all collectables now taken) break; for each Monster m in monster collection m->Run(); return (player "alive");
The displays must be set up. Obviously, the Dungeon object owns some window objects. (Some might belong to the Player object; this can be resolved later.) The Dungeon object will get primary responsibility for any work needed to set up display structures.
The main loop has two ways of terminating - "death" of player, and all collectable objects taken. The game was won if the player is alive at the end.
The Dungeon object owns the collection of monsters, the collection of collectable items, and the player object. Collections could use class List or class DynamicArray.
The Player object will need to access information held by the Dungeon object. For example, the Player object will need to know whether a particular square is accessible (i.e. not part of a wall), and whether that square is occupied by a collectable item or a monster. When the Player takes a collectable item, or kills a monster, the Dungeon should be notified so that it can update its records. Similarly, the monsters will be interested in the current position of the Player and so will need access to this information.
Consequently, in addition to Load() and Run(), class Dungeon will need many other member functions in its public interface - functions like "Accessible()", and "Remove Monster()". The full set of member functions will get sorted out steadily as different aspects of the game are considered in detail.
Most "dungeon items" will need to interact with the Dungeon object in some way or other. It would be best if they all have a Dungeon* data member that gets initialized as they are constructed.
The Player object's main responsibility will be getting and interpreting a command entered by the user.
Commands are input as single characters and define either movements or, in this game, directional applications of destructive "magic". The characters 1...9 can be used to define movements. If the keyboard includes a number pad, the convenient mapping is 7 = "north west", 8 = "north", 9 = "north east", 4 = "west" and so forth (where "north" means movement toward the top of the screen and "west" means movement leftwards). Command 5 means "no movement" (sometimes a user may want to delay a little, e.g. to let the player object recover from injury).
The "magic action" commands can use the keys q, w, e, a, d, z, x, and c (on a standard QWERTY keyboard, these have the same relative layout and hence define the same directional patterns as the keys on the numeric keypad).
The main Player::Run() function will be something like:
Player::Run() char ch = GetUserCommand(); if(isdigit(ch)) PerformMovementCommand(ch); else PerformMagicCommand(ch); UpdateState(); ShowStatus();
It will involve several auxiliary (private) member functions of class Player.
A GetUserCommand() function can arrange to read the input. Input is echoed at the current location of the cursor. This could mess up the map display. Consequently it will be necessary to position the cursor prior to reading a command character. This work of cursor positioning and actual data input will involve interactions with window object(s).
A function UpdateState() can deal with the business about a Player object's health and manna levels increasing. A ShowStatus() function can keep the displays current; again this will involve interactions with windows.
The Perform... functions will involve interactions with the Dungeon object, and possibly other objects as well.
The collectable items could be made instances of a class Collectable. It does not seem that there will be any differences in their behaviours, so there probably won't be any specialized subclasses of class Collectable. At this stage, it doesn't appear as if Collectable objects will do much at all.
They have to draw themselves when asked (presumably by the Dungeon object when it is organizing displays); they will own a character that they use to represent themselves. They will also need to own integer values representing the amounts by which they change the Player object's health etc when they get taken. Some access functions will have to exist so that the Player object can ask for the relevant data.
A monster object moving onto the same point as a Collectable object will hide it. When the monster object moves away, the Collectable object should be redrawn. The Dungeon object had better arrange to get all Collectable objects draw themselves at regular intervals; this code could be added to the while() loop in Dungeon::Run().
As explained in the dungeon game specification, the basic behaviour of a Monster is to attack whenever possible, otherwise to advance toward the Player when this is possible, otherwise to continue with some "normal action". This behaviour could be defined in the Monster::Run() function which would involve a number of auxiliary functions:
Monster::Run() if(CanAttack()) Attack(); else if(CanDetect()) Advance(); else NormalMove();
Different subclasses of class Monster can specialize the auxiliary functions so as to vary the basic behaviour. Naturally, these functions will be declared as virtual. Auxiliary private member functions used by Run()
Default definitions are possible for some member functions. The default CanAttack() function should return true if the Player object is adjacent. The default Attack() function would tell the Player object that it has been hit for a specified number of points of damage. The default implementations for the other functions could all be "do nothing" (i.e. just an empty body { } for Advance() and NormalMove() and a return 0 for CanDetect()).
Checking adjacency will involve getting a pointer to the Player object (this can be provided by the Dungeon object) and then asking the Player for its position. It might be worth having some simple class to represent (x, y) point coordinates. A Monster object could have an instance of class Pt to represent its position. The Player could return its coordinates as an instance of class Pt. The first Pt could be asked whether it is adjacent to the second.
Previous experience with practical windowing systems has influenced the approach developed here for handling the display. As illustrated in Figure 29.2, the display system uses class WindowRep and class Window (and its specialized subclasses).
Actual communication with the screen is the responsibility of a class WindowRep (Window Representation). Class WindowRep encapsulates all the sordid details of how to talk to an addressable screen (using those obscure functions, introduced in Chapter 12, like cgotoxy(x,y,stdout);). In addition, it is responsible for trying to optimize output to the screen. When the WindowRep object gets a request to output a character at a specific point on the screen, it only performs an output operation if the character given is different from that already shown. In order to do this check, the WindowRep object maintains a character array in memory that duplicates the information currently on the screen.
The program will have exactly one instance of class WindowRep. All "window" objects (or other objects) that want to output (or input) a character will have to interact with the WindowRep object. (There can only be one WindowRep object in a program because there is only one screen and this screen has to have a unique owner that maintains consistency etc.)
A class for which there can only be a single instance, an instance that must be accessible to many other objects, an instance that may have to create auxiliary data structures or perform some specialized hardware initialization - this is a common pattern in programs. A special term "singleton class" has been coined to describe this pattern. There are some standard programming "cliches" used when coding such classes, they are followed in this implementation.
The unique WindowRep object used in a program provides the following services:
There is another specialized static (class) function, Instance(). This handles aspects of the "singleton pattern" (programming cliche) as explained in the implementation (Section 29.3). Essentially, the job of this function is to make certain that there is an instance of class WindowRep that is globally available to any other object that needs it (if necessary, creating the program's unique WindowRep object).
Window objects, instances of class Window or its subclasses, are meant to be things that own some displayable data (an array of characters) and that can be "mapped onto the screen". A Window object will have coordinates that specify where its "top-left" corner is located relative to the screen. (In keeping with most cursor-addressable screen usage, coordinate systems are 1-based rather than 0-based so the top left corner of the screen is at coordinate (1,1).) Window objects also define a size in terms of horizontal and vertical dimensions. Most Window objects are "framed", their perimeters are marked out by '-', '|', and '+' characters (as in Figure 29.1). A Window may not fit entirely on the screen (the fit obviously depends on the size and the origin). The WindowRep object resolves this by ignoring output requests that are "off screen".
Window objects have their own character arrays for their displayable content. Actually, they have two character arrays: a "background array" and a "current array". When told to "draw" itself, a Window object executes a function involving a double loop that takes characters from the "current array", works out where each should be located in terms of screen coordinates (taking into account the position of the Window object's top-left corner) and requests the WindowRep object to display the character at the appropriate point.
The "background array" defines the initial contents of the window (possibly all blank). Before a window is first shown on the screen, its current array is filled from the background. A subsequent "clear" operation on a specific point in the window, resets the contents of the current window to be the same as the background at the specified point.
A specific background pattern can be loaded into a window by setting the characters at each individual position. In the dungeon game, the Dungeon object owns the window used to display the map; it sets the background pattern for that window to be the same as its map layout.
The Window class has the following public functions:
The class requires a few auxiliary member functions. For example, the coordinates passed to functions like Set() must be validated.
A Window owns its dimension data and its arrays. These data members should be protected; subclasses will require access to these data.
The functionality of class Window will be extended in its subclasses. However the subclasses don't change the existing functions like ShowAll(). Consequently, these functions are not declared as virtual.
The relationships between class Window and its subclasses, and class Monster and its subclasses, are subtly different. The subclasses of Window add functionality to a working class, but don't change its basic behaviours. Consequently, the member functions of class Window are non-virtual (apart from the destructor). Class Monster defines a general abstraction; e.g. all Monster object can execute some "NormalMove" function, different subclasses redefine the meaning of "NormalMove". Many of the member functions of class Monster are declared as virtual so as to permit such redefinition. Apart from the differences with respect to the base class member function being virtual or non-virtual, you will also see differences in the accessibility of additional functions defined by subclasses. When inheritance is being used to extend a base class, many of the new member functions appear in the public interface of the subclass. When inheritance is being used to specialize an existing base class, most of the new functions will be private functions needed to implement changes to some existing behaviour. Both styles, "inheritance for extension" and "inheritance for redefinition", are common.
This program has two subclasses for class Window: NumberItem and EditText. Instances of class NumberItem are used to display numeric values; instances of class EditText can be used to input commands. As illustrated in Figure 29.3, these are displayed as framed windows that are 3 rows deep by n-columns wide. The left part of the window will normally contain a textual label. The right part is used to display a string representing a (signed) numeric value or as input field where input characters get echoed (in addition to being stored in some data buffer owned by the object).
class NumberItem : public Window { public: NumberItem(int x, int y, int width, char *label, long initval = 0); void SetVal(long newVal); long GetVal() { return fVal; } private: void SetLabel(int s, char*); void ShowValue(); long fVal; int fLabelWidth; };
In addition to the character arrays and dimension data that a NumberItem object already has because it is a kind of Window, a NumberItem owns a long integer holding the value to be displayed (and, also, an integer to record the number of characters used by the label so that numeric outputs don't overwrite the label). What does a NumberItem do?
The constructor for class NumberItem completes the normal initialization processes of class Window. The auxiliary private member function SetLabel() is used to copy the given label string into the background array. The inherited PrepareContent() function loads the current array from the background and adds the frame. Finally, using the auxiliary ShowValue() function, the initial number is converted into characters that are added to the current image array.
Once constructed, a NumberItem can be used by the program. Usually, usage will be restricted to just three functions - GetVal() (return fVal;), SetVal() (changes fVal and uses ShowValue() to update the current image array), and ShowAll() (the function, inherited from class Window, that gets the window displayed).
The primary responsibility of an EditText object is getting character input from the user. In the dungeon game program, individual input characters are required as command characters. More generally, the input could be a multicharacter word or a complete text phrase.
An EditText object should be asked to get an input. It should be responsible for getting characters from the WindowRep object (while moving the cursor around to try to get the echoed characters to appear at a suitable point on the screen). Input characters should be added to a buffer maintained by the EditText object. This input process should terminate when either a specified number of characters has been received or a recognizably distinct terminating character (e.g. 'tab', 'enter') is input. (The dungeon program can use such a more generalized EditText object by specifying that the input operation is to terminate when a single character has been entered). Often, the calling program will need to know what character terminated the input (or whether it was terminated by a character limit being reached). The input routine can return the terminating character (a '\0' could be returned to indicate that a character count limit was reached).
The EditText class would have to provide an access function that lets a caller read the characters in its buffer.
The class declaration for class EditText is:
class EditText: public Window { public: EditText(int x, int y, int width, char *label, short size); void SetVal(char*); char *GetVal() { return fBuf; } char GetInput(); private: void SetLabel(int s, char*); void ShowValue(); int fLabelWidth; char fBuf[256]; int fSize; int fEntry; };
In addition to the data members inherited from class Window, a EditText owns a (large) character buffer that can be used to store the input string, integers to record the number of characters entered so far and the limit number. Like the NumberItem, an EditText will also need a record of the width of its label so that the input field and the label can be kept from overlapping. What does a NumberItem do?
The constructor for class EditText completes the normal initialization processes of class Window. The auxiliary private member function SetLabel() is used to copy the given label string into the background array. The inherited PrepareContent() function loads the current array from the background and adds the frame. The buffer, fBuf, can be "cleared" (by setting fBuf[0] to '\0')..
The only member functions used in most programs would be GetInput(), GetVal() and ShowAll(). Sometimes, a program might want to set an initial text string (e.g. a prompt) in the editable field (function SetVal()).
Class Monster is meant to be an abstraction; the real inhabitants of the dungeon are instances of specialized subclasses of class Monster.
Class Monster has to provide the following functions (there may be others functions, and the work done in these functions may get expanded later, this list represents an initial guess):
The example implementation has three specializations: Ghost, Patrol, and Wanderer. These classes redefine member functions from class Monster as needed.
A Ghost is a Monster that:
Class Ghost needs to redefine only the Advance() and CanDetect() functions. Since a Ghost does not require any additional data it does not to change the Read() function.
A Patrol is a Monster that:
The patrol route should be defined as a sequence of points. These will have to be read from the input file and so class Patrol will need to extend the Monster:: Read() function. The Patrol::Read() function should check that the given points are adjacent and that all are accessible (within the bounds of the dungeon and not blocked by walls). Class Patrol will need to define extra data members to hold the route data. It will need an array of Pt objects (this can be a fixed sized array with some reasonable maximum length for a patrol route), an integer specifying the number of points in the actual route, an integer (index value) specifying which Pt in the array the Patrol is currently at, and another integer to define the direction that the Patrol is walking. (The starting point given for the Patrol will be the first element of the array. Its initial moves will cause it to move to successive elements of the Pt array; when it reaches the last, it can retrace its path by having the index decrease through the array.)
Wanderer is a Monster that:
A Wanderer will need to remember its current direction of movement so that it can keep trying to go in the same direction. Integer data members, representing the current delta-x, delta-y changes of coordinate, could be used. Commonalities between class Player and class Monster
There are similarities between the Monster and Player classes. Both classes define things that have a Pt coordinate, a health attribute and a strength attribute. There are similarities in behaviours: both the Player and the Monsters read initial data from file, get told that they have been hit, get asked whether they are still alive, get told to draw themselves (and erase themselves), have a "move" behaviour that involves erasing their current display, changing their Pt coordinate and then redrawing themselves. These commonalities are sufficient to make it worth defining a new abstraction, "active item", that subsumes both the Player and the Monsters.
This process of abstracting out commonalities can be repeated. There are similarities between class Collectable and the new class ActiveItem. Both are things with Pt coordinates, draw and erase behaviours; they respond to queries about where they are (returning their Pt coordinate). These common behaviours can be defined in a base class: DungeonItem.
The DungeonItem class hierarchy used in the implementation is shown in Figure 29.4.
Completion of the design stage involves coming up with the class declarations of all the classes, possibly diagramming some of the more complex patterns of interaction among instances of different classes, and developing algorithms for any complicated functions.
The finalised declaration for class Dungeon is:
class Dungeon { public: Dungeon(); ~Dungeon(); void Load(const char filename[]); int Run(); int Accessible(Pt p) const; Window *Display(); Player *Human(); int ValidPoint(Pt p) const; Monster *M_at_Pt(Pt p); Collectable *PI_at_Pt(Pt p); void RemoveProp(Collectable *pi); void RemoveM(Monster *m); int ClearLineOfSight(Pt p1, Pt p2, int max, Pt path[]); private: int ClearRow(Pt p1, Pt p2, int max, Pt path[]); int ClearColumn(Pt p1, Pt p2, int max, Pt path[]); int ClearSemiVertical(Pt p1, Pt p2, int max, Pt path[]); int ClearSemiHorizontal(Pt p1, Pt p2, int max, Pt path[]); void LoadMap(ifstream& in); void PopulateDungeon(ifstream& in); void CreateWindow(); DynamicArray fProps; DynamicArray fInhabitants; Player *fPlayer; char fDRep[MAXHEIGHT][MAXWIDTH]; Window *fDWindow; int fHeight; int fWidth; };
The Dungeon object owns the map of the maze (represented by its data elements fDRep[][], fHeight, and fWidth). It also owns the main map window (fDWindow), the Player object (fPlayer) and the collections of Monsters and Collectables. Data members that are instances of class DynamicArray are used for the collections (fInhabitants for the Monsters, fProps for the Collectables).
The Load() and Run() functions are used by the main program. Function Load() makes uses of the auxiliary private member functions LoadMap() and PopulateDungeon(); these read the various data and create Monster, Collectable, and Player object(s) as specified by the input. The auxiliary private member function CreateWindow() is called from Run(); it creates the main window used for the map and sets its background from information in the map.
Access functions like Display() and Human() allow other objects to get pointers to the main window and the Player object. The ActiveItem objects that move are going to need access to the main Window so as to tell it to clear and set the character that is to appear at a particular point.
The ValidPoint() function checks whether a given Pt is within the bounds of the maze and is not a "wall".
The functions M_at_Pt() and PI_at_Pt() involve searches through the collections of Monsters and Collectables respectively. These function return the first member of the collection present at a Pt (or NULL if there are no objects at that Pt). The Remove... function eliminate members of the collections.
Class Dungeon has been given responsibility for checking whether a "clear line of sight" exists between two Pts (this function is called in both Wanderer:: CanDetect() and Patrol::CanDetect()). The function takes as arguments the two points, a maximum range and a Pt array in which to return the Pts along the line of sight. Its implementation uses the auxiliary private member functions ClearRow() etc.
The algorithm for the ClearLineOfSight() function is the most complex in the program. There are two easy cases; these occur when the two points are in the same row or the same column. In such cases, it is sufficient to check each point from start to end (or out to a specified maximum) making certain that the line of sight is not blocked by a wall. Pseudo code for the ClearRow() function is:
Dungeon::ClearRow(Pt p1, Pt p2, int max, Pt path[]) delta = if p1 left of p2 then 1 else -1 current point = p1 for i < max do current point's x += delta; if(current point is not accessible) return fail path[i] = current point; if(current point equal p2) return success i++ return fail
Cases where the line is oblique are a bit more difficult. It is necessary to check the squares on the map (or screen) that would be traversed by the best approximation to a straight line between the points. There is a standard approach to solving this problem; Figure 29.5 illustrates the principle.
The squares shown by dotted lines represent the character grid of the map or screen; they are centred on points defined by integer coordinates. The start point and end point are defined by integer coordinates. The real line between the points has to be approximated by a sequence of segments joining points defined by integer coordinates. These points define which grid squares are involved. In Figure 29.5 the squares traversed are highlighted by ¥ marks.
The algorithm has to chose the best sequence of integer points, for example choosing point (2, 1) rather than (2, 2) and (8, 4) rather than (8, 3) or (8, 5). The algorithm works by calculating the error associated with different points (shown by the vertical lines in Figure 29.5). The correct y value for say x=8 can be calculated; the errors of taking y = 3, or 4, or 5 are easy to calculate and the best approximation is chosen. When the squares traversed have been identified, they can be checked to determine that they are not blocked by walls and, if clear, added to the path array. The algorithm is easiest to implement using two separate versions for the cases where the change in x is larger or the change in y is larger. A pseudo-code outline for the algorithm for where the change in x is larger is as follows:
Dungeon::ClearSemiHorizontal(Pt p1, Pt p2, int max, Pt path[]) ychange = difference in y values of two points xchange = difference in x values of two points if(xchange > max) return fail deltax = if x increasing then 1 else -1 deltay = if y increasing then 1 else -1 slope = change in y divided by change in x error = slope*deltax current point = p1 for i < abs(xchange) do if(error*deltay>0.5) current point y += deltay error -= deltay error += slope*deltax current point x += deltax if(current point not accessible) return fail path[i] = current point if(current point equal p2) return success i++ return fail
It is worth introducing a simple class to represent coordinates because many of the functions need coordinates as arguments, and there are also several places in the code where it is necessary to compare the coordinates of different objects. A declaration for class Pt is:
class Pt { public: Pt(int x = 0, int y = 0); int X() const; int Y() const; void SetPt(int newx, int newy); void SetPt(const Pt& other); int Equals(const Pt& other) const; int Adjacent(const Pt& other) const; int Distance(const Pt& other) const; private: int fx; int fy; };Most of the member functions of Pt are sufficiently simple that they can be defined as inline functions.
The declaration for class WindowRep is
class WindowRep { public: static WindowRep *Instance(); void CloseDown(); void PutCharacter(char ch, int x, int y); void Clear(); void Delay(int seconds) const; char GetChar(); void MoveCursor(int x, int y); private: WindowRep(); void Initialize(); void PutCharacter(char ch); static WindowRep *sWindowRep; char fImage[CG_HEIGHT][CG_WIDTH]; };
The size of the image array is defined by constants CG_HEIGHT and CG_WIDTH (their values are determined by the area of the cursor addressable screen, typically up to 24 high, 80 wide).
The static member function Instance(), and the static variable sWindowRep are used in the implementation of the "singleton" pattern as explained in the implementation section. Another characteristic of the singleton nature is the fact that the constructor is private; a WindowRep object can only get created via the public Instance() function.
Most of the members of class Window have already been explained. The actual class declaration is:
class Window { public: Window(int x, int y, int width, int height, char bkgd = ' ', int framed = 1); virtual ~Window(); void Clear(int x, int y); void Set(int x, int y, char ch); void SetBkgd(int x, int y, char ch); int X() const; int Y() const; int Width() const; int Height() const; void ShowAll() const; void ShowContent() const; void PrepareContent(); protected: void Change(int x, int y, char ch); int Valid(int x, int y) const; char Get(int x, int y, char **img) const; void SetFrame(); char **fBkgd; char **fCurrentImg; int fX; int fY; int fWidth; int fHeight; int fFramed; };
The declarations for the specialized subclasses of class Window were given earlier.
The base class for the hierarchy defines a DungeonItem as something that can read its data from an input stream, can draw and erase itself, and can say where it is. It owns a link to the Dungeon object, its Pt coordinate and a symbol.
class DungeonItem { public: DungeonItem(Dungeon *d, char sym); virtual ~DungeonItem(); Pt Where() const; virtual void Draw(); virtual void Read(ifstream& in); virtual void Erase(); protected: Dungeon *fD; Pt fPos; char fSym; };
Since class DungeonItem is the base class in a hierarchy, it provides a virtual destructor and makes its data members protected (allowing access by subclasses).
A Collectable object is just a DungeonItem with three extra integer data members and associated access functions that can return their values. Because a Collectable needs to read the values of its extra data members, the class redefines the DungeonItem read function.
class Collectable : public DungeonItem { public: Collectable(Dungeon* d, char sym); int Hlth(); int Wlth(); int Manna(); virtual void Read(ifstream& in); private: int fHval; int fWval; int fMval; };
An ActiveItem is a DungeonItem that gets hit, gets asked if is alive, moves, and runs. Member function Run() is pure virtual, it has to be redefined in subclasses (because the "run" behaviours of subclasses Player and Monster are quite different). Default definitions can be provided for the other member functions. All ActiveItem objects have "strength" and "health" attributes. The inherited DungeonItem::Read() function will have to be extended to read these extra data.
class ActiveItem : public DungeonItem { public: ActiveItem(Dungeon *d, char sym); virtual void Read(ifstream& in); virtual void Run() = 0; virtual void GetHit(int damage); virtual int Alive() const; protected: virtual void Move(const Pt& newpoint); Pt Step(int dir); int fHealth; int fStrength; };(Function Step() got added during implementation. It returns the coordinate of the adjacent point as defined by the dir parameter; 7 => north west neighbor, 8 => north neighbor etc.)
A Player is a specialized ActiveItem. The class has one extra public member function, ShowStatus(), and several additional private member functions that are used in the implementation of its definition of Run(). A Player has extra data members for its wealth and manna attributes, a move counter that gets used when updating health and manna. The Player object owns the NumberItem and EditText windows that are used to display its status and get input commands.
class Player : public ActiveItem { public: Player(Dungeon* d); virtual void Run(); virtual void Read(ifstream& in); void ShowStatus(); private: void TryMove(int newx, int newy); void Attack(Monster *m); void Take(Collectable *pi); void UpdateState(); char GetUserCommand(); void PerformMovementCommand(char ch); void PerformMagicCommand(char ch); int fMoveCount; int fWealth; int fManna; NumberItem *fWinH; NumberItem *fWinW; NumberItem *fWinM; EditText *fWinE; };
Class Monster is just an ActiveItem with a specific implementation of Run() that involves the extra auxiliary functions CanAttack() etc. Since instances of specialized subclasses are going to be accessed via Monster* pointers, and there will be code of the form Monster *m; ... ; delete m;, the class should define a virtual destructor.
class Monster : public ActiveItem { public: Monster(Dungeon* d, char sym); virtual ~Monster(); virtual void Run(); protected: virtual int CanAttack(); virtual void Attack(); virtual int CanDetect(); virtual void Advance(); virtual void NormalMove() { } };
Class Ghost defines the simplest specialization of class Monster. It has no extra data members. It just redefines the default (do nothing) CanDetect() and Advance() member functions.
class Ghost : public Monster { public: Ghost(Dungeon *d); protected: virtual int CanDetect(); virtual void Advance(); };
The other two specialized subclasses of Monster have additional data members. In the case of class Patrol, these must be initialized from input so the Read() function is redefined. Both classes redefine Normal Move() as well as CanDetect() and Advance().
class Wanderer : public Monster { public: Wanderer(Dungeon *d); protected: virtual void NormalMove(); virtual int CanDetect(); virtual void Advance(); int fLastX, fLastY; Pt fPath[20]; }; class Patrol : public Monster { public: Patrol(Dungeon *d); virtual void Read(ifstream& in); protected: virtual void NormalMove(); virtual int CanDetect(); virtual void Advance(); Pt fPath[20]; Pt fRoute[100]; int fRouteLen; int fNdx, fDelta; };
Figure 29.6 illustrates some of the interactions involved among different objects during a cycle of Dungeon::Run().
The diagram illustrates aspects of the loop where each Monster object in the fInhabitants collection is given a chance to run. The Dungeon object will first interact with the DynamicArray fInhabitants to get a pointer to a particular Monster. This will then be told to run; its Run() function would call its CanAttack() function.
In CanAttack(), the Monster would have to get details of the Player object's position. This would involve first a call to member function Human() of the Dungeon object to get a pointer, and then a call to Where() of the Player object.
The diagram in Figure 29.7 illustrates the case where the Player is adjacent and the Monster object's Attack() function is called. This will again involve a call to Dungeon::Human(), and then a call to the GetHit() function of the Player object.
Figure 29.7 illustrates some of the interactions that might occur when the a movement command is given to Player:Run().
The Player object would have had to start by asking its EditText window to get input. When EditText::GetInput() returns, the Player object could inspect the character string entered (a call to the EditText's GetVal() function, not shown in diagram). If the character entered was a digit, the Player object's function PerformMovementCommand would be invoked. This would use Step() to determine the coordinates of the adjacent Pt where the Player object was to move. The Player would have to interact with the Dungeon object to check whether the destination point was occupied by a Monster (or a Collectable).
The diagram in Figure 29.7 illustrates the case where there is an adjacent Monster. The Player object informs the Monster that it has been hit. Then it checks whether the Monster is still alive. In the illustration, the Monster object has been destroyed, so the Player must again interact with the Dungeon object. This removes the Monster from the fInhabitants list (interaction with the DynamicArray is not shown) and deletes the Monster.