Press "Enter" to skip to content

Lesson 14.5: Move Remaining Game Data to XML Files

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

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
<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
<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
<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"));
        }
    }
}

NEXT LESSON: Lesson 15.1: Bug Fixes, Unit Tests, and Tooltips

PREVIOUS LESSON: Lesson 14.4: Read Monster data from an XML file

6 Comments

  1. Steven
    Steven 2021-11-14

    After we create the xml files dose that make it easier to add other items,quests,monsters, and locations?

    • SOSCSRPG
      SOSCSRPG 2021-11-14

      Yes. With the data in XML or JSON files, you can add new locations, monsters, items, quests, recipes, etc. by just modifying those files. You won’t need to make any changes to the program.

      • Steven
        Steven 2021-11-15

        how can u make two monsters spawn in one area?

        • SOSCSRPG
          SOSCSRPG 2021-11-15

          Hi Steven,

          In the “Monster” node, where we have “ID” and “Percent” attributes, you could add an attribute like “NumberOfMonstersToSpawn”. Then, in the code that instantiates the CurrentMonster object (when moving to a new location), you would create that many Monster objects.

          But, you would need to make some other changes. The CurrentMonster property would need to change to a list of Monster objects. And, when the player attacks a monster, you would need to know which monster is being attacked.

  2. Peter
    Peter 2022-02-04

    Hi. can you help me ? i had in my code two errors, one of them is no more…
    but the second one… i cant find it.
    can you try to locate it ? i think that it is in worldFactory, or QuestFactory…
    LINK REMOVED FOR PRIVACY

    • SOSCSRPG
      SOSCSRPG 2022-02-04

      Hello Peter,

      Check the Quests.xml file and make sure all the quests have values for the RewardExperiencePoints and RewardGold. If they don’t have values, the QuestFactory will have an error and stop loading quests.

      Please let me know if that doesn’t help solve the error.

Leave a Reply

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