Home
Archives
About us...
Advertising
Contacts
Site Map
 

ruby in steel

 

ADVENTURES IN CODING #3

Extending the class hierarchies and virtual methods
by Huw Collingbourne

Requirements: C# Compiler, The .Net Framework 1.1 or above, a C# IDE such as Visual Studio .NET, SharpDevelop or Delphi 2005

Download The Source Code:
cshp3src.zip

 

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


Next Month...

I'll take an in-depth look at some of the more arcane features of class hierarchies and virtual methods

July 2005

 


Home | Archives | Contacts

Copyright © 2006 Dark Neon Ltd. :: not to be reproduced without permission