In this lesson, we’ll start moving the game data to XML files, and change the factories to read from them.
After we do this, if you want to add more locations, monsters, items, quests, and recipes, you will only need to update these data files. You won’t need to modify the source code and rebuild the solution.
There will be three lessons to convert the ItemFactory to use XML data files, because I want to cover three new topics. This lesson will cover reading the XML file and using it to create objects. The next two will cover “generics” and “extension methods”.
Then, there will be a final lesson to convert all the other factories to read data from XML files.
Step 1: Create a new GameData folder in Engine
This is where we’ll add the game data files.
Step 2: Create Engine\GameData\GameItems.xml
To create this file, right-click on the GameData folder and select Add -> New Item… -> Visual C# Items -> Data -> XML File.
Select GameItems.xml in the Solution Explorer and view its properties. Set the value of “Copy to Output Directory” to “Copy Always”.
Now, when you build the program, Visual Studio will create a GameData folder in the same folder as the executable files and copy the GameItems.xml data file into the it.
XML (Extensible Markup Language) is a format for storing (or passing) data.
We’re already using XAML (a specialized form of XML) to define controls in the UI. Our data files will follow a similar style: opening and closing tags, attributes, and the possibility of an element having child elements.
In the Items.xml file (shown below), the first line is the “XML declaration” line. It tells what version of XML is being used, and what character set is being used. I’m using “utf-8” for the encoding. It’s the most-common encoding.
The “GameItems” node/element is the root element – all the other elements in this file are its children, because they are in between the “
GameItem’s three child elements are “Weapons”, “HealingItems”, and “MiscellaneousItems”. Each of those elements has their own child elements – one for each game item object.
We store the item’s property values as attributes. When we get to objects that have List properties (like the Ingredients property in the Recipes class), we will make an element for each recipe, which will have a child element for each ingredient.
Another popular format is JSON (JavaScript Object Notation). If you ever want to use JSON in a .NET program, you will probably want to include the Newtonsoft.Json NuGet package in your project, for its helper functions.
GameItems.xml
<?xml version="1.0" encoding="utf-8" ?>
<GameItems>
<Weapons>
<Weapon ID="1001" Name="Pointy stick" Price="1" MinimumDamage="1" MaximumDamage="2"/>
<Weapon ID="1002" Name="Rusty sword" Price="5" MinimumDamage="1" MaximumDamage="3"/>
<Weapon ID="1501" Name="Snake fang" Price="0" MinimumDamage="0" MaximumDamage="2"/>
<Weapon ID="1502" Name="Rat claw" Price="0" MinimumDamage="0" MaximumDamage="2"/>
<Weapon ID="1503" Name="Spider fang" Price="0" MinimumDamage="0" MaximumDamage="4"/>
</Weapons>
<HealingItems>
<HealingItem ID="2001" Name="Granola bar" Price="5" HitPointsToHeal="2"/>
</HealingItems>
<MiscellaneousItems>
<MiscellaneousItem ID="3001" Name="Oats" Price="1"/>
<MiscellaneousItem ID="3002" Name="Honey" Price="2"/>
<MiscellaneousItem ID="3003" Name="Raisins" Price="2"/>
<MiscellaneousItem ID="9001" Name="Snake fang" Price="1"/>
<MiscellaneousItem ID="9002" Name="Snakeskin" Price="2"/>
<MiscellaneousItem ID="9003" Name="Rat tail" Price="1"/>
<MiscellaneousItem ID="9004" Name="Rat fur" Price="2"/>
<MiscellaneousItem ID="9005" Name="Spider fang" Price="1"/>
<MiscellaneousItem ID="9006" Name="Spider silk" Price="2"/>
</MiscellaneousItems>
</GameItems>
Step 3: Modify Engine\Factories\ItemFactory.cs
Now, we’ll change the ItemFactory class to read from the GameItems.xml file.
We need to add three new “using” statements, since we are using code from new namespaces (for the XML, reading the file, and converting values). Add:
using System;
using System.IO;
using System.Xml;
On line 13 is a constant to hold the data file name. The single “.” means, “start looking for this file from the current directory” (the directory where the program will be running.
We need to double the “\” characters, because a single “\” in a string is normally used to insert special characters, like tabs, carriage returns, and line feeds. This is called an “escape sequence“. By having “\\” C# knows we just want to include the “\” character in the string.
I’ve changed the ItemFactory function from the hard-coded values. Now, it checks if the data file exists. If it does, the function creates an XmlDocument object, reads all the text from the data file, and loads the data into the XmlDocument object. Now we can use the XML functions to retrieve data from the XML elements.
This calls the LoadItemsFromNodes function that accepts a list of XML nodes (elements). The parameter passed into “SelectNodes” is the XPath of the nodes we are looking for in the XmlDocument.
So, “/GameItems/Weapons/Weapon” finds all the nodes named “Weapon” that are children of the node named “Weapons”, which is a child of the node named “GameItems”.
The LoadItemsFromNodes function (lines 44-78) loops through each node in the passed-in parameter (an XmlNodeList object), gets the values from the node’s attributes, and creates an object to add to the _standardGameItems variable.
DetermineItemCatagory (lines 80-91) looks at the Name property of the current node to determine the value for the object’s ItemCategory property.
To read the attribute values, there are three new functions on lines 93-113: GetXmlAttributeAsInt, GetXmlAttributeAsString, and GetXmlAttribute.
GetXmlAttribute looks in the node’s Attributes collection (line 105) for the attribute with the name we’re looking for. If it finds it, it returns the attribute’s Value as a string (line 112).
The two extra functions are to get the values back as either a string or an integer. We’re going to clean this up in the upcoming lesson on generics.
The rest of the code in LoadItemsFromNodes reads values from the node’s attributes and uses them to populate the GameItem object’s properties.
On lines 62-68, we build the AttackWithWeapon action for weapons. On lines 69-74, we build the Heal action for healing items.
ItemFactory.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml;
using Engine.Actions;
using Engine.Models;
namespace Engine.Factories
{
public static class ItemFactory
{
private const string GAME_DATA_FILENAME = ".\\GameData\\GameItems.xml";
private static readonly List<GameItem> _standardGameItems = new List<GameItem>();
static ItemFactory()
{
if(File.Exists(GAME_DATA_FILENAME))
{
XmlDocument data = new XmlDocument();
data.LoadXml(File.ReadAllText(GAME_DATA_FILENAME));
LoadItemsFromNodes(data.SelectNodes("/GameItems/Weapons/Weapon"));
LoadItemsFromNodes(data.SelectNodes("/GameItems/HealingItems/HealingItem"));
LoadItemsFromNodes(data.SelectNodes("/GameItems/MiscellaneousItems/MiscellaneousItem"));
}
else
{
throw new FileNotFoundException($"Missing data file: {GAME_DATA_FILENAME}");
}
}
public static GameItem CreateGameItem(int itemTypeID)
{
return _standardGameItems.FirstOrDefault(item => item.ItemTypeID == itemTypeID)?.Clone();
}
public static string ItemName(int itemTypeID)
{
return _standardGameItems.FirstOrDefault(i => i.ItemTypeID == itemTypeID)?.Name ?? "";
}
private static void LoadItemsFromNodes(XmlNodeList nodes)
{
if(nodes == null)
{
return;
}
foreach(XmlNode node in nodes)
{
GameItem.ItemCategory itemCategory = DetermineItemCategory(node.Name);
GameItem gameItem =
new GameItem(itemCategory,
GetXmlAttributeAsInt(node, "ID"),
GetXmlAttributeAsString(node, "Name"),
GetXmlAttributeAsInt(node, "Price"),
itemCategory == GameItem.ItemCategory.Weapon);
if(itemCategory == GameItem.ItemCategory.Weapon)
{
gameItem.Action =
new AttackWithWeapon(gameItem,
GetXmlAttributeAsInt(node, "MinimumDamage"),
GetXmlAttributeAsInt(node, "MaximumDamage"));
}
else if(itemCategory == GameItem.ItemCategory.Consumable)
{
gameItem.Action =
new Heal(gameItem,
GetXmlAttributeAsInt(node, "HitPointsToHeal"));
}
_standardGameItems.Add(gameItem);
}
}
private static GameItem.ItemCategory DetermineItemCategory(string itemType)
{
switch(itemType)
{
case "Weapon":
return GameItem.ItemCategory.Weapon;
case "HealingItem":
return GameItem.ItemCategory.Consumable;
default:
return GameItem.ItemCategory.Miscellaneous;
}
}
private static int GetXmlAttributeAsInt(XmlNode node, string attributeName)
{
return Convert.ToInt32(GetXmlAttribute(node, attributeName));
}
private static string GetXmlAttributeAsString(XmlNode node, string attributeName)
{
return GetXmlAttribute(node, attributeName);
}
private static string GetXmlAttribute(XmlNode node, string attributeName)
{
XmlAttribute attribute = node.Attributes?[attributeName];
if(attribute == null)
{
throw new ArgumentException($"The attribute '{attributeName}' does not exist");
}
return attribute.Value;
}
}
}
Step 4: Test the game
NEXT LESSON: Lesson 14.2: Creating extension methods
PREVIOUS LESSON: Lesson 13.2: More keyboard actions (and fixes)