Lesson 19.4 – Saving and loading the player information

Lesson objectives

At the end of this lesson, you will know…

  • The basics of XML (Extensible Markup Language)
  • How to save the player information to disk and reload it when the player restarts the game.
  • How to make prevent your program from crashing, if it reads in a bad file.
  • A very simple version of the “Factory” design pattern

 

Someone recently asked me how to save the player information for a game and reload it when they restart the game.

So, here’s a new lesson on how to do that.

 

NOTE: There is a lot happening in this lesson. But that’s what happens when you start to go beyond simple coding examples. In order to do one thing, you need to know how to do a couple other things. If you have problems understanding this lesson, take it one step at a time, until you are sure you know what is happening.

 

There are several different ways you could save data: in a database, a comma-separated value file, an XML file, etc.

We’re going to use an XML file. It doesn’t require installing anything extra, such as a database. It’s also easy to read, since an XML file includes the structure (definition) and the data.

 

Defining the XML file structure

It’s easier to understand XML with a sample. So, here is the format we’ll use to store the game’s data.

 

What is XML?

Think of XML data as one long string, or file, with special formatting rules.

There are three main parts to XML: nodes, attributes, and values.

 

“Nodes”, or “tags”, are the names of values, kind of like a property in a class.

In our file, the nodes are: Player, Stats, CurrentHitPoints, Gold, ExperiencePoints, Currentlocation, InventoryItems, InventoryItem, PlayerQuests, and PlayerQuest.

A few thing you need to know about XML nodes:

  1. Nodes have a start and an end. Notice that there is <Player> node at the very beginning, and </Player> at the end. The “/” before the node name indicates that it is the end of the node.
  2. Node start and end names must match exactly. They are case-sensitive. So, you cannot start a node with “<NAME>” and end it with “</name>”.
  3. Node names cannot contain a space. So, we can have a node named “CurrentHitPoints”, but not one named “Current Hit Points”.
  4. Node starts and ends cannot cross. For example, you can have <A><B><C></C></B></A>, but you cannot have <A><B><C></B></C></A>. In the second example, the C node is started after the B node; however, the B node ends before the C node ends, which is not allowed in XML.
  5. Nodes can also be “self-closing”. Notice that the InventoryItem nodes don’t have a corresponding </InventoryItem>. The “end” of those nodes is done with the “/” just before the “>”.
  6. If you start a new node before the previous node ends (like the CurrentHitPoints node, being between the start and end of the Stats node) then it is called a “child node”. CurrentHitPoints is a child node of Stats, and Stats is a child node of Player.
  7. A node can have multiple child nodes, even with the same name. See how InventoryItems has multiple InventoryItem nodes.
  8. You can store data as “values”, like the 7 in CurrentHitPoints, or as “attributes”, like the ID and Quantity in the InventoryItems.

 

Notice in the InventoryItem nodes, we store the ID and Quantity as attributes. If you wanted to, you could store them as values in child nodes.

So, instead of this:

You could have this:

It’s personal preference, which way you do it.

 

That’s enough information about XML for us to get started. There are some more rules you need to know about, if you want to do more work with XML. Check out the Wikipedia page on XML to learn about them.

One thing to watch out for is if you ever output user-entered data into XML. If the user enters a less-than sign “<“, XML thinks it is the beginning of a node – unless you follow some of the special techniques you’ll find on the Wikipedia page (look for CDATA).

 

Why we are writing our own serialization code, instead of using the built-in serialization

Sometimes you need to pass objects to something outside your program. The problem is that it doesn’t understand your program’s object, or class. One example is the file system, which understands strings (and a few other things).

One way to do pass an object is to use “serialization”.

There are built-in functions in the .Net framework to do serialization (converting from an object) and deserialization (converting back into an object).

However, these built-in functions can be a little complex to use, especially when you have an object that contains a list of other objects. You need to modify all your classes to know how to serialize/de-serialize themselves.

That’s a lot of work for a small game. So, we won’t be using that.

We’ll write our own functions to convert the Player object information to XML, and back from XML into a Player object.

By the way, there are several types of serialization. You may have also heard of JSON (JavaScript Object Notation), which is used to transfer object information in many web sites and web apps. It uses a different format for its data, but the concept is similar to XML.

 

OK, enough talk. Time to write some code.

 

Saving and reloading player information

Step 1: Open the SuperAdventure solution in Visual Studio and select the Player class to modify.

Step 2: At the top of the Player class, add this line to the “using” statement section, so we have access to the XML functions.

 

Step 3: Add this new ToXMLString() function to the Player class. This will take the Player information and create an XML string with all the Player’s current data.

 

At the start of this function, we create the playerData XmlDocument. This object lets us add nodes in a safe way, so we can’t break the most common XML formatting rules.

Now that we have the XmlDocument, we’ll populate it with our player data.

First, we create the “Player” XmlNode and add it to the document.

Next, we create the “Stats” XmlNode and add it as a child node to the Player node. That means it will start, and end, between the start and end of the Player node tags <Player> and </Player>.

Then we create nodes for the CurrentHitPoints, MaximumHitPoints, Gold, ExperiencePoints, and CurrentLocation. These nodes hold data, so their only child nodes are the values added to them with the CreateTextNode function.

Notice that we don’t have a node to store the Level value. That’s because Level is always calculated from the ExperiencePoints.

These nodes are all child nodes of “Stats”. So, when we finish creating them, we use the AppendChild method on the Stats node.

Then we add the “InventoryItems” node, and “InventoryItem” nodes for all the items in the player’s inventory.

Notice that we are using the CreateAttribute method to add the values to the nodes. After creating the attribute, we set its value.

Finally, we add the “PlayerQuests” node and its “PlayerQuest” child nodes (if any exist)

Since we are writing the XML value to a file, we return the InnerXml property of the XmlDocument, which is all the XML as a string.

 

This is probably a good time to rebuild your solution, and see if there are any errors, before we go to the next step.

 

Step 4: The next step is to make a new constructor for the Player class that takes the XML data and creates a new Player object, with the values from it.

This is where we’ll use the “Factory” design pattern.

A design pattern is basically a general method, or technique, to do something. It’s good to learn the common ones, especially if you work with other programmers. Then, instead of giving a long explanation of what you’re doing, you can just say, “It’s a Singleton,” or, “Use a Decorator.” Then the other programmer will know what you’re doing.

So far, in this program, whenever we want an object, we use the constructor to create an instance of the class. We’re going to change that for the Player class. We’re going to use a factory to create the object for us.

Replace the constructor for the Player class with the code below (and add in the two new functions).

 

Notice that the constructor is now private. That means it can only be called by another function inside the Player class. We didn’t need to do this. However, since we are only going to use the other two methods to create a Player object, I made it private.

So now, if you want to create a new Player object, you need to either call the CreateDefaultPlayer function or the CreatePlayerFromXmlString function. These methods are public. So they can be accessed by the rest of the solution. Since they are static, you can call them directly from the class, without an object (you’ll see to how to do that in a minute).

The CreateDefaultPlayer function should look familiar. It’s basically the same thing that the SuperAdventure page does when it creates a new player.

The CreatePlayerFromXmlString function is where we take the XML string with the game’s data, read the values in it, and create a player object with the values from the saved game.

First, we load the string into an XmlDocument object.

To get the data that only has a single value, we use the SelectSingleNode function on the XmlDocument. We give it the XPath of the data (like “/Player/Stats/CurrentHitPoints”). The “InnerText” says to get the value. InnerText always returns a string, so we need to wrap all that with Convert.ToInt32, to get the number.

Think of XPath, and child nodes, kind of like directories/folders, with sub-directories/folders, on your hard disk.

For the nodes that can have multiple items (inventory items and player quests), we use the SelectNodes function on the XmlDocument. This gets every node that matches the XPath. For each node, we create an InventoryItem, or PlayerQuest, object, set its values, and add it to the player object.

Notice that this function has a “try” and a “catch” section. This is known as a “try catch block”. What happens here is that the program tries to run everything in the “try” section. If there is an error, it does whatever is in the “catch” section.

We have this here in case there is ever a problem with the file (like, if someone tries to modify it, and messes it up). Instead of the user seeing an error, the program will execute the “catch” block and create the default player for the game.

Try catch blocks are a very powerful tool in professional programming. Instead of having your program crash, you can have it deal with problems more gracefully.

 

Don’t try building your program now. Since we made the Player constructor private, we need to make a couple changes before we can build without errors.

 

Step 5: Next, we need to have the program write the player’s information to disk when they exit the program.

We’re going to do this by adding code to the “FormClosing” event of the program. Whenever the program is being closed (stopped), this function will run. It will take the player’s current information, using the new ToXMLString() method of the Player class, and write it to the disk, in a file named “PlayerData.XML”

Select the SuperAdventure.cs file, and open it in design mode (so you see the screen). In the lower-right corner of Visual Studio, in the Properties box, click on the lightning symbol.

Events

Now you see all the events that this form has. Scroll to the “FormClosing” event, and double-click on it.

FormClosingEvent

That should have taken you to the code for the SuperAdventure form and created a function named “SuperAdventure_FormClosing”. This is the function that will be run when you close the program.

 

NOTE: It is important to create the function this way. This method adds a line of code to another file that connects the function to the FormClosing event. If you only copy/paste the new SuperAdventure.cs code into your program, that line will not be created, and this function will not ever run.

 

Before we add the code for this function, we need to include a reference to the library that lets us write to, and read from, the disk. Go to the top of the page and add this line to the section with the other “using” statements:

 

Scroll down to the section where we define our class-level variables _player and _currentMonster (this should be around line 20). Add this new line as class-level constant:

 

That is the file name where we will save the player’s data. We made it a constant, because it doesn’t change. And it’s a class-level variable, so we can use it in the function that creates the file and the function that loads the file. We need it visible in both places.

Go back to the SuperAdventure_FormClosing function and change it to this:

 

Now, when the program is closing, it will write the player’s data, in our XML format, to the PlayerData.xml file.

 

Step 6: Finally, when the game starts, we want to look for the PlayerData.xml file.

If it exists, we’ll read it and create the player object from its values. If it doesn’t exist, we’ll assume we are starting a new game and use the normal Player constructor to create a new Player object.

Change the “public SuperAdventure” function to this:

 

When the game starts, it will look for the PlayerData.xml file.

If the file exists, it will read it and create the Player object with the values in the file. If it doesn’t find the file, it will create the default Player object.

Now you can build the solution.

Try moving the player to a new location, then exit the game and restart it. You should see that the game starts with the player in the new location.

If you ever want to have the player start over, you can manually delete the PlayerData.xml file (it will be in the same directory as SuperAdventure.exe). If you want to get fancy, you could add a menu to the game and let the play save and restore multiple games, with different file names.

But, I’m going to stop for now. We’re already way over 2000 words in this lesson.

 

Check your work

Build the solution and make sure there are no errors in the “output” box at the bottom of Visual Studio. If you see any problems, double-check the changes you made in this lesson.

 

Summary

This was a big lesson, with some new concepts.

If you want to become a professional programmer, read about design patterns and learn about good practices to use with try catch blocks.

 

Source code for this lesson

Get it from GitHub: https://gist.github.com/ScottLilly/01f71afc73843dfa2f0a

or DropBox: Lesson 19.4 – https://www.dropbox.com/sh/kpswil2xtw55h71/AAB2VVkmDWLsl7aPj63R0N7Ya?dl=0

 

Previous lesson: Lesson 19.3 – Clean up the source code by converting foreach to LINQ

Next lesson: Lesson 19.5 – Changing dropdown default values

All lessons: Learn C# by Building a Simple RPG Index

38 thoughts on “Lesson 19.4 – Saving and loading the player information

    1. You could encrypt the XML string before you write it to the file. When you read in the data, you couldn’t use the LoadXml(). You’d need to read in the file with a method like this https://msdn.microsoft.com/en-us/library/ezwyzy7b.aspx, then decrypt it before loading it into the XML document.

      If you don’t need a super secure level of encryption (you just want to prevent average players from modifying the data), you could use a Base64 conversion on the string, like shown here: https://msdn.microsoft.com/en-us/library/dhx0d524%28v=vs.110%29.aspx. If you need something more secure, you want to look at the System.Security.Cryptography namespace in the .Net framework. You can make your data more secure, but there is more work involved.

  1. Uhm, I’m having a problem here, my game is saving the PlayerData.xml once i close it, it generates the file, but when i open then file i just start a new game, yet when debuging it the game goes through

    if (File.Exists(PLAYER_DATA_FILE_NAME))
    {
    _player = Player.CreatePlayerFromXmlString(File.ReadAllText(PLAYER_DATA_FILE_NAME));
    }

    and it shows the “PlayerData.xml” file on it, i’m going to look better onto this but help would be appreciated, also can you make a tutorial to show us how to add Merchants? and maybe armor/spells

     

    1. The first thing I’d do is verify that the PlayerData.xml is properly-formed XML. If it isn’t, that will cause an error when the program tries to read it. You can do this by opening the PlayerData.XML file in a browser. If should look something like this:

      If it’s not properly-formed XML, the browser will give you an error message. Then you’d need to double-check the code in Player.ToXmlString() method.

      If that looks good, I’d modify the “catch” part of Player.CreatePlayerFromXmlString(), to this:

      catch(Exception ex)
      {
      string errorMessage = ex.Message;

      // If there was an error with the XML data, return a default player object
      return Player.CreateDefaultPlayer();
      }

      Then, set a breakpoint on the line where you assign a value to the errorMessage variable. I suspect the program is getting an error reading the file, causing it to start with the default (beginning) game information. If this happens, you can examine the “ex” variable to find out exactly which line has the error. That should get you right on the cause of the error. But, let me know if it doesn’t.

      I’m also finally getting back into working on an expanded version of the game. I’ve been busy moving back to the US, getting an apartment/job/etc. But things have finally settled down, and I’m getting some free time again.

  2. Hey so Im trying to make mine a little fancier and cant seem to get it to work. When you my game it prompts the user to either select to load the game or choose a new one. If they choose load game, it just loads the game back to where you left off, when you hit new game, it takes the user to a screen where they can input there name and hit start. What I want it to do is when you hit new game, to completely start a new game, even if there is a saved game on file. Any ideas on how to make this work?

    1. Also I cant seem to get it to save the player name working how it is now. Im pretty sure Ive added all the code in the needed spots, but when you leave the game, the name you set does not reappear. Any idea why this is happening?

      1. With XML, it’s important to make sure you write and read the data in the exact same place (XPath), with the exact same name – even matching the upper/lower-case of the node and attribute names. I’d check the XML file, after you save the game, to make sure it’s writing the data where you expect it to. If that looks good, I’d put a debugger breakpoint in the Player.CreatePlayerFromXmlString() method, and watch what happens when you read in the XML file. Make sure it reads it in fine and take a look at the XmlDocument.

        If you can’t find the problem that way, please post the Player.CreatePlayerFromXmlString() and Player.ToXmlString() source code to https://gist.github.com/, and I’ll take a look at it.

    2. How do you ask the player if they want to start a new game or re-load the last one?

      The two ways I’m thinking of would be to either have a MessageBox pop-up and ask them during the startup of the SuperAdventure UI form, or have a separate UI form that starts up, then calls the SuperAdventure form. The change would depend on how you’re doing it. If you can post the code online somewhere, like https://gist.github.com/, and send me the link, I can take a look at it and give you a specific answer.

      1. Sorry it took me so long to reply! Yes the way I have it is when you click the launch to run the game, a windows form called StartScreen pops up that has two buttons, one to load the game and one to start a new game. If you click the load game, I want it to load the saved game if there is one, if not, display a message box saying “There is no saved file.”. The new game button takes you to another form called NewGame which lets you input your name, and click the start button to take you to the game. I have included the necessary files to work with to solve this in git hub at this link: https://gist.github.com/anonymous/efdf6bff5fa52a73cc8a.

        I have been fiddling around trying to get it to work but so far have nothing good to show. The code posted does not work correctly, as of now the load game just takes you to the game and the new game button, takes you too the new game, but will not start a new game if there is one saved already. Any ideas how to make this work the way I want it to?

        1. OK. What you need to do is get your answer from the NewGame screen to the Adventure screen. Here is a way to do that.

          In Adventure.cs, change line 22 to this:

          public Adventure(bool startNewGame)

          Then, in StartScreen.cs, change line 26 to:

          Adventure loadGame = new Adventure(false);

          And change line 37 of NewGame.cs to:

          Adventure newGame = new Adventure(true);

          Now, the constructor of Adventure.cs will know if it needs to start a new game or not. You’ll need to change the conditions on lines 26 through 33 of Adventure.cs, to use the “startNewGame” variable to decide whether or not to load the existing game (if it exists), or to start a new game.

          You can do the same type of thing to pass in the player name from NewGame.cs. Add a second parameter to the Adventure.cs contructor. You’ll need to pass in an empty string for the name parameter, from line 26 of StartScreen.cs, and you’ll just ignore that, since you’ll get the player’s name from the XML file.

          Let me know if that needs clarification, or if you have any trouble adding that to your game.

          1. Yes that worked! It now will load and save correctly. There is one thing that I still cant get to work though. For some reason it will not save the players name. I changed the way you get it from the new game form to what you said to, and I’m pretty sure I edited all the XML methods to include a name field, but maybe I missed something.
            I put a link to those files, if you want to take a look at it. Again thanks for all your help already!

            https://gist.github.com/anonymous/9569502ca3f3bffc8670

          2. You are close. So I’ll give you a hint, instead of telling you exactly what to change.

            In Adventure.cs, on lines 34 and 40, you create a new _player object. Where are you setting the PlayerName property on that _player object?

          3. Thank you for all your help getting this to work. Now I can start working on adding more. One thing Im trying to implement is a picture box that changes an image based on your location. Now since I have roughly 200 locations, at the moment, I dont set each location to have a picture. Say for instance There is 20 open spaces that all use the same picture. Rather than set each location to have the same picture, I have it set to change the picture when you first come into contact with that area. This saves a lot of time and space for coding however there is a problem with it. If you exit the game, and load it again, the image that was there will not be, unless it has code to set the image. So for all those locations that dont have a set image method, they appear blank when reloading the game. Soooooo after all that my question is, can you write an image to an xml file to save it? And if so how? I have tried a way to get it to work, by simply just adding another field to the player object and putting it in the XML function, but kept running into error after error. Any idea how to make this possible but functional?

          4. You’re welcome.

            Personally, I’d set the image for each location (in a new property of the Location class). That’s probably the simplest solution, and simple solutions usually pay off as you make more changes later.

            If you don’t want to do that, I’d create a new property in the Player class for CurrentImage, to hold the name of the image to load. In the SuperAdventure.MoveTo() function, after the line “_player.CurrentLocation = newLocation;”, I’d add this:

            if(newLocation.ImageName != null)
            {
            _player.CurrentImage = newLocation.ImageName;
            }

            // Add code to load _player.CurrentImage into the picture box here

            Then I’d change the Player ToXmlString() and CreatePlayerFromXmlString() functions to save and retrieve the Player.CurrentImage value.

            I think that should work. Since you call MoveTo(), in SuperAdventure.cs, after creating the _player object from the saved game, it won’t overwrite the _player.CurrentImage value for locations without a CurrentImage value.

  3. When I got to the point where I had to change “public SuperAdventure” function (at the end of the lesson), the last line of the new function, which is “UpdatePlayerStats();” is underlined with red line in my code and says ” it does not exists in the current context” what could be the problem? I think I followed the descriptions exactly, and before step4 I rebuilt the solution and worked well!

    BTW: enjoyed the whole lesson, successfully made the game working, learned a lot of new things! 🙂 I want to create similar game with little graphics and with hunger/thirst in the near future! 🙂 (traveling costs hunger and thirst, and player needs food and drink, kind of survival)

    Thanks for your help and for the GREAT tutorials! – David

    1. Hi David. Thanks for your comment!

      It sounds like the UpdatePlayerStats() function is missing from SuperAdventure.cs. The code for it is from Step 5 of Lesson 19.2, or just add this into SuperAdventure.cs:

      private void UpdatePlayerStats()
      {
      // Refresh player information and inventory controls
      lblHitPoints.Text = _player.CurrentHitPoints.ToString();
      lblGold.Text = _player.Gold.ToString();
      lblExperience.Text = _player.ExperiencePoints.ToString();
      lblLevel.Text = _player.Level.ToString();
      }

      I’ll review the lessons this weekend, in case that function is missing from the source code on GitHub or in Dropbox. It was a little difficult keeping the code synchronized everywhere.

      FYI: In December, I’m going to restart work on an open-source RPG engine with a lot more features (and cleaner code). Since you want to create more games, you might want to check it out at: https://github.com/ScottLilly/ScottsOpenSourceRPG

  4. Hi Scott,

    I just finished going through the excellent Super Adventure RPG builder tutorial, i cant tell you how interesting and helpful i have found this!

    I was wondering if you had any plans for showing how we could adapt this to start storing the data in a simple database rather than XML file output? or if you have any good suggested reading on the topic of basics around setting up your first database?

    Thanks

     

    Toby

  5. First off, want to say love the guide and good work on it.

    Disclaimer: I didn’t copy and paste and code because I am writing the application on a different computer than the guide.

    So, everything works as normal. However, when I start the game in debugging mode and move around/attack monsters/etc. and then quit and reopen it. It doesn’t load the PlayerData.xml. Instead it creates a new default player. But the PlayerData.xml file is still being created and written to with updated information. Do you know what could be causing this?

    Also, when I am playing the game and finish the first quest, clearing the alchemist’s garden, The rtbMessage display shows that I’ve received the rewardItem, however it shows it as Engine.HealthPotion. Do you know what could be causing this??

     

    Thank you

    1. To add onto my previous question about the Engine.HealthPotion. When I get the adventurer’s pass, it is also displayed in the rtbMessage area as Engine.Item.

      1. In case you checked my response right after I posted it, I mistakenly pointed at a similar solution for datagrids, and not the richtextbox (I’m working on a new lesson for databinding to datagrids, and that’s stuck in my brain right now). I’ve fixed it to answer the question for the richtextbox control.

    2. Thank you Matthew!

      Check the code that gives the player the quest reward, and make sure it says:

      rtbMessages.Text += newLocation.QuestAvailableHere.RewardItem.Name + Environment.NewLine;

      I suspect it has this in there right now:

      rtbMessages.Text += newLocation.QuestAvailableHere.RewardItem + Environment.NewLine;

      Notice that the second line only has “RewardItem”, and not “RewardItem.Name”. That’s the most likely reason it would show the object/class name, and not the “Name” property of the object.

      Not loading the PlayerData.xml file might be trickier to track down. I made a video to show you how to use the debugger (link below), if you want to try finding the problem on your own. You’d want to put a breakpoint in the constructor of the SuperAdventure.cs class, on this line:

      if(File.Exists(PLAYER_DATA_FILE_NAME))

      Then, you can watch what is happening in your program. I suspect there may be a typo in either the ToXmlString() or CreatePlayerFromXmlString() function of the Player class. XML is very picky, and one little typo will cause an error.

      If you’re still stuck, let me know and we can dig into it some more.

      “Live Coding” example of debugging a C# program

  6. I am up to date with the tutorial atm and the latest part thats up is https://scottlilly.com/learn-c-by-building-a-simple-rpg-index/lesson-21-4-completing-the-trading-screen/ .

    And Everything works exept saving with xml. I dont know what happend really but I had not done any of the “making the code easier” parts but I now have becuase I wanted to add the vendor which I now have and it works. its just that I must have done something wrong and I cant figure out why it wont create the PlayerData.xml file anymore.

    PS: one thing I dont understand either is the “UNSELLABLE_ITEM_PRICE” I dont know what to create with it. sorry if its obvius and I missed it. But I just created a new int in the world.cs thats named “UNSELLABLE_ITEM_PRICE“.

    thx in advance!

    1. Usually, if there is a problem with XML, it is because the node names are different between when they were written, and when they are being read. If the name is mis-typed in one place, that will cause an error – and we do not do error-checking in this project. Can you upload your Player.cs file somewhere (Dropbox, Google Drive, Gist)? Then I can look at it and see if I can find the problem.

      For UNSELLABLE_ITEM_PRICE, did you add this line: public const int UNSELLABLE_ITEM_PRICE = -1; to the World class, in step 4 of Lesson 21.1? A variable, with a descriptive name, will prevent you from asking yourself, “Why do some items have a price of -1?”, when you make changes in the future.

      Please let me know if that answered your question.

  7. I managed to figure it out! For some reason the close function was not in the event handler thing. So I just had to press the little arrow in the event box and choose a function/method that the program would be executing when it closed^^

    Thx again for all the help and keep up with the great tutorials! I absolutely love them!

  8. Hey Scott!

    First off, sorry for bothering you so much.

    After reading your explanation and example code for saving and rebuilding the player from XML I wanted to try and manually rebuild your code myself, understanding everything one line at a time with no copy pasting (just reading your code again when I got stuck).

    When writing the CreatePlayerFromXmlString I did the same thing as you did except didnt in-line the collection of xmlnodes because it seemed easier for me like this (because in my mind it seems as if you are running the selectnodes to get the collection after every loop cycle over and over again, while not inlining means only getting the collection once):

    XmlNodeList nodes = playerData.SelectNodes(“/Player/InventoryItems/InventoryItem”);
    foreach (XmlNode node in nodes)
    {
    // read ID and Quantity
    // Create new InventoryItem object
    // add to inventory
    }

    So as you can see I comment what I want to do first and then work through my list of whatever it is I want to do. Then I read your code to check if I was correct, but I saw that you merely used the AddItemToInventory method to loop using the Quantity property to determine the amount of loop cycles. And you didnt have to create an inventoryitem object here because the AddItemToInventory method takes care of that.

    Now the code isn’t hard to understand for me having some experience as a developer. Understanding code that someone else wrote is the easiest thing to learn from.

    The thing that is hard for me to understand (and this is my real actual question) is how you thought of this design in advance!

    When you designed this game, did you think in advance like “well my method is gonna take an item object and I’m going to let that method ‘convert’ it to an inventoryitem rather than adding inventoryitems directly”. Do you design the entire game on paper in advance? Or do you get these insights while going along the way? I probably wouldnt have thought of this in advance (probably creating new InventoryItems in the loadfromxml method, resulting in bad coding practices). Were you able to code like this when you were starting out, or did you learn it from experience (so that I may be able to write code like that later on as well)?!

    Sorry for the wall of text , I just get these thoughts when coding and having fun and I realise there’s still alot for me to learn. This tutorial is awesome 🙂

    1. Hi J!

      It’s no bother 🙂

      When I came up with the idea for the game, I didn’t do much designing. I only thought of the basic functionality I wanted in the game (player moves to locations, gets quests, fights monsters, receives loot, etc.) That gave me the main “business” classes – Player, Location, etc.

      Then, I built the program in the same order as the lessons. For each feature I added, I’d follow this order:
      • Figure out what feature I want to add (and any sub-features it needs)
      • Come up with a rough plan (like what you did with the comments)
      • Write the code for the feature
      • Test that it worked
      • Clean up the code after everything works (I didn’t do that for all these lessons, but that’s what I do in my “real-life” programs.

      What you did, with writing comments, is something many programmers have done for decades. It’s called pseudo-coding. You write down a plan for the functionality to want to do, see if there is anything missing from the plan, then write the code.

      If you started the pseudo-code at a little higher level, it might have looked like this:
      • For each inventory node
      o Read the value from the XML file
      o Add the item to the player’s inventory

      Then, you might remember that you already have a function that adds items to the player’s inventory. So, you only need to pass the XML values to that function, instead of writing something new.

      Or, you could have “refactored” after adding each function. That way, if you wrote the inventory-adding logic inside the loop, you would review your code afterwards. That’s when you might notice the duplicated code, and change the loop to call the already-existing function.

      Much of my decisions for how to write a feature is based on experience. After you do some technique hundreds of times, you just “know” how/when/where to use that technique again. But, there are a couple ways you can gain this type of experience faster.

      Learn design patterns. These are standard solutions to common problems. When you learn these, and work on programs, you’ll start to see places where you should implement those patterns (although, don’t make the mistake of forcing design patterns into places where they aren’t needed). I’m creating a couple videos per month on C# design patterns, but if you want to learn them faster, check out “Head First Design Patterns”. The code samples in the book are in Java, but it’s close enough to C# that you probably won’t have any problems understanding them.

      You could also do programming “katas”. Try to re-write a feature a different way (after saving your current version, so you can go back to it). If a function has a lot of “if” statements, try to re-write it without any “if”s. That might lead you to learning about polymorphism – which will make you familiar another technique. Try to eliminate base classes. That might lead to learning about “composition over inheritance”.

      The important thing is to try new techniques, see where they are good, and see where they are bad. That will give you “more tools in your toolbox”.

  9. Hello.

    I ran into a problem. While writing/loading from the XML file works just fine, I wonder what I could do to make the code slimmer.

    For example, this is a part of my “Save” method:

    XmlNode Strength = playerData.CreateElement(“Strength“);
    Strength.AppendChild(playerData.CreateTextNode(p.Strength.ToString()));
    stats.AppendChild(Strength);

    XmlNode Speed = playerData.CreateElement(“Speed“);
    Speed.AppendChild(playerData.CreateTextNode(p.Speed.ToString()));
    stats.AppendChild(Speed);

    XmlNode Dexterity = playerData.CreateElement(“Dexterity“);
    Dexterity.AppendChild(playerData.CreateTextNode(p.Dexterity.ToString()));
    stats.AppendChild(Dexterity);

    …………………………

    As you can see these lines are nearly identical excluding the attribute names. My RPG has many stats as well as other information so the code is quite long as I resort to copy-pasting. Is there any way to fix it?

    1. Hi Jim,

      Cleaning repetitive code is definitely a good idea.

      I’ll give you a hint, so you can practice a little “thinking like a programmer”. If you want to see a completed solution, you can look at the Player class from lesson 24, which has refactoring done for this.

      Those repeated lines of code:
      1. Create an XmlNode with the attribute name
      2, Add the attribute’s value to the node’s text
      3. Add the attribute’s XmlNode to the parent XmlNode (playerData)

      You want to create a function that does all that, but takes parameters for the values – so you can use the new function for each attribute. In this case, it needs: the attribute name, the attribute value, and the parent XmlNode. The function will need to do the same thing the current code does, except it will use the parameters it receives.

      It’s easiest to write the function and only use it for one attribute, to start. When it works for that one attribute, change the code for the other attributes. This way, if you have a problem, there is a very small area to look (only the code that has changed).

      Please let me know if you have any questions about this.

  10. + $exception
    {System.FormatException: Input string was not in a correct format. at System.Number.StringToNumber(String str, NumberStyles options, NumberBuffer& number, NumberFormatInfo info, Boolean parseDecimal)
    at System.Number.ParseInt32(String s, NumberStyles style, NumberFormatInfo info)
    at System.Convert.ToInt32(String value)
    at Engine.Player.CreatePlayerFromXmlString(String xmlPlayerData) in f:\Development\Projects\SuperAdventure\Engine\Player.cs:line 71} System.Exception {System.FormatException}

    I used your above breakpoint recommendation, since I was able to establish for both my girlfriend’s and my own project that my code will write, but not read; however the above code is what i got. Referencing the line I have pointed to

    foreach(XmlNode node in playerData.SelectNodes(“/Player/PlayerQuests/PlayerQuest”))
    {
    This is the broken line >> int id = Convert.ToInt32(node.Attributes[“ID”].Value);
    bool isCompleted = Convert.ToBoolean(node.Attributes[“IsCompleted”].Value);

    PlayerQuest playerQuest = new PlayerQuest(World.QuestByID(id));
    playerQuest.IsCompleted = isCompleted;

    player.Quests.Add(playerQuest);
    }

    My code is identical to yours, so it seems like it should be converting properly but… It clearly isn’t. Since the issue is the same from both of us, without having copied each others code, I a worried the issue is larger than I’m understanding

    1. To add to my confusion… Provided I do not move more than one ‘location’ away from home (I can stay at home, move to town square, or move to Blacksmith, which I created directly to the south of home) data is saved, but the second I reach the alchemists garden, or farm house the next time I try and load SuperAdventure the above comments exception is triggered

    2. Hi Josh,

      I’ll tell you some more debugging steps. If they don’t help, can you upload your solution (and the files in the folders underneath it – including the PlayerData.xml file) to GitHub or Dropbox?

      Now that you know which line the error happens on, we can start to think of what might cause that error.

      The error happens when the program tries to parse the ID attribute value into an integer. So, I would look at the XML file and see if there is an unexpected, or missing, value. For example, is the value in the ID attribute blank/missing, or not a valid number? Or, is the ID attribute missing, or misnamed. Because XML is case-sensitive, an attribute of “Id” (with a lower-case “d”) would be considered a different attribute from “ID” (with an upper-case attribute). If any of those is the problem, then the Player.ToXmlString() function might need to be fixed.

      Please tell me if that helps you find the source of the problem, or if you want me to look at the source code.

      1. Alright I resolved two issues, the first made all my error codes go away, which was even more frustrating, the following bit of script
        XmlAttribute idAttribute = playerData.CreateAttribute(“ID”);
        idAttribute.Value = item.Details.ID.ToString();
        inventoryItem.Attributes.Append(idAttribute);

        XmlAttribute quantityAttribute = playerData.CreateAttribute(“Quantity”);
        quantityAttribute.Value = item.Quantity.ToString();
        inventoryItem.Attributes.Append(quantityAttribute);

        was written in such a way that the quantity, not the ID was being written to the XML, for quantity AND ID.

        After I fixed that I was still having issues with the game breaking if I moved toof ar. It turns out that was because whenever I was accepting a quest I had ALSO copied that section in to override the quest section with the same value for PlayerQuest and IsCompleted in the XML.

        I think I just tried to write out too much code, too late at night and skipped around lines without noticing

        Sorry for bothering you, but you sent me in the exact right direction to figure my issue out, and now hopefully I can work out my girlfriends in the same manner!

        your tutorial has been absolutely invaluable; Once I complete this (with all your bonus goodies to add) I hope I’m able to find more stuff from you, because you rock!

        1. Great! Figuring out how to track down a bug, and fix it, is a major part of programming – unfortunately 🙂

          Let me know if you need help finding the source of the problem in your girlfriend’s code.

          1. LINK REMOVED FOR PRIVACY

            I gave it two days of going over her code after work.She did her camel casing/pascal casing differently than your tutorial, so I’m half tempted to scrap it and just copy my project over to her.

            I did the breakpoint fix in the ‘catch’ method to no avail, and have since removed it. It wasn’t catching any exceptions.

            I pored over her xml, but aside from different casing on variables I don’t see anything that would be pushing out nor grabbing the wrong data.

            as it stands her game boots up and runs just fine, saves data at shutdown, but always loads default values. I threw a rtbmessage on her load if/else to see if it was going to default that way, but it ‘says’ it’s loading her playerdata xml.

          2. There are two things that need to be fixed in the CreatePlayerFromXmlString function.

            First, after getting the currentLocationID value, it needs to be set on the Player object. So, after line 66, in the Player class, add this line:
            player.CurrentLocation = World.LocationByID(currentLocationID);

            Second, on line 68, the foreach is currently trying to loop through the child nodes of “/Player/Stats/CurrentLocation”. This is the foreach for the player’s inventory items, which needs to use the XML node “/Player/InventoryItems/InventoryItem”.

            Because of the problem with the inventory items, you might need to manually add a weapon to her PlayerData.xml file – or add temporarily a line like this to the CreatePlayerFromXmlString function:
            player.AddItemToInventory(World.ItemByID(World.ITEM_ID_CLUB));
            You could put that after the line that assigns the player.CurrentLocation, run the program, exit the program (so it saves the club in the PlayerData,xml file), then remove the line.

            Please tell me if you have any other questions.

Leave a Reply

Your email address will not be published. Required fields are marked *