Lesson 14.5: Move Remaining Game Data to XML Files

Let’s finish converting the factory classes to create objects from XML files.

 

 

 

Lesson Steps

Step 1: Create the new XML files.

In the Engine project, in the GameData folder, create three new XML files: Quests.xml, Recipes.xml, and Traders.xml.

Notice that the Traders have an ID in their XML data. We’re going to use that later.

Set their “Build Action” to “None” and “Copy to Output Directory” to “Copy Always”.

 

Quests.xml

<?xml version="1.0" encoding="utf-8" ?>
<Quests>
  <Quest ID="1" RewardExperiencePoints="25" RewardGold="10">
    <Name><![CDATA[Clear the herb garden]]></Name>
    <Description><![CDATA[Defeat the snakes in the Herbalist's garden.]]></Description>
    <ItemsToComplete>
      <Item ID="9001" Quantity="5"/>
    </ItemsToComplete>
    <RewardItems>
      <Item ID="1002" Quantity="1"/>
    </RewardItems>
  </Quest>
</Quests>

 

Recipes.xml

<?xml version="1.0" encoding="utf-8" ?>
<Recipes>
  <Recipe ID="1">
    <Name><![CDATA[Granola bar]]></Name>
    <Ingredients>
      <Item ID="3001" Quantity="1"/>
      <Item ID="3002" Quantity="1"/>
      <Item ID="3003" Quantity="1"/>
    </Ingredients>
    <OutputItems>
      <Item ID="2001" Quantity="1"/>
    </OutputItems>
  </Recipe>
</Recipes>

 

Traders.xml

<?xml version="1.0" encoding="utf-8" ?>
<Traders>
  <Trader ID="1">
    <Name><![CDATA[Susan]]></Name>
    <InventoryItems>
      <Item ID="1001" Quantity="1"/>
    </InventoryItems>
  </Trader>
  <Trader ID="2">
    <Name><![CDATA[Farmer Ted]]></Name>
    <InventoryItems>
      <Item ID="1001" Quantity="1"/>
    </InventoryItems>
  </Trader>
  <Trader ID="3">
    <Name><![CDATA[Pete the Herbalist]]></Name>
    <InventoryItems>
      <Item ID="1001" Quantity="1"/>
    </InventoryItems>
  </Trader>
</Traders>

 

Step 2: Change Engine\Models\Trader.cs

To be consistent with our other classes, I added an ID property to the Trader class. This will let us access the traders by ID, instead of Name – like we do for all our other model objects.

 

Trader.cs

namespace Engine.Models
{
    public class Trader : LivingEntity
    {
        public int ID { get; }

        public Trader(int id, string name) : base(name, 9999, 9999, 9999)
        {
            ID = id;
        }
    }
}

 

Step 3: Modify Engine\Factories\QuestFactory.cs, RecipeFactory.cs, and TraderFactory.cs.

Change these to load from the XML files, like we did with the user factory classes.

 

QuestFactory.cs

using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml;
using Engine.Models;
using Engine.Shared;

namespace Engine.Factories
{
    internal static class QuestFactory
    {
        private const string GAME_DATA_FILENAME = ".\\GameData\\Quests.xml";

        private static readonly List<Quest> _quests = new List<Quest>();

        static QuestFactory()
        {
            if(File.Exists(GAME_DATA_FILENAME))
            {
                XmlDocument data = new XmlDocument();
                data.LoadXml(File.ReadAllText(GAME_DATA_FILENAME));

                LoadQuestsFromNodes(data.SelectNodes("/Quests/Quest"));
            }
            else
            {
                throw new FileNotFoundException($"Missing data file: {GAME_DATA_FILENAME}");
            }
        }

        private static void LoadQuestsFromNodes(XmlNodeList nodes)
        {
            foreach(XmlNode node in nodes)
            {
                // Declare the items need to complete the quest, and its reward items
                List<ItemQuantity> itemsToComplete = new List<ItemQuantity>();
                List<ItemQuantity> rewardItems = new List<ItemQuantity>();

                foreach(XmlNode childNode in node.SelectNodes("./ItemsToComplete/Item"))
                {
                    itemsToComplete.Add(new ItemQuantity(childNode.AttributeAsInt("ID"),
                                                         childNode.AttributeAsInt("Quantity")));
                }

                foreach(XmlNode childNode in node.SelectNodes("./RewardItems/Item"))
                {
                    rewardItems.Add(new ItemQuantity(childNode.AttributeAsInt("ID"),
                                                     childNode.AttributeAsInt("Quantity")));
                }

                _quests.Add(new Quest(node.AttributeAsInt("ID"),
                                      node.SelectSingleNode("./Name")?.InnerText ?? "",
                                      node.SelectSingleNode("./Description")?.InnerText ?? "",
                                      itemsToComplete,
                                      node.AttributeAsInt("RewardExperiencePoints"),
                                      node.AttributeAsInt("RewardGold"),
                                      rewardItems));
            }
        }

        internal static Quest GetQuestByID(int id)
        {
            return _quests.FirstOrDefault(quest => quest.ID == id);
        }
    }
}

 

RecipeFactory.cs

using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml;
using Engine.Models;
using Engine.Shared;

namespace Engine.Factories
{
    public static class RecipeFactory
    {
        private const string GAME_DATA_FILENAME = ".\\GameData\\Recipes.xml";

        private static readonly List<Recipe> _recipes = new List<Recipe>();

        static RecipeFactory()
        {
            if(File.Exists(GAME_DATA_FILENAME))
            {
                XmlDocument data = new XmlDocument();
                data.LoadXml(File.ReadAllText(GAME_DATA_FILENAME));

                LoadRecipesFromNodes(data.SelectNodes("/Recipes/Recipe"));
            }
            else
            {
                throw new FileNotFoundException($"Missing data file: {GAME_DATA_FILENAME}");
            }
        }

        private static void LoadRecipesFromNodes(XmlNodeList nodes)
        {
            foreach(XmlNode node in nodes)
            {
                Recipe recipe =
                    new Recipe(node.AttributeAsInt("ID"),
                               node.SelectSingleNode("./Name")?.InnerText ?? "");

                foreach(XmlNode childNode in node.SelectNodes("./Ingredients/Item"))
                {
                    recipe.AddIngredient(childNode.AttributeAsInt("ID"),
                                         childNode.AttributeAsInt("Quantity"));
                }

                foreach(XmlNode childNode in node.SelectNodes("./OutputItems/Item"))
                {
                    recipe.AddOutputItem(childNode.AttributeAsInt("ID"),
                                         childNode.AttributeAsInt("Quantity"));
                }

                _recipes.Add(recipe);
            }
        }

        public static Recipe RecipeByID(int id)
        {
            return _recipes.FirstOrDefault(x => x.ID == id);
        }
    }
}

 

TraderFactory.cs

using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml;
using Engine.Models;
using Engine.Shared;

namespace Engine.Factories
{
    public static class TraderFactory
    {
        private const string GAME_DATA_FILENAME = ".\\GameData\\Traders.xml";

        private static readonly List<Trader> _traders = new List<Trader>();

        static TraderFactory()
        {
            if(File.Exists(GAME_DATA_FILENAME))
            {
                XmlDocument data = new XmlDocument();
                data.LoadXml(File.ReadAllText(GAME_DATA_FILENAME));

                LoadTradersFromNodes(data.SelectNodes("/Traders/Trader"));
            }
            else
            {
                throw new FileNotFoundException($"Missing data file: {GAME_DATA_FILENAME}");
            }
        }

        private static void LoadTradersFromNodes(XmlNodeList nodes)
        {
            foreach(XmlNode node in nodes)
            {
                Trader trader =
                    new Trader(node.AttributeAsInt("ID"),
                               node.SelectSingleNode("./Name")?.InnerText ?? "");

                foreach(XmlNode childNode in node.SelectNodes("./InventoryItems/Item"))
                {
                    int quantity = childNode.AttributeAsInt("Quantity");

                    // Create a new GameItem object for each item we add.
                    // This is to allow for unique items, like swords with enchantments.
                    for(int i = 0; i < quantity; i++)
                    {
                        trader.AddItemToInventory(ItemFactory.CreateGameItem(childNode.AttributeAsInt("ID")));
                    }
                }

                _traders.Add(trader);
            }
        }

        public static Trader GetTraderByID(int id)
        {
            return _traders.FirstOrDefault(t => t.ID == id);
        }
    }
}

 

Step 3: Modify Engine\GameData\Locations.xml

For locations that have a Trader, change the Trader node to have an ID attribute, instead of using a Name attribute – since the TraderFactory now gets Traders by ID.

Change lines 11, 18, and 37 to store the Trader’s ID, instead of their Name.

 

Locations.xml

<?xml version="1.0" encoding="utf-8" ?>
<Locations RootImagePath="/Images/Locations/">
  <Location X="-2" Y="-1" Name="Farmer's Field" ImageName="FarmFields.png">
    <Description><![CDATA[There are rows of corn here, with giant rats hiding between them.]]></Description>
    <Monsters>
      <Monster ID="2" Percent="100"/>
    </Monsters>
  </Location>
  <Location X="-1" Y="-1" Name="Farmer's House" ImageName="Farmhouse.png">
    <Description><![CDATA[This is the house of your neighbor, Farmer Ted.]]></Description>
    <Trader ID="2"/>
  </Location>
  <Location X="0" Y="-1" Name="Home" ImageName="Home.png">
    <Description><![CDATA[This is your home.]]></Description>
  </Location>
  <Location X="-1" Y="0" Name="Trading Shop" ImageName="Trader.png">
    <Description><![CDATA[The shop of Susan, the trader.]]></Description>
    <Trader ID="1"/>
  </Location>
  <Location X="0" Y="0" Name="Town Square" ImageName="TownSquare.png">
    <Description><![CDATA[You see a fountain here.]]></Description>
  </Location>
  <Location X="1" Y="0" Name="Town Gate" ImageName="TownGate.png">
    <Description><![CDATA[There is a gate here, protecting the town from giant spiders.]]></Description>
  </Location>
  <Location X="2" Y="0" Name="Spider Forest" ImageName="SpiderForest.png">
    <Description><![CDATA[The trees in this forest are covered with spider webs.]]></Description>
    <Monsters>
      <Monster ID="3" Percent="100"/>
    </Monsters>
  </Location>
  <Location X="0" Y="1" Name="Herbalist's Hut" ImageName="HerbalistsHut.png">
    <Description><![CDATA[You see a small hut, with plants drying from the roof.]]></Description>
    <Quests>
      <Quest ID="1"/>
    </Quests>
    <Trader ID="3"/>
  </Location>
  <Location X="0" Y="2" Name="Herbalist's Garden" ImageName="HerbalistsGarden.png">
    <Description><![CDATA[There are many plants here, with snakes hiding behind them.]]></Description>
    <Monsters>
      <Monster ID="1" Percent="100"/>
    </Monsters>
  </Location>
</Locations>

 

Step 4: Modify Engine\Factories\WorldFactory.cs

Change line 97 to get the location’s Trader with the new ID attribute, and the TraderFactory’s new GetTraderByID function.

 

WorldFactory.cs

using System.IO;
using System.Xml;
using Engine.Models;
using Engine.Shared;

namespace Engine.Factories
{
    internal static class WorldFactory
    {
        private const string GAME_DATA_FILENAME = ".\\GameData\\Locations.xml";

        internal static World CreateWorld()
        {
            World world = new World();

            if(File.Exists(GAME_DATA_FILENAME))
            {
                XmlDocument data = new XmlDocument();
                data.LoadXml(File.ReadAllText(GAME_DATA_FILENAME));

                string rootImagePath =
                    data.SelectSingleNode("/Locations")
                        .AttributeAsString("RootImagePath");

                LoadLocationsFromNodes(world, 
                                       rootImagePath, 
                                       data.SelectNodes("/Locations/Location"));
            }
            else
            {
                throw new FileNotFoundException($"Missing data file: {GAME_DATA_FILENAME}");
            }

            return world;
        }

        private static void LoadLocationsFromNodes(World world, string rootImagePath, XmlNodeList nodes)
        {
            if(nodes == null)
            {
                return;
            }

            foreach(XmlNode node in nodes)
            {
                Location location =
                    new Location(node.AttributeAsInt("X"),
                                 node.AttributeAsInt("Y"),
                                 node.AttributeAsString("Name"),
                                 node.SelectSingleNode("./Description")?.InnerText ?? "",
                                 $".{rootImagePath}{node.AttributeAsString("ImageName")}");

                AddMonsters(location, node.SelectNodes("./Monsters/Monster"));
                AddQuests(location, node.SelectNodes("./Quests/Quest"));
                AddTrader(location, node.SelectSingleNode("./Trader"));

                world.AddLocation(location);
            }
        }

        private static void AddMonsters(Location location, XmlNodeList monsters)
        {
            if(monsters == null)
            {
                return;
            }

            foreach(XmlNode monsterNode in monsters)
            {
                location.AddMonster(monsterNode.AttributeAsInt("ID"),
                                    monsterNode.AttributeAsInt("Percent"));
            }
        }

        private static void AddQuests(Location location, XmlNodeList quests)
        {
            if(quests == null)
            {
                return;
            }

            foreach(XmlNode questNode in quests)
            {
                location.QuestsAvailableHere
                        .Add(QuestFactory.GetQuestByID(questNode.AttributeAsInt("ID")));
            }
        }

        private static void AddTrader(Location location, XmlNode traderHere)
        {
            if(traderHere == null)
            {
                return;
            }

            location.TraderHere =
                TraderFactory.GetTraderByID(traderHere.AttributeAsInt("ID"));
        }
    }
}

 

Return to main page

2 thoughts on “Lesson 14.5: Move Remaining Game Data to XML Files

  1. Hi Scott,

    Thank you for putting the time and work in to bring us such informative tutorials! I have one question about the project now that I have gone through everything.

    What is involved in saving the state of the game? For example, if I were to level up to level 3 and close the game, I would want to be able to open it back up later and still be level 3. Obviously right now, everything resets and goes back to the beginning states and values.

    1. Hi John,

      You’re welcome. In lesson 19.4 of the Windows Form version of this game, I added code to save the Player object’s information to disk, and load it when you restart the game.

      You could add the same type of logic to this game – making whatever changes you need to make to match this version’s Player class. It might be even easier to look at JSON serialization, and maybe use the Newtonsoft.JSON library to do JSON serialization. In lesson 19.4, we manually do XML serialization. If you can have a library automatically do that for you, that’s better.

Leave a Reply

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