Press "Enter" to skip to content

Lesson 14.4: Read Monster data from an XML file

Now that we can display location images from XML data files, we’ll use the same method for the monsters. However, we do have another thing we’ll need to change – the random loot.

We’ll modify the MonsterFactory to work like the ItemFactory class, storing a base monster and instantiating new instances of Monster objects through a CreateMonster(ID) function.

Step 1: Modify image files in Engine\Images\Monsters

Just like we did with the Location image files, we need to change the Monster image files, so they are not project resources.

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

Step 2: Create Engine\Models\ItemPercentage.cs

We’re going to store the monster’s loot table inside the Monster object, instead of having the factory determine the monster’s inventory.

To do this, we’ll use this new class that stores the Item ID and the percentage chance it is in a monster’s inventory.

ItemPercentage.cs
namespace Engine.Models
{
    public class ItemPercentage
    {
        public int ID { get; }
        public int Percentage { get; }
        public ItemPercentage(int id, int percentage)
        {
            ID = id;
            Percentage = percentage;
        }
    }
}

Step 3: Modify Engine\Models\Monster.cs

Previously, we instantiated a new Monster object every time MonsterFactory.GetMonster() was called. Now, we’re going to build a list of “standard” Monster objects, populated from the XML data file, that we can clone whenever we need a new Monster object.

So, we’re moving the loot table information into the Monster class, to make it available when we call the Monster.GetNewInstance() function – which is how we will create a clone of the default Monster. In this function, we also populate the new Monster’s inventory, based on its loot table values.

TThe AddItemToLootTable is what we’ll use to populate the loot table. We could add a new parameter to the constructor whose datatype is List<ItemPercentage>, and pass in a complete loot table. But I usually prefer populating list properties with a separate function. Either way works – this is just my preference.

Monster.cs
using System.Collections.Generic;
using Engine.Factories;
namespace Engine.Models
{
    public class Monster : LivingEntity
    {
        private readonly List<ItemPercentage> _lootTable =
            new List<ItemPercentage>();
        public int ID { get; }
        public string ImageName { get; }
        public int RewardExperiencePoints { get; }
        public Monster(int id, string name, string imageName,
                       int maximumHitPoints,
                       GameItem currentWeapon,
                       int rewardExperiencePoints, int gold) :
            base(name, maximumHitPoints, maximumHitPoints, gold)
        {
            ID = id;
            ImageName = imageName;
            CurrentWeapon = currentWeapon;
            RewardExperiencePoints = rewardExperiencePoints;
        }
        public void AddItemToLootTable(int id, int percentage)
        {
            // Remove the entry from the loot table,
            // if it already contains an entry with this ID
            _lootTable.RemoveAll(ip => ip.ID == id);
            _lootTable.Add(new ItemPercentage(id, percentage));
        }
        public Monster GetNewInstance()
        {
            // "Clone" this monster to a new Monster object
            Monster newMonster =
                new Monster(ID, Name, ImageName, MaximumHitPoints, CurrentWeapon, 
                            RewardExperiencePoints, Gold);
            foreach(ItemPercentage itemPercentage in _lootTable)
            {
                // Clone the loot table - even though we probably won't need it
                newMonster.AddItemToLootTable(itemPercentage.ID, itemPercentage.Percentage);
                // Populate the new monster's inventory, using the loot table
                if(RandomNumberGenerator.NumberBetween(1, 100) <= itemPercentage.Percentage)
                {
                    newMonster.AddItemToInventory(ItemFactory.CreateGameItem(itemPercentage.ID));
                }
            }
            return newMonster;
        }
    }
}

Step 4: Create Engine\GameData\Monsters.xml

This is like the GameItems.xml and Locations.xml files.

In the root “Monster” node, we have the directory path to the monsters’ image files. Then, we have the details for each type of monster in the game.

NOTE: Remember to set this file’s “Copy to Output Directory” to “Copy Always”.

Monsters.xml
<?xml version="1.0" encoding="utf-8" ?>
<Monsters RootImagePath="/Images/Monsters/">
  <Monster ID="1" Name="Snake" MaximumHitPoints="4" WeaponID="1501" RewardXP="5" Gold="1" ImageName="Snake.png">
    <LootItems>
      <LootItem ID="9001" Percentage="25"/>
      <LootItem ID="9002" Percentage="75"/>
    </LootItems>
  </Monster>
  <Monster ID="2" Name="Rat" MaximumHitPoints="5" WeaponID="1502" RewardXP="5" Gold="1" ImageName="Rat.png">
    <LootItems>
      <LootItem ID="9003" Percentage="25"/>
      <LootItem ID="9004" Percentage="75"/>
    </LootItems>
  </Monster>
  <Monster ID="3" Name="Giant Spider" MaximumHitPoints="10" WeaponID="1503" RewardXP="10" Gold="3" ImageName="GiantSpider.png">
    <LootItems>
      <LootItem ID="9005" Percentage="25"/>
      <LootItem ID="9006" Percentage="75"/>
    </LootItems>
  </Monster>
</Monsters>

Step 5: Modify Engine\Factories\MonsterFactory.cs

The changes here are like the changes to the LocationFactory class.

On line 12, we have the path to the Monsters.xml file.

On line 14, we have our list of “base” monsters that we will populate from the XML file and use to create clones for the player to fight.

The LoadMonstersFromNodes function (lines 35-65) is where we build the base monsters from the XML file. Remember that the attribute names are case-sensitive. If you have any errors, double-check that you don’t have a problem like typing “Id”, when it should be “ID”.

MonsterFactory.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 MonsterFactory
    {
        private const string GAME_DATA_FILENAME = ".\\GameData\\Monsters.xml";
        private static readonly List<Monster> _baseMonsters = new List<Monster>();
        static MonsterFactory()
        {
            if(File.Exists(GAME_DATA_FILENAME))
            {
                XmlDocument data = new XmlDocument();
                data.LoadXml(File.ReadAllText(GAME_DATA_FILENAME));
                string rootImagePath =
                    data.SelectSingleNode("/Monsters")
                        .AttributeAsString("RootImagePath");
                LoadMonstersFromNodes(data.SelectNodes("/Monsters/Monster"), rootImagePath);
            }
            else
            {
                throw new FileNotFoundException($"Missing data file: {GAME_DATA_FILENAME}");
            }
        }
        private static void LoadMonstersFromNodes(XmlNodeList nodes, string rootImagePath)
        {
            if(nodes == null)
            {
                return;
            }
            foreach(XmlNode node in nodes)
            {
                Monster monster =
                    new Monster(node.AttributeAsInt("ID"),
                                node.AttributeAsString("Name"),
                                $".{rootImagePath}{node.AttributeAsString("ImageName")}",
                                node.AttributeAsInt("MaximumHitPoints"),
                                ItemFactory.CreateGameItem(node.AttributeAsInt("WeaponID")),
                                node.AttributeAsInt("RewardXP"),
                                node.AttributeAsInt("Gold"));
                XmlNodeList lootItemNodes = node.SelectNodes("./LootItems/LootItem");
                if(lootItemNodes != null)
                {
                    foreach(XmlNode lootItemNode in lootItemNodes)
                    {
                        monster.AddItemToLootTable(lootItemNode.AttributeAsInt("ID"),
                                                   lootItemNode.AttributeAsInt("Percentage"));
                    }
                }
                _baseMonsters.Add(monster);
            }
        }
        public static Monster GetMonster(int id)
        {
            return _baseMonsters.FirstOrDefault(m => m.ID == id)?.GetNewInstance();
        }
    }
}

Step 6: Modify WPFUI\MainWindow.xaml

Finally, we need to add the FileToBitmapConverter to the Image Source for the Monster’s image file on lines 148-149.

MainWindow.xaml (lines 143-149)

                    <Image Grid.Row="1"
                           HorizontalAlignment="Center"
                           VerticalAlignment="Center"
                           Height="125"
                           Width="125"
                           Source="{Binding CurrentMonster.ImageName, 
                                            Converter={StaticResource FileToBitmapConverter}}"/>

NEXT LESSON: Lesson 14.5: Move Remaining Game Data to XML Files

PREVIOUS LESSON: Lesson 14.3: Read World (Location) data from an XML file

2 Comments

  1. Anthony
    Anthony 2023-08-14

    I’m not sure why, but after this lesson the program starts normal, but as soon as I move to a location that has a monster I get an error that says

    System.TypeInitializationException: ‘The type initializer for ‘Engine.Factories.MonsterFactory’ threw an exception.’

    Inner Exception
    FileNotFoundException: Missing data file: .\GameData\Monsters.xml

    This exception was originally thrown at this call stack:
    Engine.Factories.MonsterFactory.MonsterFactory() in MonsterFactory.cs

    this error is always pointing at

    public static Monster GetMonster(int id)
    {
    return _baseMonsters.FirstOrDefault(m => m.ID == id)?.GetNewInstance();
    }(this here)
    }

    • SOSCSRPG
      SOSCSRPG 2023-08-16

      The first thing I’d check is that the file is being copied and that the monsters are being loaded from the file.

      After building the solution, can you find the directory with SuperAdventure.exe and see if the Monsters.xml file is in the directory? If it isn’t, make sure the Monsters.xml file in Solution Explorer has its Build Action set to “Copy Always”.

      If the file is there and you know how to use the debugger, you can try putting a breakpoint on the “return” line and check the contents of “_baseMonsters”. If that variable is empty, then there’s a problem loading the file or parsing its data. You could set a breakpoint on line 18 of MonsterFactory.cs and use F10 to step through the program and see where the problem is.

      Let me know if that doesn’t help you find the problem.

Leave a Reply

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