Home
Archives
About us...
Advertising
Contacts
Site Map
 

ruby in steel

 

ADVENTURES IN CODING #5

This month we are finally ready to start adding puzzles to 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:
cshp5src.zip

 

See Part Four for the ins and outs of out parameters

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.

Our adventure game system starts to get really interesting this month. Now that we’ve built the class hierarchy and designed a map, it is time to let the player loose on it. We’ll write some code that will allow the player to wander around from room to room collecting treasures en route.

Writing a ‘take object’ routine is pretty simple. Let’s look at the code. From directory \a\wombat in our source code, load up the wombat.sln solution. Things, such as treasures, in this game are stored in ThingList objects. You can find the definition of the ThingList class in the AdvThings.cs unit. The ThingList class has a thisOb() method which returns an object with a specific name if such an object is found in the list:

public Thing thisOb( string _aName )
{
  Thing athing = null;
  foreach(Thing t in this)
  {		
    if(t.Name.Trim().ToLower().Equals(_aName.Trim().ToLower()))
      athing = t;			
  }
  return athing;
}

This method takes an object’s name as a string argument, _aName. If that name matches the Name property of one of the objects in the current list (this) then that object is returned. Otherwise null is returned. Notice that the code uses a few methods to trim the strings, set them to lower case and test them for equality. Refer to Stringing Along (below) for more on string-handling methods.

Now switch to the MainForm unit to see what happens when the ‘Take’ button is clicked. The code passes the text from the input box, inputTB, to the takeOb() method. Find the takeOb() method higher in the MainForm unit. This starts off by calling the thisOb() method of CurrentRoom.Things (the ThingList containing any objects in the current room) to obtain a Thing, t, whose Name property matches the string, obname. If none is found, t is null and the string, "no, it's not here!", is returned. Otherwise, t is instantiated to a Thing object and this is passed as the first of three arguments to the transferOb() method. The remaining two arguments are the list of things in the current room and the list of things owned by the player. The transferOb() method, simply removes the Thing, t, from the first list and adds it to the second list.

Now, implementing a dropOb() method is trivial. In essence it is the same as takeOb() apart from the fact that the thisOb() method is used with the player’s list rather than the room’s list and the order of the two lists is reversed when transferOb() is called so that the item is dropped rather than taken.

Restoration Drama

Finally, it would be handy if we could save and restore games or restart the default game. That’s easily done. Restarting is simplest of all. You can just recreate the adv object that defines the game as a new instance of the Adventure class:

adv = new Adventure();

Saving and loading is a bit more complex. It requires that we ‘serialize’ objects so that they can be stored in streams. You will find the code in the OnClick event-handlers associated with the Save and Load menu items. Note too that I have had to mark all the classes I want to save with the [Serializable] attribute. This has been done in the AdvThings unit. I have also had to add some appropriate references to the using section at the top of the MainForm unit. Once that’s been done, everything is remarkably straightforward.

 


I’ve added Load, Save and Restart items to the File menu. Currently, however, the program only saves and loads a single predefined file name. We’ll be adding save and load file dialogs shortly…

As an added user-friendly touch I have added a bit of code to test whether the save file already exists (using the File.Exists() method). If so, a dialog is displayed and the user is asked if the save file should be overwritten. If the ‘Yes’ button is clicked, the file is saved, otherwise the save operation is aborted. The File.Exists() method has been called when attempting to load a file too. In this case, if the file does not exist, the code does not attempt to load it.

Programmer’s Playtime!

At last the time has come to start adding some puzzles to our game! The first thing we need to do in order to expand the basic game framework into a real game is to create a bigger map. This is done by creating a series of room objects and adding them to the map in the Adventure() constructor. Inevitably this involves entering a great deal of similar code for each object created. I decided to save some typing effort by creating a keyboard macro to do this.

Recording Macros in VS .NET

It’s worth becoming familiar with Visual Studio .NET’s macros. These let you record repetitive actions and play them back instantly.

To start recording: click the Tools menu then select Macros, Record Temporary Macro.

Now just enter all the keystrokes you want to record. I entered the following text:

_map.Add(new Room("","",N,S,W,E,new ThingList()));

In the macro record panel which has popped up click the Stop button

 

Next, save the macro so that it can be used later. Select Tools, Macros, Save Temporary Macro.

 

The macro will show up in the Macro Explorer window (if this is not visible, press [ALT]-[F8] to display it). I named the macro newRoom.

While you can run a macro by double-clicking its name in the Macro Explorer, it is usually more convenient to assign it to a combination of keystrokes. Select Tools, Options, Environment, Keyboard.

Click in the pane showing a list of commands and press ‘M’ to scroll down to the Macros section. If you created the same macro I did, you should find a macro here called Macros.MyMacros.RecordingModule.newRoom. Click the mouse in the ‘Press Shortcut keys’ field and press whichever combination of keys you would like to execute the macro. Many key combinations are already assigned.

You can reassign one of these or create a new two-key combination. For example, I entered [CTRL][SHIFT]-/ followed by ‘R’. From now on, I will always use the [CTRL][SHIFT]-/ hotkey to start a macro. Then, I can press any of the remaining letters of the alphabet for 26 different macros. Once you’ve selected a hotkey, click the ‘Assign’ button. You will be prompted to make a copy of the default keyboard settings. You may want to name these ‘My Settings’ and save them so that they can be reused subsequently whenever you wish.

Now, back to the adventure...

When creating a map, it is best to start by drawing it on a piece of paper. Draw out a grid with a square for each room. Number each room from 0 upwards and draw lines between the squares to indicate the exits connecting one room to another. Normally, the exit-lines will be straight so that if, for example, Room 0 has an exit South into Room 1 then Room 1 will have an exit North into Room 0. This is not an invariable rule, however. In some circumstances you may want to distort the geography so that, say, the eastern exit from Room 0 enters Room 1 at the northern side. If you do this, make sure that the map is internally consistent. In the map of my game, I have created an apparently enormous meadow (Room 0) simply by making the exits to the North, West and East loop back into Room 0. This means that the meadow will seem to go on forever if the player wanders in those directions. Similarly, I have created an apparent maze of passageways from just three rooms (5, 7 and 8) by the trick of linking these three rooms together and pointing the northern exit of Room 7 back to itself.

However, the astute player will soon be able to spot this ‘phoney exit’. All that needs to be done is to drop a different item in each part of the passage (that is in each of rooms 5, 7 and 8). Having exited through the North of room 7, the player will see that the object that was dropped there is also in the ‘new’ location.

Gateway To Adventure

The time has come to begin work on a real puzzle. Load up the extended version of this project, wombat.sln, which you will find in the directory \b\wombat in the source code. The puzzle I’ve been working on is found in Room 10, the “Gateway”. The code which creates all the rooms and adds them to the map is found in the Adventure.cs unit. Currently Room 10 has no exit to Room 11 (and the remainder the of the rooms to the North). This is because there is a locked door to the North. There is a sign on the door that states: “Notice to all wombats - Squeak once to open door” (this sign is an object in rm10list. Scroll up the unit to find the code which creates and initialises this list).

It would seem, then, that we have to find a wombat in order to make any progress. Before we do that, however, I’ve just noticed another problem. Up to now, all objects in the game can be taken by the player. This includes the door and the sign! Obviously, we must have some way of distinguishing those objects which can be taken from those which can’t. My solution is to add a Boolean CanTake property to the Thing class in the AdvThings.cs unit.

I might, later on, add other similar properties to this class such as CanOpen, CanBreak and so on. You need to be sure to set values for each of these properties when each object is constructed. Rather than adding a parameter for each property to the default Thing() constructor, which would make object creation a chore, I have simply set a default value for the _cantake field in the constructor. I have then added a second constructor with the extra parameter. Only if I specifically want to apply non-default values do I need to call this alternative constructor. Note that multiple constructors for a single class are only legal when the parameter list of each is different. If you look in the Adventure() constructor in Adventure.cs, you will see that the door and sign objects are created using the second version of the Thing() constructor which takes three arguments, the final one setting the bool value of _cantake.

All that now remains to be done to ensure that non-takeable objects aren’t taken is to add a test of the CanTake property of the specified Thing, t, in the takeOb() method of MainForm.cs:

if (t.CanTake) 
{
  transferOb(t, adv.Player.CurrentRoom.Things,adv.Player.Things);
    return t.Name + " taken!";
}
else return "You can't take the " +t.Name+"!";

Now I somehow need to get that wombat. Unfortunately, the beast is inside its kennel in Room 12. The game hasn’t got a “take something out of something” command, so I can’t reach in and get the animal. Even if that command did exist, I would make sure that the puzzle wasn’t that easy. The kennel would either be too deep to reach into or the wombat would wriggle out of your grasp. No, the player has to entice the wombat out of the kennel some other way. There is a clue here. The kennel has a sign saying: “Do not feed the wombat!’. In an adventure game, when you are told not to do something, obviously you should do it. There was a carrot lying around in the meadow at the start of the game (Room 0). Maybe if I take the carrot and then drop it in the kennel room, (Room 12) the wombat will come out? When the wombat emerges I can grab it and take it to the Gateway (Room10). It will squeak and the door will open, leading me to room 11 (that is, the value specifying Room10’s North exit will be changed from dir.NOEXIT to 11).

In order that all this shall happen, I need to be able to add some special coding that executes when the carrot is dropped in the kennel room. Later in the game, I might need to have special coding when other crucial objects are dropped (or taken or looked at) in other specific locations. I’ve decided to add this coding in a method called dropObSpecialAction(). I call this method from dropOb() (in MainForm.cs) and it tests whether specific conditions are met. In this case, this is the test:

if (( t.Name == "carrot" ) && ( r.Name == "Kennel Room" ))

You might also want to add a test that the wombat is still in its kennel – that is, you must test that it is in the kennellist list. If so, you can transfer the wombat to the current room list in the same way that I previously transferred an object between the player’s list and a room’s list when that object is dropped. From there on, coding the rest of the puzzle is pretty straightforward. Give it a go. This is where the fun really begins….


GOING FURTHER...

Exploring The Boundaries of Your Virtual World

Text based adventures take place in their own virtual worlds. As with any world, the world of the adventure game has boundaries. The game I’ve coded thus far lets you wander around, picking things up and dropping them. The puzzle I’ve been working on is, however, incomplete. I need to find the kennel containing the wombat and lure the animal out by dropping a carrot. I’ve coded everything up to the point of dropping the carrot. As the implementer of the game, what you have to do now is to move the wombat out of its kennel. This is easy. The wombat is, after all, a Thing object (just like the carrot) and can be taken from a list in one location (here, the kennellist object) and added to another list in another location (such as rm12list, the list that contains the objects in ‘Room 12’ the “Kennel Room”). If you want to give the wombat more freedom to move around the map, you might think about making it an instance of the Actor class, which defines the player object. In principle, you could create all kinds of different Actor objects which, with a bit of clever coding, could move around the map and interact with other Actor objects or steal treasures before the player has a chance to get them.


Enticing The Wombat


To take, drop or look at objects, enter the object’s name - here “carrot” into the text box and click the appropriate button

To get to the wombat and try to entice him from his kennel, follow these instructions.

  • Load the game, wombat.exe.
  • Enter “carrot” into the text box and click “Take”
  • Click the following direction buttons in sequence:
  • S
  • E
  • S
  • E
  • N
  • E
  • You are now in the “Kennel Room”
  • Enter “sign” into the text box and click “Look At”
  • Read the sign.
  • Enter “carrot” into the text box and click “Drop”
  • You have arrived at the boundaries of this game. From here on in, it’s up to you….

File Loading and Disk Browsing

Use the ready-to-run Windows dialogs to save and load files and browse the disk.

Formerly our game was only able to load a single save file. It would be much more useful to let the user save and restore any number of times so that multiple files could contain the state of the game at different points. In order to do this, we need to make use of file save and load dialogs to let the user browse for files on disk.


Now the game displays a dialog to let the user save or load different game files

While you can create dialogs entirely in code (for an example, see the code in testBtn_Click()), it is simpler just to drop save and load dialogs from the toolbox. This makes it easy to set properties in the Property Inspector. One of the key properties is Filter. This lets you specify which file extensions will be displayed in the dialog’s ‘Files of type’ drop-down list. The filter we use specifies two entries, one with a *.sav extension and one with any extension, *.*:

sav files (*.sav)|*.sav|All files (*.*)|*.*

Note that each entry takes the form of two pieces of text separated by an upright bar. The text before the upright bar is a brief description, while the text after the bar is the extension itself. Multiple entries are separated by another upright bar. If you wanted the second entry to appear by default you would set the FilterIndex property to 2. We want the first entry to be the default, so we have set FilterIndex to 1.

I have also set the following properties to the values shown:

AddExtension = True
DefaultExt = *.sav
FileName = wombat.sav
InitialDirectory = .
Title = Load a saved game

Since the default extension is *.sav and AddExtension is True, the extension ‘.sav’ will be automatically added to a file name such as “wombat”, if no extension is specified. The FileName “wombat.sav” appears in the dialog as the default. The single dot in InitialDirectory opens the dialog in the current directory and the Title is the one that appears at the top of the dialog.


Nailing Your Files

You can only load a file from disk if that file exists. In .NET, it’s easy to verify this

Before loading a file, it is essential to check that the file actually exists. If a user enters a file name into a dialog, it is quite possible that the file name will be misspelled. Even if you hard-code a file name into the source code, there is always the possibility that the directory may have been changed or the file might have been renamed, making it impossible to load.

In previous projects, I handled this problem by checking the file name using the Exists() method of the File class like this:

string fn = "wombat.sav";
if (File.Exists(fn))
{
  // add code here to load or save file
}
else
   MessageBox.Show(String.Format("File {0} does not exist!", fn));

When using FileOpen and FileSave dialogs, however, there is a simpler alternative. Just set the dialog’s CheckFileExists property to True using the Property Inspector. Then, if the specified file does not exist, a dialog box will automatically pop up to warn the user. The focus is then returned to the dialog itself, giving the user the opportunity either to select a file that does exist or to cancel the operation.

Similarly, if the CheckPathExists property is set to true, an error message is displayed if the user enters a non-existent directory name. Incidentally, if CheckPathExists is False but CheckFileExists is true, a ‘file name’ error will appear rather than a ‘path name’ error. Unless you have a very good reason for choosing otherwise, it would generally be good practice to have both CheckPathExists and CheckFileExists set to True.

When using a SaveFileDialog, you might also want to ensure that the OverwritePrompt property is set to True. If this is done, a dialog will prompt the user for confirmation before saving data to a file that already exists.


Stringing Along

To make the most of string-handling in C#, it pays to go to the library!

The .NET framework provides a decent number of ready-to-use string handling methods and properties in its class library. I have used a few of these in the thisOb() method of the Thing class in AdvThings.cs :

t.Name.Trim().ToLower().Equals(_aName.Trim().ToLower())

 In the code above, I have trimmed leading and trailing whitespace from the string, t.Name, set it to lower case and then tested it for equality with _aName, which has also been trimmed and set to lowercase.

Most applications need to manipulate strings at some point. It is worthwhile, therefore, familiarising yourself with the members of the String class. One of the most useful properties is Length which returns the numbers of characters in a string. You can index into a string using an integer in square brackets. Remember, the first char is at index 0, so the following displays “char=e”:

string s = "Hello world";
textBox1.Text = "char="+ s[1];

September 2005

 


Home | Archives | Contacts

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