Returning to the Adventure Kit Program

When I first started thinking about the Adventure Kit project (long before C# was invented), I knew that I wanted a reusable adventure engine. I wanted to have a game engine that was really easy to use with a mouse, and I wanted to encourage users to build their own adventures. The process began by my thinking about how the adventure would be organized. I thought about an adventure as a series of rooms. Each room could have multiple choices, and the user could wander through the rooms to get a sense of space. A group of rooms would form a dungeon (although the game doesn’t have to take place in a dungeon–that just seems like a good collective noun for a bunch of adventure rooms!). The user should be able to move easily between rooms. The game should be edited with an interface similar to the game interface. Because you already saw the game at the beginning of the chapter, you know that I succeeded somewhat. I want to take you through the process, though, because that’s where the real thrill of programming is.

NOTE

IN THE REAL WORLD

If you’re an experienced programmer, you might be wondering why I haven’t covered random access files yet. The techniques described so far can be used to generate random access files (files that can be read in any order, without necessarily going through each element one at a time). You will find that C# provides other tools including XML and ADO data access. These tools provide all the functionality of random access files and are easier to implement. Still, I’ll show you a way to store and manipulate several classes at once before this chapter is over.

As usual, most of the code for this chapter’s project contains things you have learned in this chapter and earlier chapters. The Adventure Kit is interesting because it has several layers of complexity. The best way to approach this program is to look first at the data structure and then at how that data structure is used in a more complex structure. I’ll take you through the game engine itself, which is surprisingly easy to write when you have thought out the data. Next, I’ll show you how to build the editor, which is similar to the game engine but requires a little more thought to create. Finally, I’ll show you the main program, which attaches the pieces together.

NOTE

IN THE REAL WORLD

Note that this way of thinking about a program is almost completely opposite of how a user typically sees software. The first thing the user will see is the main screen. The game screen will come next, and for many users, that is all they will ever see. Only the more sophisticated users will try to use the editor, and almost none will think about the underlying data structure. Throughout this book, I’ve been trying to show you how programmers think, which is very different from how users think. If you want to write interesting programs, you need to practice thinking about your programs from the “data up” instead of the way users usually see things.

Examining the Room Class

The first thing I did when designing the Adventure Kit was turn off the computer. I got out some paper and drew a picture, shown in Figure 9.19. I then thought about how I could describe each room. After a couple iterations, I came up with Table 9.2. If you compare Table 9.2 with Figure 9.19, you will see that Table 9.2 describes the information in Figure 9.19.

Figure 9.19. I drew a very simple dungeon to get a sense of what kind of data an adventure game requires.


Table 9.2. A SIMPLE DUNGEON
Number Name Description N E S W
0 Stuck Can’t go that way 1 0 0 0
1 Start Go North 2 0 0 0
2 Room 2 Go East 0 3 1 0
3 Room 3 Go South 0 0 4 2
4 Goal You Win! 3 0 0 0

As you can see from the table, each room has a number, name, and description. In addition, each room has several direction elements. Each direction box indicates which room the user will encounter if he goes in that direction. For example, if the user is in room 2 and goes north or west, he will be sent to room 0, which tells him that he is stuck. If the user goes east from room 2, he will be sent to room 3. If he goes south from room 2, he will end up in room 1. Compare the chart to the diagram in Figure 9.19 to see how the chart describes the diagram.

I’m using the term room very loosely here. It’s possible that each row of the chart represents an actual room in your game, but that’s not necessarily the case. For example, in the Enigma adventure described at the beginning of this chapter, some rooms are actions and some are decisions. Still, having a consistent vocabulary is convenient, so I’ll consider each node in the adventure a room.

When you have the chart, you will see a data structure to build. It occurred to me that each row represents a room and that building a room class that encapsulates all the data about a room would be easy.

The table I used to design the Enigma game featured at the beginning of this chapter is included on the CD-ROM as Enigma.doc. Take a look at that document to see the data I used for that somewhat more complex program. Be sure you play the game through first, though, because reading the data will spoil the game for you.

Here’s my code for the room class: (I showed only the property code for the name property to preserve space. All the other properties work exactly like the name property, with a very standard get and set procedure. Check out the full code on the CD-ROM for the complete code.)

using System;

namespace Adventure
{
  /// <summary>
  /// Basic Data class for Adventure Game.
  /// Dungeon uses an array of these
  /// No methods, just a bunch of properties and a constructor
  /// Andy Harris, 3/11/02
  /// </summary>
  [Serializable]

  public class Room  {

    private string pName;
    private string pDescription;
    private int pNorth;
    private int pEast;
    private int pSouth;
    private int pWest;

    //Properties
    public string Name {
      set {
        pName = value;
      } // end set
      get {
        return pName;
      } // end get
    } // end Name prop

   // Other properties not shown.  See CD-ROM for complete code 
    //Constructor
    public Room(string name,
                 string description,
                 int north,
                 int east,
                 int south,
                 int west) {
      Name = name;
      Description = description;
      North = north;
      East = east;
      South = south;
      West = west;
    } // end constructor
  } //   end class def
} // end namespace

Room is a simple class. It contains six properties, one for each column of the table. The properties surround private instance variables, and the only constructor for the class requires values for every property. I made the Room class serializable because I’m sure that I’ll need to save and load it down the road. Now that I have a way for my code to describe the most important part of my program, I’ve finished the hard part of the overall design.

NOTE

IN THE REAL WORLD

This strategy of organizing your information into tables and then building a class to represent the table data isn’t just for game programming. In fact, it’s the key to any kind of programming that involves large amounts of information. Getting a handle on your data is clearly the starting point of writing a good program. If you design your data well, your program will flow towards completion with relative smoothness. If you’re sloppy in the way you design data (for example, you don't clearly think through how the user will get from one room to another), you will struggle throughout the entire process. Nothing seems to have a more important effect on a programmer's success than his understanding of the data.

Creating the Dungeon Class

I am proud of my Room class because it will help me with my goal of building a dungeon. However, the Room represents just one row of the chart. I need to represent many rows at once. The easiest way to group the rooms is to build another class that holds an array of rooms. That class is named the Dungeon class:

using System;

namespace Adventure
{
  /// <summary>
  /// Class for storing a dungeon. Mainly holds an array of rooms.
  /// Designed to be stored in a serial form.
  /// 3/11/02, Andy Harris
  /// </summary>

  [Serializable]
  public class Dungeon
  {
    private string pName;
    private int pNumRooms = 20;
    private Room[] pRooms;

    public string Name {
      set {
        pName = value;
      } // end set
      get {
        return pName;
      } // end get
    } // end name property

    public int NumRooms {
      //no set - make it read-only
      get {
        return pNumRooms;
      } // end get 
    } // end numRooms property 
    public Room[] Rooms{
      set {
        pRooms = value;
      } // end set
      get {
        return pRooms;
      } // end get
    } // end property

    public Dungeon(){
      Rooms = new Room[pNumRooms];
    } // end constructor
  }  // end class def
} // end namespace

The Dungeon class has three properties and a constructor. The Name property is the name of the current game. The numRooms property is a read-only property that stores the number of rooms in the current dungeon. I made the numRooms property read-only because it can cause some serious problems if the number of rooms is changed thoughtlessly. I preset the number of rooms at 20, but changing the Dungeon class to handle more rooms would be easy. However, none of the files stored with the 20-room version of the program will work with the new one, and vice versa. It seems that 20 rooms is enough to build complex adventures (such as the Enigma game) without becoming overwhelming.

The most critical property of the Dungeon class is the array of Room objects, named (cleverly enough) Rooms. By having the rooms stored in an array, I gain several important advantages. First, I don’t have to worry about adding a room number to the room class, because the index in the array will serve as the room number. Second, accessing each room by its index will be easy. Third, because the array of rooms is part of the Dungeon class, I can store and load all the rooms at once by serializing Dungeon.

The Dungeon class is serializable. Because it includes instances of the Room class, Room must be serializable as well. The combination of Room and Dungeon completes the basic data structure for the game.

Writing the Game Class

It might surprise you that the actual game form is probably the simplest part of the program. All the careful work designing the data makes the game itself quite simple to write.

Creating the Game Form's Visual Design

The Game class is designed around the metaphor of a scroll, with arrows pointing in four directions. Figure 9.20 shows the game form's visual design. The most obvious feature of the game window is the central label named lblDescription. I added a scroll image as the background image of the label and gave it a font that reminded me of a treasure map. The four surrounding labels are used to describe what will happen when the player moves in a particular direction. Each of these labels features a background image as well. I used figures of pointing hands to illustrate the possible positions. Each label also features text that describes the name of the room the user will encounter if he goes in that direction.

Figure 9.20. The game window features several labels and a menu.


NOTE

IN THE REAL WORLD

Because this program involves several forms, it is important that they have a unifying visual design. I built a Scroll image in my image editor and placed it on the back of every form, so it looks as though the instructions are written on a treasure map. I also chose similar fonts throughout the program and kept the general layout of the editor and the game screen similar, even though the actual controls are completely different in these two forms. It pays to keep visual unity in your program to reassure the user that he is still using your program even though he changes screens several times.

LblName will hold the name of the current room. The form contains one menu with only two choices. If the user chooses to edit the game, the current form closes, and the editor opens with the current game loaded in it. If the user chooses to quit playing, the program returns to the main screen.

Building the Game Class Instance Variables

The Game class has only two instance variables. Both are used to keep track of the adventure data:

private int currentRoom = 1;
public Dungeon theDungeon = new Dungeon();

The currentRoom variable is used to specify which room is currently being displayed. theDungeon refers to the current dungeon structure, which, in turn, holds all the room data.

Initializing in Game_Load()

When a Windows program first loads, it automatically calls a Load() method. I decided to add all my initialization code to Game_Load() rather than to the constructor. I like the fact that the constructor has all the Designer-generated code and my custom initialization goes in the Game_Load() method. The code for Game_Load() simply calls other custom methods:

private void Game_Load(object sender, System.EventArgs e) {
  setupRooms();
  showRoom(1);
} // end load

The setupRooms() method (as you will see shortly) gives default values to each of the rooms. The showRooms() method takes a room number as a parameter and displays the appropriate room on the form.

Setting Up the Rooms

The setupRooms() method is an interesting method. It’s an artifact from the early development process but is still a useful method. It sets up a default game. Mainly, I used setupRooms() before I had the save and load procedures working, to test the basic operation of the program. Later versions of the program made this method unnecessary, but it remains in the code in case I want to use it again:

private void setupRooms(){
  //used to set up a 'default' game.
  //Also used to test before editor was finished 

theDungeon.Rooms[0] = new Room(
   "Game Over",
   "You have lost",
   0, 0, 0, 0);

 theDungeon.Rooms[1] = new Room(
   "Start",
   "Go North",
   2, 0, 0, 0);

 theDungeon.Rooms[2] = new Room(
   "Room 2",
   "Go East",
   0, 3, 1, 0);

 theDungeon.Rooms[3] = new Room(
   "Room 3",
   "Go South",
   0, 0, 4, 2);

 theDungeon.Rooms[4] = new Room(
   "You Win!",
   "You have won!",
    3, 0, 0, 0);

} // end setupRooms

If you look at the Game class code on the CD-ROM, you will see that it also includes a Main() method. When I started writing this program, I began with the Room and Dungeon classes. Then I wrote the Game class as a standalone program. When I was able to get the basic form of the game working, I was ready to add the editor and main menu classes. It’s very common for programs to live through several iterations like this. Even after I added the other forms, I kept in some of the code that allows the Game class to be run as a standalone program, because I might want that functionality again.

Showing a Room

The main way the game communicates with the user is by loading values in the various labels, based on a given room number. The showRoom() method performs this task:

private  void showRoom(int roomNum){
  // show a room on the form

  int nextRoom;

  currentRoom = roomNum;
  lblName.Text = theDungeon.Rooms[roomNum].Name;
  lblDescription.Text =
    theDungeon.Rooms[roomNum].Description;

  nextRoom = theDungeon.Rooms[roomNum].North;
  lblNorth.Text = theDungeon.Rooms[nextRoom].Name;

  nextRoom = theDungeon.Rooms[roomNum].East;
  lblEast.Text = theDungeon.Rooms[nextRoom].Name;

  nextRoom = theDungeon.Rooms[roomNum].South;
  lblSouth.Text = theDungeon.Rooms[nextRoom].Name;

  nextRoom = theDungeon.Rooms[roomNum].West;
  lblWest.Text = theDungeon.Rooms[nextRoom].Name;

} // end showRoom

The showRoom() method requires a room number as a parameter. It then examines the Rooms array of theDungeon. It extracts the appropriate elements from the current room and copies the values to appropriate parts of the screen. I copied the Name property from the current room to the room name label (lblName). I also copied the Description property over to lblDescription. The Room class stores the indices of the rooms in each direction, but these indices are integers, which mean nothing to the user. Instead, I used the nextRoom variable to determine the index in each direction and then requested the Name property associated with that variable. This results in room names in each direction label.

Responding to Label Events

The user indicates which room he wants to visit next by clicking one of the direction labels. I added code to each of the direction arrows to respond to the user’s requests:

//label events
private void lblNorth_Click(object sender,
         System.EventArgs e) {
  showRoom(theDungeon.Rooms[currentRoom].North);
}

private void lblEast_Click(object sender,
         System.EventArgs e) {
  showRoom(theDungeon.Rooms[currentRoom].East);
}
private void lblSouth_Click(object sender,
         System.EventArgs e) {
  showRoom(theDungeon.Rooms[currentRoom].South);
}

private void lblWest_Click(object sender,
         System.EventArgs e) {
  showRoom(theDungeon.Rooms[currentRoom].West);
}

The code for all the direction labels follows a common plan. In each case, I simply call the showRoom() method with the index of the correct direction property of the current room.

Creating the Open Game Method

I actually created two distinct versions of the OpenGame() method. The first version was needed when the game program was meant to stand on its own. It calls the fileOpener() to request the file name from the user and then reads a dungeon from the file, using the binary formatter to deserialize the data. It then sets the current room to room number 1 and shows that room. Finally, it closes the file stream.

public void OpenGame(){
  //no longer needed
  //read the data from a binary file
  FileStream s;
  BinaryFormatter bf = new BinaryFormatter();
  if (fileOpener.ShowDialog() !=
      DialogResult.Cancel){
    s = new FileStream(fileName, FileMode.Open);   
 theDungeon = (Dungeon) bf.Deserialize(s); 
    currentRoom = 1;
    showRoom(currentRoom);
    s.Close();
  } // end if
} // end openGame

public void OpenGame(Dungeon passedDungeon){
  theDungeon = passedDungeon;
  currentRoom = 1;
  showRoom(currentRoom);
} // end OpenGame

The second version of the OpenGame() method is used when the Game class is run as part of the Adventure Kit. In that case, I decided that the user should choose a game before calling the Game class. As you will see when you examine the MainMenu code, a dungeon will already be loaded in memory when the Game class is started from MainMenu. The new version of OpenGame simply takes a dungeon as a parameter and copies it to theDungeon. It then sets the current room to 1 and shows the room.

Remember, there’s nothing wrong with having two versions of the same method, as long as they have different sets of parameters. The OpenGame() method is a good illustration of the power of polymorphism.

Responding to the Menu Events

The game form supports a very simple menu. The two menu items allow the user to close the game form and return to the main window or to edit the currently loaded game.

The exit code clears the current form from memory using this.Dispose():

private void mnuExit_Click(object sender, System.EventArgs e) {
  this.Dispose();
} // end mnuExit

private void mnuEdit_Click(object sender, System.EventArgs e) {
  Editor theEditor = new Editor();
  theEditor.Show(); 
  theEditor.OpenGame(theDungeon); 
  this.Dispose(); 
} // end game_load 

The Edit menu also closes the game form, but first, it creates an instance of the Editor class, opens the current dungeon in the editor, and displays the Editor class on the screen with its Show() method.

Writing the Editor Class

The adventure program would have been interesting if I had stopped at the Game class. I think that adding the editor makes the game much more interesting, though, because it enables the user community to create many adventures. The Editor class is more challenging than the Game class, but it isn’t too tricky.

Creating the Editor Form's Visual Design

Visually, the editor form looks much like the game form but is designed to let the user edit each field. Figure 9.21 shows the editor form's visual layout. The central description for each room is a text box instead of a label, and each direction is represented with a drop-down list box populated with the names of all the rooms in the dungeon. The user navigates through the rooms with Next and Prev buttons. The editor features save and load dialogs, and its menu structure is slightly more complex than the game program because it allows for saving a game, as well as creating a new game from scratch, closing the editor, and playing the current game.

Figure 9.21. The layout for the editor is similar to the game form, but the controls can be edited.


Building the Editor Class Instance Variables

The instance variables for the Editor class are much like those for the Game class. theDungeon is used to store all the game data, and roomNum stores the index of the current room:

private Dungeon theDungeon = new Dungeon();
private int roomNum = 1;

Initializing in Editor_Load()

As in the game program, I chose to do my own initialization in the Load() event. The setupRooms() method initializes all the rooms to a default value, and setupCombo() assigns the combo boxes the names of the rooms in the dungeon.

private void Editor_Load(object sender,
         System.EventArgs e) {
  setupRooms();
  setupCombos();
} // end editorLoad

Setting Up the Rooms

The setupRooms() method is used to initialize the rooms. It is called when the class first loads and when the user calls for a new game:

private void setupRooms(){
  //initialize rooms
  int i;
  for (i = 0; i < theDungeon.NumRooms; i++){
    theDungeon.Rooms[i] = new Room(
      "room " + Convert.ToString(i),
      "",
      0,0,0,0);
  } // end for loop
} // end setupRooms

The method uses a for loop to step through each room in the dungeon and set its values to appropriate default values. I chose to have the room names include a string representation of the room number because I think that it makes editing a game much easier.

Setting Up the Combo Boxes

The user will edit the game by creating a room at a time. In each room, by using a series of combo boxes, the user will determine what happens when the player goes in a particular direction. The combo boxes contain the current list of room names. Each time the user changes a room name, all the combo boxes need to be updated:

private void setupCombos(){
  //ensures the combo boxes are up-to-date
   int i;

   //clear the combos
   cboNorth.Items.Clear();
   cboEast.Items.Clear();
   cboSouth.Items.Clear();
   cboWest.Items.Clear();

  //repopulate the combos
  for (i = 0; i < theDungeon.NumRooms; i++){
    cboNorth.Items.Add(theDungeon.Rooms[i].Name);
    cboEast.Items.Add(theDungeon.Rooms[i].Name);
    cboSouth.Items.Add(theDungeon.Rooms[i].Name);
    cboWest.Items.Add(theDungeon.Rooms[i].Name);
  } // end for loop

  //preselect room zero
  cboNorth.SelectedIndex = 0;
  cboEast.SelectedIndex = 0;
  cboSouth.SelectedIndex = 0;
  cboWest.SelectedIndex = 0;

} //end setupCombos

The easiest way to update the combos is to clear them out completely and rebuild them. The Items property of the combo box has a Clear() method, which performs this task admirably. The method then steps through each room, adding each room’s name to each combo box. Finally, the method presets each combo so that it points to room 0.

As I developed examples for this chapter, I started evolving my own convention about the games developed with this kit. I reserved room 0 as the “You can’t go there” room and used room 1 as the basic startup room. For that reason, when you call up an adventure in the editor, it will begin in room 0, but if you load the same file into the Game interface, it will begin in room 1.

Showing a Room

The user will be able to move between the rooms with the command buttons at the bottom of the screen. It is important to be able to display any given room:

private void showRoom(){
       //displays a room in editor

  setupCombos();
  txtName.Text = theDungeon.Rooms[roomNum].Name;
  txtDescription.Text =
      theDungeon.Rooms[roomNum].Description;

  cboNorth.SelectedIndex = theDungeon.Rooms[roomNum].North;
  cboEast.SelectedIndex = theDungeon.Rooms[roomNum].East;
  cboSouth.SelectedIndex = theDungeon.Rooms[roomNum].South;
  cboWest.SelectedIndex = theDungeon.Rooms[roomNum].West;
  lblRoomNum.Text = "room " + Convert.ToString(roomNum);
} // end showRoom

The first task is to reset the combo boxes to take into account any changes in the room data. After that, the method copies the name and description to the appropriate text boxes. Rather than copy the direction values into the database, these numeric values are used to set the index of the combo boxes to the appropriate value, which will display the room number associated with the room. For example, if the North value of the current room is 3, the third element of the combo box will be the name of room 3, because of the setupCombos() call. Setting cboNorth.SelectedIndex to 3 causes the third element of the combo box to appear, which will be the name of room 3.

Storing a Room

Storing a room is the logical opposite of saving the room. Basically, the method copies values from the form elements to the current room. Note that the selected index of the direction combos is set, not the text value:

private void storeRoom(){
  //stores the current room to the database
  theDungeon.Rooms[roomNum].Name = txtName.Text;
  theDungeon.Rooms[roomNum].Description =
              txtDescription.Text;

  theDungeon.Rooms[roomNum].North = cboNorth.SelectedIndex;
  theDungeon.Rooms[roomNum].East = cboEast.SelectedIndex;
  theDungeon.Rooms[roomNum].South = cboSouth.SelectedIndex;
  theDungeon.Rooms[roomNum].West = cboWest.SelectedIndex;
} // end storeRoom

The relationship between the directional values and the list boxes illustrates an important point. The numeric values are convenient from the programmer’s perspective because they are unambiguous. It is very easy to see which room number to display next if the user clicks the North label. However, human users much prefer text or visual cues to numeric values. The value of the combo boxes is how the way to bridge this gap. When I’m interested in the actual numeric value associated with a direction, I use the selectedIndex property. The user can just deal with the string values without knowing or caring that the position of something in the list box is what matters to the program, not what it says.

Responding to the Next and Prev Button Events

The Next and Prev buttons are used to let the user navigate between records in the game editor. They do a lot of work, but most of that work is encapsulated into the storeRoom() and showRoom() methods you’ve already seen:

private void btnPrev_Click(object sender,
         System.EventArgs e) {
  storeRoom();
  roomNum--;
  if (roomNum < 0){
    roomNum = 0;
  } // end if
  showRoom();
} // end btn prev

private void btnNext_Click(object sender,
         System.EventArgs e) {
  storeRoom(); 
  roomNum++;
  if (roomNum >= theDungeon.NumRooms){
    roomNum = theDungeon.NumRooms - 1;
  } // end if
  showRoom();
} // end btnNext click

The btnPrev_Click event stores the current room to preserve any changes that have happened. It then decrements the room number and checks whether the room number is less than 0. If so, the room number is set to 0. The call to showRoom() shows the room, based on the current room number.

The btnNext_Click event works in very much the same way, except that it increments the variable, rather than decrements it, and checks whether the value is larger than or equal to the number of rooms in the dungeon. If so, roomNum is set to the number of rooms–1. (Remember, arrays begin with an index of 0, so the largest possible value will be theDungeon.NumRooms - 1.

Saving the Adventure

The adventure game is saved with the now familiar binary serialization technique. The method starts by pulling the game’s name from its textbox and assigning the result to the Name property of theDungeon. Then the current room is stored in case changes have been made but the Next or Prev button hasn’t been clicked. The data is stored in the file, using a FileStream and a BinaryFormatter:

private void mnuSaveAs_Click(object sender,
         System.EventArgs e) {
  //get the game's name
  theDungeon.Name = txtGameName.Text;
  //store the current room
  storeRoom();
  //write the data out to a binary file
  FileStream s;
  BinaryFormatter bf = new BinaryFormatter();
  if (fileSaver.ShowDialog() != DialogResult.Cancel){
    s = new FileStream(fileSaver.FileName, FileMode.Create);
    bf.Serialize(s, theDungeon);
    s.Close();
  } // end if
} // end mnuSave

Loading the Adventure

Loading the adventure works just as it did in the Game class, using binary serialization. After I loaded the game in memory, I set the room number to 1 and showed the room:

private void mnuOpen_Click(object sender,
        System.EventArgs e) {
  //read the data from a binary file
  FileStream s;
  BinaryFormatter bf = new BinaryFormatter();
  if (fileOpener.ShowDialog() != DialogResult.Cancel){
    s = new FileStream(fileOpener.FileName, FileMode.Open);
    theDungeon = (Dungeon) bf.Deserialize(s);
    roomNum = 0;
    showRoom();
    s.Close();
  } // end if
} // end mnuOpen

Opening a Game

As in the Game class, it will be possible for the MainForm to start the editor remotely, so I added an OpenGame() method that will start the editor when given a Dungeon as a parameter:

public void OpenGame(Dungeon passedDungeon){
  theDungeon = passedDungeon;
  roomNum = 0;
  txtGameName.Text = theDungeon.Name;
  showRoom();
} // end Open_game

The openGame() method simply copies the passed dungeon parameter to theDungeon, initializes the room number to 0, copies the dungeon name to the appropriate text box, and shows the room.

Exiting, Playing, and Creating a New Game

The other menu event handlers are (as usual) standard fare. When the user chooses to exit the editor, I use this.Dispose() to eliminate the form from memory.

If the user wants to create a new game, the setupRooms() method does most of the work, but I also reset roomNum to 0 and showed room 0. Finally, I reset the txtGameName text box to empty.

The mnuPlay_Click() method directly calls the game screen so that the user can immediately play whatever adventure he has been working on without having to return to the main form first. It opens a new instance of the Game class, shows the form, and opens up the current dungeon with a call to theGame.OpenGame(). Finally, the method disposes the current (Editor) class to get it out of the way.

private void mnuExit_Click(object sender,
  System.EventArgs e) {
  this.Dispose();
} // end mnuExit

private void mnuNewGame_Click(object sender,
         System.EventArgs e) {
  setupRooms();
  roomNum = 0;
  showRoom();
  txtGameName.Text = "";
} // end mnuNewGame

private void mnuPlay_Click(object sender,
        System.EventArgs e) {
  Game theGame = new Game();
  theGame.Show();
  theGame.OpenGame(theDungeon);
  this.Dispose();
} // end mnuPlay

Writing the MainForm Class

The MainForm class is the user’s initial entry and exit point to the program. It serves mainly as a control center, routing the user between the other forms. Although it appears to the user to be the main part of the program, it is actually the simplest part of the project, and the last part I designed. Each button calls up the appropriate form or closes the program altogether.

Creating the Main Form's Visual Design

The primary purpose of the main form is to tie the rest of the program together. Ironically, the form is most effective if the user spends very little time on it at all. For this reason, all the main actions belong to prominent command buttons that dominate the form. To keep the program appearance unified, I assigned the same scroll background to each button and to a picture box on the form. The form also has a label indicating which game, if any, is currently loaded in memory.

Building the MainForm Class Instance Variables

The MainForm class uses instance variables to control each form, the adventure data, and a file name for the current game:

//classes for game and editor screens
private Editor theEditor = new Editor();
private Game theGame = new Game();
private Dungeon theDungeon = new Dungeon();
private string gameFile = "";

Loading the Dungeon

The MainForm class is capable of loading a dungeon class before calling the other classes. This ensures that the Game class is never called without a valid dungeon in memory. Also, this makes debugging your adventures easier because you can simultaneously call a game window and an editor window to see how your game plays while you examine it in the editor.

private void btnLoad_Click(object sender,
         System.EventArgs e) {

  //read the data from a binary file
  FileStream s;
  BinaryFormatter bf = new BinaryFormatter();
  if (fileOpener.ShowDialog() != DialogResult.Cancel){
    gameFile = fileOpener.FileName;
    s = new FileStream(fileOpener.FileName, FileMode.Open);
    theDungeon = (Dungeon) bf.Deserialize(s);
    s.Close();
    lblCurrent.Text = "Game: " + theDungeon.Name;
  } // end if
} // end btnLoad

The binary serialization technique, which has been such a workhorse in this program, is called into service one more time. As usual, the value of the file is copied to a Dungeon instance after being deserialized.

This theDungeon variable will be used to call the OpenGame() methods of the other two forms.

Playing the Game

If the user wants to play a game, the program first checks whether a game has been loaded. If not, it asks the player to do so. If a game has been loaded, starting up the game window is a simple process:

private void btnPlay_Click(object sender,
         System.EventArgs e) {
   if (gameFile != ""){
     theGame = new Game();
     theGame.Show();
     theGame.OpenGame(theDungeon);
   } else {
     MessageBox.Show("Please load a game first");
   } // end if
} // end btnPlay

If a game has been successfully loaded into the program, the value of the gameFile variable will be something besides its starting value of "". In that case, the program creates an instance of the Game class, shows the form, and opens the game stored in the current dungeon. If the value of gameFile is still null, the method reminds the user to load a game before trying to play it.

Editing the Game

The process for opening the editor is simpler than for opening the game because it’s reasonable for the user to open up the editor without a game in memory, especially if the user wants to create a new game from scratch.

private void btnEdit_Click(object sender,
         System.EventArgs e) {
   theEditor = new Editor();
   theEditor.Show();
   if (gameFile != ""){
     theEditor.OpenGame(theDungeon); 
  } // end if
} // end btnEdit

The method simply creates a new instance of the Editor class, displays the form, and opens up the game if it is already in memory.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.14.131.212