See Part Two for an explanation of class
hierarchies and customised Collections
See Part Four for information on out parameters and overriding
virtual methods
If you have experience of another programming language
but are new to C#, this series will introduce you
to the fundamental features of the C# language and
the .NET Framework as we develop a simple ‘classic
style’ text adventure game. |
In part two of
this series, we spent some time learning how to create
custom class hierarchies. One of our projects was an
exploring-style text adventure game. This month I’ll
develop this further. In the course of so doing, I shall
extend the class hierarchy and consider ways in which
it will be possible to make this increasingly complex
program remain understandable, controllable and maintainable.
|
This is the view of the project in the Visual Studio
Solution Explorer. Each file with the '.cs' extension
is a separate C# source file |
Load up the new version of the game, wombat.sln.
I’ve
added several new classes to this now. The AdvThings.cs unit
contains the essential object types of the game. Notice
that I now have two distinct list management classes,
ThingList and RoomList. Strictly speaking, neither of
these classes is necessary. I could equally well maintain
lists of objects using the standard ArrayList class.
However, as explained last month, an ArrayList accepts
any type of object. There are benefits to having strictly
typed lists which accept only specific types of object.
The main benefit is that errors caused by an attempt
to add the wrong type of object to a typed list are trapped
at design time. With an untyped list, these errors could
cause a program crash at runtime. The ThingList class
only accepts Thing objects and the RoomList class only
accepts Room objects.
Map Reading
Both these custom collection types are really no more
than type-checking wrappers around a standard collection
(the List or InnerList fields). I’ve provided them
with Add(), AddRange(), Remove() and Item() methods.
These interface to methods with the same names in their
internal collections. Finally, I’ve given each
of these classes a method called describe(). This returns
a string that describes the internal state of the object.
I’ve added a describe() method to the other classes
too. When one class contains another class, the container
class’s describe() may call the contained class’s
describe(). You can see an example of this in ThingList.describe():
foreach(Thing t in this)
{
s = s + t.describe()+"; ";
}
Here, the describe() method is called for each Thing
object, t, in the list maintained by the current ThingList
(the ThingList object refers to itself using the keyword,
this).
The foreach loop builds a string, s, from each of the
strings returned by t.describe(). Examine the implementation
of Thing.describe() to see how a string is returned by
this method.
There is another completely new class in AdvThings.cs – the
Actor class. This can be used to define a character in
the game. In this sense, a character might be any mobile
object – an animal, person, robot or alien – that
can move around the map interacting with other objects.
For the time being, this game will have just one Actor
object which represents the person playing the game.
Since every character, including the player, must occupy
a specific location on the map, the Actor class contains
its own internal Room object. I haven’t yet implemented
any methods to move characters around on the map, so
at present the Room specified at the time the Actor object
is created, is never changed. Later, I shall add methods
to update the Room object as characters move around from
place to place.
Adventures In Coding
One new class in the wombat.sln project is so important
that it has been given its own source file, Adventure.cs.
The Adventure class contains the entire game. In the
previous project (see part two), the objects of the game
were created and manipulated within MainForm.cs. This
is all very well when you are simply trying out a few
ideas. When you move on to programming a complete application,
however, it makes sense to leave the form definition
code in the form definition unit and move all other code
into other units.
Currently, the Adventure class contains a fixed, invariable
version of the game. In other words, when you create
a new object from the Adventure class you will always
end up with the same game. Later on I shall change this
so that different games can be saved to and loaded from
disk.
Notice that the Adventure’s _map object is constructed
from a number of Room objects which are added, one by
one to the _map. Each Room object is initialised with
a name, a description, four exists which can either be
a room number or dir.NOEXIT (defined in AdvConsts.cs),
and finally a ThingList which is either empty or contains
a list of objects in the room.
Finally, an Actor object called _player is created
and its internal _room field is initialised with Room
_map.Item(3). This is the Room object at index 3 (i.e.
the 4th item) in the _map collection. This defines the
player’s position at the start of the game. Turn
to MainForm.cs to see how the game is created. The testBtn_Click()
method creates a new Adventure object, adv. To display
a description of all the Rooms in the Map it simply executes
this statement:
displayTB.Text = adv.Map.describe();
The description of the Player
and the Room in which the Player is located can be displayed
equally easily with this single line of code:
displayTB.AppendText(adv.Player.CurrentRoom.describe());
Manual Override
Look at the code that initialises the Adventure class
in the Adventure() constructor within the Adventure.cs unit.
This creates a list of objects, rm4list:
ThingList rm4list = new ThingList();
rm4list.Add( new Thing( "Thing one", "1st thing"));
rm4list.Add( new Thing( "Thing two", "2nd thing"));
rm4list.Add( new Thing( "Thing three", "3rd thing"));
rm4list.Add( new ThingHolder( "a pot", "a brass container.", potlist));
It then assigns this list to the “room4” Room
when it is created and added to the _map:
_map.Add( new Room( "room4", "A
dark cave.", 2, dir.NOEXIT, dir.NOEXIT, 5,
rm4list));
One of these objects, “a pot”, itself contains
a couple of other objects. That is because the potlist object
added as the "a pot" ThingHolder is itself a ThingList
which has a collection of two Thing objects:
ThingList potlist = new ThingList();
potlist.Add(new Thing("Jewel", "A lovely diamond"));
potlist.Add(new Thing("Ring", "A ring of power."));
When the Test button
is clicked in MainForm.cs, it should
be sufficient to call the game’s
Map.describe() method in order to display details of
all the rooms, the objects they contain and any objects
that happen to be contained within other objects such
as the pot. The Map’s describe() method calls each
Room’s describe() method, which calls each object’s
describe() method and so on.
Even though we have created a list of objects and added
them to the "a pot" ThingHolder, the pot does not display
its contents when we click the Test button!
There is a problem, however. The pot does not currently
display its contents. The pot object is a ThingHolder.
But if you place a breakpoint on the ThingHolder class’s
describe() you will see that it never executes.
This breakpoint is never encountered
The reason
for this can be found in the ThingList’s describe()
method. This iterates over each Thing in a list, calling
Thing.describe() for each. When it encounters a ThingHolder
it does not call ThingHolder.describe() but Thing.describe().
This is valid, since ThingHolder is a descendant of
Thing. In order to ensure that the correct version
of describe() is called when a ThingHolder object is
processed by the foreach loop,
we need to make Thing.describe() ‘virtual’ (I’ve
done this already in my code). The ThingHolder.describe()
method must then override this virtual method. To do
this add the keyword override before
the describe method name of ThingHolder.
This time the override keyword has been added and the
breakpoint is encountered...
....and now the "a pot"
object describes its contents
A Room With A View
Note that both the Room and Actor classes in wombat.sln are
descendants of ThingHolder since both may need to maintain
a list of Thing objects. A Room might contain things
(e.g. treasures) and an Actor might collect things as
he or she wanders around the game, taking items from
each of the rooms. The constructors of each of these
classes initialises its ancestor, ThingHolder, with two
strings, aName and aDescription. These arguments are
passed to the ancestor class using the keyword base:
public Room(string aName, string aDescription, int
aN, int aS, int aW, int aE, ThingList tl): base(aName,
aDescription)
In fact, the ThingHolder class does not directly initialise
these fields. Instead, it passes it to its own ancestor,
the Thing class. The final argument to both Room and
Actor is a ThingList object, tl. This list is added to
the object by calling the ThingList method, addThings():
this.addThings( tl );
OK, so we now have a solid structure for our game.
But, so far, we haven’t provided many ways for
the user to interact with it. In other words, you can
run the game but you can’t play it! I’ll
be adding some commands to let the user move around,
take and drop objects later in this series….
I'll
take an in-depth look at some of the more arcane features
of class hierarchies and virtual methods
July 2005 |