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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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;
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
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.
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.
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 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.
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.
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 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
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.
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
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.
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.
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 = "";
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.
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.
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.
3.14.131.212