Home
Archives
About us...
Advertising
Contacts
Site Map
 

ruby in steel

 

ADVENTURES IN CODING #2

How to create class hierarchies and customised Collections
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:
cshp2src.zip

 

See Part One for an explanation of streaming and serialization
See Part Three for an explanation of 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.

If you think you think that writing an adventure game is trivial, you’ve obviously never written one! In fact, an adventure game is the perfect project with which to explore the possibilities of an Object Orientated (or ‘Oriented’ if you prefer) language such as C# and make use of some of the most important features of the .NET class library.

This month I plan to start work on the design of a hierarchy of classes to represent the locations of the game along with treasures, characters and other objects. I shall even be developing a custom collection class instead of using the general-purpose ArrayList class provide by .NET.

Before embarking upon this project, let’s finish off the application I began in the first part of this series. You may recall that this was a CD database. My initial version of this program provided the basic class definition needed to define individual CD objects which could be added to an ArrayList. However, it lacked any way to traverse the list – to move to the first and last CD record, or to move to the next or previous one. It also lacked the ability to save and restore the list to and from disk.

In this month’s project, cddb2.sln, I have added all these capabilities. If you want to follow along, download the source code and load this project into your C# IDE. First, I have created a new list management class, CDArrayList, which descends from ArrayList. This has an int Pos property which is –1 at the outset or when there are no objects in the list. When a new object is added in AddObBtn_Click(), the Pos property is set to the index returned by the Add() method:

CDList.Pos = CDList.Add(new CDClass( CDNameTB.Text, ArtistTB.Text, CommentsTB.Text ));

The form has buttons to move to the next or previous object in which case the Pos property is incremented or decremented using the ++ and -- operators. My UpdateForm() method is then called to display the data from the object at the index indicated by Pos. To move to the start of the list, I set Pos to 0. To move to the end, I set Pos to CDList.Count-1. Saving and loading the list is easy. I just use the built-in serialization features explained in part one of this series.


Load and run the cddb2.sln solution and select File, Load to open a small database of CDs. Notice that you can easily navigate forward and backward or to the start or end of the list - and the buttons even display Tooltips

As in any large object orientated project, the first thing you need to do is to decide which objects you want to create as the fundamental building blocks of the application. Let’s do that now.

Open the wombat.sln solution. With stunning originality, I have decided to call my game ‘The Lord Of The Wombats’. It will be a trilogy, of course, and we shall be working on Part One: The Fellowship of the Wombat. Now turn to the AdvThings.cs unit. Any adventure game will inevitably contain Things of many types. These will include, for example, Treasure Things and Room Things. We’ll develop the complete hierarchy as we go along. For the time being, I have defined a very basic Thing class which has Name and Description properties. You will find the definition of the Thing class close to the bottom of the unit.

Things Of The Wild Frontier

We also need a class dedicated to managing lists of Things. In another moment of inspired originality, I decided to call this the ThingList class. Later on we will need ThingLists to hold the lists of objects in each room and the list of objects that player collects while playing the game. Even the game map itself will be a ThingList – it will contain the list of all the Rooms in the game.

As you have already seen in the CD database program, the ArrayList class is good at managing lists of objects. However, it is not quite ideal here. For one thing, I may want to add some additional special-purpose methods to my list class. To do this, I could simply create the ThingList class as a direct descendant of ArrayList, much as I created CDArrayList in my last project.

There is one disadvantage to this approach. A descendant of ArrayList will allow you to add any type of object to it. In a big program this could be a potential source of bugs since I (or somebody else) might accidentally add some non-Thing object to the list. The program would compile without error. But if some piece of code did something to a non-Thing object that is appropriate only to a Thing object, the program would crash. Personally, I would prefer to trap any potential errors at compile-time rather than at run-time. For that reason, I would prefer ThingList to be more strongly typed, so that it can only operate on objects descended from the Thing class.

There are several possible ways in which the ThingList class might be coded. In this project, I have tried out three possibilities in order to get some idea of their pros and cons. Before running the code, turn to MainForm.cs. Find testBtn_Click(). This is the code that executes when you click the button labelled ‘Test’. First it creates a ThingList object, tl. Then it adds to this list a few Thing objects. It also attempts to add a string. Finally it creates a ThingHolder object, thh. A ThingHolder is simply a Thing that contains a ThingList. You will find its definition in AdvThings.cs.


My first version of the ThingList class is a descendent of ArrayList, as you can see beneath the ThingList’s Bases and Interfaces branch of the Visual Studio Class View

Now run the code. As you see, it compiles without error. But when you click the Test button, the compiler complains that the ‘specified cast is not valid’ and it highlights this line of code:

foreach(Thing th in this)


Our first version of ThingList is obviously error prone!

 

Note that the keyword, this , refers to the object itself – that is, the actual instance of this ThingList class. At first sight, it may not be obvious what is wrong here. But now look back at the calling code in testBtn_Click(). See the line where I add the string, “xxx” to the ThingList, tl. It’s no wonder that the foreach loop can’t convert a string to a Thing object! The current ThingList class descends from ArrayList and this illustrates the deficiency I mentioned earlier – that is, an ArrayList accepts any type of object. Not only does this error crash the program but, in a big project (as this one might eventually become), it could be difficult to track down. The programmer could spend a lot of time trying to debug the perfectly innocent foreach loop rather than concentrating on the real source of the problem back in testBtn_Click() where an attempt is made to add the rogue data (the string) to the list.

Class Wars

Let’s see if we can improve upon this. Comment out version 1 of the ThingList class in the AdvThings.cs unit by placing /* before public class ThingList and */ after its terminating brace.


To comment out the first implementation of ThingList, enclose all its code between the C# comment delimiters /* and */

Now uncomment version 2 by removing its /* and */ delimiters. This version implements ThingList as a custom class which, instead of descending directly from ArrayList, maintains an internal ArrayList field, _thlist.


This is the second version of the ThingList class shown in the Class View. Notice that this time it descends from the Object class rather than ArrayList. However, it has an internal field, _thlist, which is an ArrayList.

ThingList has its own methods such as Add() and AddRange() which match the name of ArrayList methods. When one of these methods is called it simply calls the matching method of its internal ArrayList object. However, since the ThingList methods specifically require Thing or ThingList arguments, the compiler refuses to compile the code if other types are used. So, in effect, this version of ThingList is a type-checking wrapper around ArrayList. When you compile the code this time, the attempt to add a string cause an ‘invalid argument’ error. You can fix this by commenting out this line:

tl.Add("xxx");

Run the program to verify that it works. When you’ve finished, uncomment the line above so that you can carry on testing the code. In AdvThings.cs, comment out version 2 and uncomment version 3 of ThingList. While version 2 was an improvement over version 1, its actual class type does not descend from a .NET collection. Microsoft recommends that user-defined collection classes should descend from System.Collections.CollectionBase. And who am I to argue with Microsoft? In version 3, I’ve followed Microsoft’s advice.


My third version of ThingList is a descendant of the CollectionBase class. As you can see, this class provides a number of useful methods and properties

Just like my own version 2 of ThingList, the .NET CollectionBase class maintains an internal collection object. You can access this object via two properties called List and InnerList. The List property returns an IList (indexed list), while the InnerList property returns an ArrayList.

As before, I’ve defined my own method names to match those of the internal collection’s methods. My methods enforce type-checking on arguments and then call the matching methods of the InnerList property. Once again, you will find that the compiler traps the attempt to add a string. This compile-time error is, for our purposes, entirely desirable!

Constant Companions

Load up the wombat2.sln solution. Here I’ve defined ThingList as a descendant of CollectionBase. I’ve also defined a more complete class hierarchy. The ThingHolder class is a class that contains a ThingList. The Room class is a descendant of ThingHolder since each room in the game will contains some Things. A Room can also have up to four exits: North, South, East and West which are defined as ints. Later on I will use exits to move from one room to another.

Incidentally, having created this class I decided that it would be desirable to define meaningful constants to represent the int values of directions. This would allow my code to specify easily understandable values such NORTH and SOUTH instead of meaningless numerical value such as 1 and 2. But C# doesn’t allow global constants. Refer to ‘Global Constants’ (below) to discover one simple way of getting around this problem.

The adventure game will be just one of several different applications that we’ll be developing over the coming months. So, take up your trusty Elvish sword, dear reader, and join us in the quest…


Global Constants

C# doesn’t have global constants. No problem!

If you want a method to move a game player, you could pass a compass direction to the player’s yet-to-be-written moveTo() method. For the sake of clarity, I feel that moveTo(dir.NORTH) is preferable to moveTo(1) . Similarly, when initialising Room objects, the second example shown below (in which the four last arguments indicate the room numbers leading from the N, S, E and W exits) is clearer than the first example:

// example 1
Room rm = new Room("A Room", "This is a dark dungeon.", 1,2,3,-1);

// example 2
Room rm = new Room("A Room", "This is a dark dungeon.", 1,2,3,dir.NOEXIT);

This poses a small problem. While it would be nice to have meaningful constant names, such as NOEXIT, available throughout a project, C# does not permit global variables or constants. You could define the constants separately within each class. But this would be messy. Moreover, it could lead to bugs were you subsequently to redefine the values of the constants within one of the classes.

In fact, there are a few ways in which you can ‘fake’ global constants. I’ve used a very simple technique in this month’s code. I have created a class named dir in the AdvConsts.cs unit. This is a public class inside the wombat namespace so it is visible to all the other classes and units in the same namespace. Inside the class I have defined a set of int constants. These are ‘static’ – which means they belong to the class itself, not to objects created from it. I have given the class a private dir() constructor to prevent objects being created. Now my code can access the constants by referring to the class, like this:

dir.NOEXIT;


Adding ToolTips

How to get users to take a hint…

When you run my database application, cddb2.sln, you’ll find that little tooltip ‘hints’ pop up when you let the mouse pointer hover over one of the navigation buttons at the bottom of the form. However, when you drop a button onto a form in the form designer, you will find that (unlike other development tools such as Delphi) the Property Inspector does not list a Hint property. So how did I get the hints to pop up over these buttons?

In .NET, the secret is to use the ToolTip class. The Form1_Load() event-handler is executed after a form is created and before it is displayed for the first time. Within this method, I have created a ToolTip object, mytooltip, and called its SetToolTip() method with two arguments, like this:

mytooltip.SetToolTip(FirstBtn,"Go to first CD");

The first argument specifies the control to which the tooltip applies and the second argument is the string to display. You don’t have to create a separate ToolTip object for each control. You can use the same object for each control on your form.

The ToolTip class also has other methods that you can use to customise the appearance of the tip. For example, the InitialDelay and AutoPopDelay properties respectively set the delay in milliseconds before which the tip appears and disappears. The following code cause the tips to pop up after half a second and disappear again after 5 seconds:

mytooltip.InitialDelay = 500;
mytooltip.AutoPopDelay = 5000;


String Formatting

A simple way to display values in a string

In C#, you can concatenate data to a string using the + operator. In my code you will see frequently see examples similar to the following:

MessageBox.Show("name ="+rm.Name+"; Description="+rm.Description);

In fact, it is sometimes neater to use the String class’s Format() method which lets you specify a sequence of values to be inserted into a string. The string itself must contain place-holders for these values between curly brackets. So “{0}” would be replaced by the first value, “{1}” by the second and so on. You can see an example of this in the testBtn2_Click() event-handler in MainForm.cs of the wombat2 project:

String.Format("Exits: N:{0} S:{1} W:{2} E:{3}",myN,myS,myW,myE)

Here the four variables, myN to myE are ints but other data types could equally well be used. The variables are indexed from 0 so they match the format specifiers from 0 to 3 in the string.


What next...?

Ultimately we shall be programming an adventure game with many locations and objects which you will be able to pick take, drop and look at. In so doing, we shall be exploring many useful features of C# and .NET

 

June 2005

 


Home | Archives | Contacts

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