Home
Archives
About us...
Advertising
Contacts
Site Map
 

ruby in steel

 

ADVENTURES IN CODING #4

The ins and outs of parameters and more developments in our C# adventure game
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:
cshp4src.zip

 

See Part Three for an introduction to class hierarchies and virtual methods
See Part Five for the final stages of this project

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.

This month I shall be concentrating on some useful, but confusing, features of the C# language. In particular I shall be discussing the difference between virtual and non-virtual methods and how to return multiple values from a method using ‘out’ parameters. It isn’t all tedious theory though as I shall also be returning to our adventure game project in order to find out how virtual methods and ‘out’ parameters can be used in practice.


By the end of this month’s column we’ll have created the bare bones of an adventure game with a map of rooms, each of which may contain objects, some of which (such as the ‘pot’ object shown in the room above) may contain other objects. We haven’t yet implemented the ability to take and drop objects or to save and restore a game - but those features will be coming soon….

Virtual Reality

In last month’s column, I touched briefly upon the mysteries of ‘virtual methods’. Often useful and sometimes essential, virtual methods can be quite difficult to understand if you’ve never used them before. We’ll be using virtual methods quite a bit in our projects so we shall spend some time this month looking at them in nit-picking detail.

Note: If you are unfamiliar with virtual methods, the following explanation will require some concentration. You may want to grab a cup of coffee (we recommend freshly ground Peaberry) or tea (green or Assam will suffice) before continuing…

In order to understand why virtual methods are needed, let’s consider how a normal ‘non-virtual’ method works. Let’s suppose you have defined a class that has a method with the same name and argument list as a method of an ancestor class. Normally, the method in the descendent class will, in effect, replace the method in the ancestor class. Imagine, for example, that your program contains an ancestor class, a, and a descendent class, b. A descendent class is compatible with its ancestor. You might say that class b is a type of class a (though normally with some additional features).

Our Methods.sln solution defines two classes, a and b. Class b is a descendant of class a (as you can see from this diagram in the Visual Studio Class View pane). Not only does it inherit methods called method1() and method2() from its ancestor a class but it also defines its own methods with the same names.

Now let’s suppose that class a has a method called method1() and class b also has a method called method1(). For simplicity, we’ll suppose that a.method1() returns the string, “class a: method1” whereas b.method1() returns “class b: method1”. Instances of these classes are created as follows:

a myaOb = new a();
b mybOb = new b();

Were our code now to call mybOb.method1(), it would, of course, return the string “class b: method1”. But now consider what would happen if we were dealing with a whole batch of mixed objects, some of which might be b objects, others a objects and others c, d and e objects. All of the objects in the hierarchy are descended from class a. We’ve added these objects, to an ArrayList called al. Now we write a foreach loop that iterates through all the objects in the list, al. This loop has to know which base type of object it is dealing with (here that’s class a). Since all descendent objects (b,c,d,e) are compatible with their ultimate ancestor (a), it is possible to call method1() (and other methods of class a, such as method2()) for each object encountered, like this:

foreach(a aOb in al)
{ 
    a.method1();
    a.method2();
}

This time consider what will happen if the foreach loop processes our two objects, myaOb and mybOb. The foreach loop has been told it is dealing with instances of the a class. When mybOb is processed within the loop, will b.method1() or a.method1() be called?

In fact, a.method1() will be called. So all the objects processed within the loop will return the string “class a: method1”, even though some of the objects may actually be b, c, d or e objects with their own unique method1() implementations. How can we get around this problem? The answer is to make a.method1() a ‘virtual’ method and to make all the method1() implementations in descendent objects ‘overridden’ methods. You do this by adding the keywords virtual and override before the method type and name, like this:

public virtual String method2() // in class a
public override String method2() // in class b etc.

If we call the virtual method2() inside the foreach loop, C# works out the exact type of the object being processed before calling the specified method. So when the b object, mybOb, goes through the loop, the a.method1() non-virtual method is executed, but the b.method2() virtual method is executed.

 


Here I have declared class b to be a descendant of class a. In class b, method1() replaces method1() in class a but method2() overrides the virtual method2() in class a.

Note: When a non-virtual method in a descendent class which has the same name as a method in an ancestor class you should precede the method name with the new modifier rather than the override modifier in order to ‘hide’ the ancestor method.

If virtual methods are new to you, you may find this much easier to understand by running our Methods.sln project and examining the code. Re-read the explanation given above and see how each step has been coded. Note that we have used the new keyword in b.method1(). This is recommended for clarity when an ancestor class defines a non-virtual method of the same name and argument list.

All Work, No Play

Right, that’s enough serious stuff for this month. Now let’s get back to the fun. Over the past couple of months I’ve been developing an adventure game system that owes much to the classic exploring games of days gone by such as Colossal Cave and Zork. There is more to this than mere nostalgia, though. My game will serve to introduce many of the most important features of C# and .NET.

Load up wombat.sln and look in the AdvThings.cs unit. You will see that I have already created a moderately complex hierarchy of objects here. Pay particular attention to the virtual describe() method of the Thing class and the overridden describe() method in its descendants such as ThingHolder and Room. You will see that the implementations of the describe() method vary dramatically from class to class, making it vital that the correct method be called. The ThingList class has its own non-virtual describe() method in which the virtual describe() method of each Thing in a list is called.

However, the most interesting addition to this month’s version of the project is the code to let us move the player (or other characters) from one location to another. To see this in action, run the program and click the four direction buttons.

This diagram shows the linked structure of room objects which comprise the map of our game.

To find your way around, refer to the diagram of the map above (you’ll also find this at the top of the Adventure.cs unit). This shows the six rooms in the map at present and uses dashes and vertical bars to indicate which rooms are connected. For example, the initial location, room0, connects east to room1 and south to room2. If you click the 'N' button, however, “No Exit!” is displayed.

Look at the Click event-handler code for each of the direction buttons in MainForm.cs. This could hardly be simpler. Each event-handler simply calls the movePlayer() method, passing to it one of the dir class constants, defined in AdvConsts.cs.

In the design of this game, I had to decide whether the characters or ‘Actor’ objects, including the player, are given the freedom to move themselves from place to place like mice in a maze or whether they are moved around by some ‘outside’ force like the pieces in a chess game. I decided on the latter option for the simple reason that each actor only has a knowledge of its own location. This is due to the fact that the Actor class contains a _room field which provides a reference to the Room object which an Actor object hypothetically ‘occupies’. The Adventure object that contains the game, on the other hand, has the entire map at its disposal. I felt it was simpler that the Adventure object should be the only object which can change the state of the game (say, by moving Actor objects) rather than letting numerous Actor objects take control of their own actions.

At any rate, it is the Adventure.movePlayerTo() method that changes the player’s location. This it does by calling a generic moveTo() method which can be used to move any Actor object. This takes an int parameter indicating the direction in which to move. A switch statement tests this direction against one of the values defined in the dir class. If the value is not dir.NOEXIT then the moveActorTo() method is called to change the location (the CurrentRoom property) of the Actor object to the Room found at the index specified by Map.Item(exit). So, for instance, if the S (for ‘South’) property of room0 is 2 (this property is initialised when the Room object is created in the Adventure() constructor), then the value of exit is set to 2 when the game-player attempts to move south from room0 in the Adventure.moveTo() method. In the call to moveActorTo(a, Map.Item(exit)), the call to Map.Item(exit) returns the Room at index 2 in the map object (an instance of the RoomList class). As you will see if you refer back to the Adventure() constructor, this is the room2 object – that is, the third Room which we added to the map.

Out and Proud

Let’s now take a look at another novel feature of the C# language - its three varieties of parameter. Most languages recognise two types of parameter which can be passed to a function or subroutine: a parameter that is passed ‘by value’ is a copy of the original variable. Alterations made to the value of parameter inside a function do not affect the value of the original variable.

Alternatively, a parameter can be passed ‘by reference’. A by-reference parameter is really a pointer to the original variable. Alterations made to the parameter within the function affect the original variable too.

By default C# parameters are ‘by value’ copies of the original variables. When you want changes which are made within a method to affect the original variables, you should use ‘out’ parameters. These are parameters preceded by the keyword out both in the method and the calling code. For example, in my AdvThings.cs unit, the Room class returns the int values of its four exits in this method:

public void getDirs( out int aN, out int aS, out int aW, out int aE )

Now look at the calling code in testBtn_Click() in MainForm.cs. Note that this too uses the out keyword. If this keyword is omitted, the compiler flags an error:

adv.Player.CurrentRoom.getDirs(out _n, out _s, out _w, out _e);

You can also use the ref keyword instead of out when you wish to modify the values of variables within a method. There is one fundamental difference between ref and out parameters: whereas ref parameters must be initialised before use, out parameters can be passed without being initialised.


Misleading Cases

If you want to make quick decisions, use the C# Switch..Case statement

If you have never programmed in a C-like language, you might not understand the block of code starting with the keyword switch in the MoveTo() method of wombat.sln. This is, in essence, a short-hand way of doing multiple if..else tests. The argument between brackets is the selector value that is tested by the case statements. If the value specified by a case statement matches that of the selector, the code if that statement is executed. If no match is made, the default statement executes. Unlike C++, it is not legal to have one case statement trickle down to the next one. The compiler insists on a break statement to break out of the switch block or a goto statement to jump to another case statement.

Load switchtest.sln and find button1_Click(). Run the program and click button1. Try altering the value of i to 6 and run it again to see how this causes the default statement to jump to case –1. Incidentally, the selector value doesn’t have to be an integer. It could even be a string. See an example of this in button2_Click().

Next Month...

We’ll be wrapping up this series with a few ideas on how to complete the adventure game by adding some puzzles

August 2005

 

 


Home | Archives | Contacts

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