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