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.

 

 

 

Lesson Steps

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.

The 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}}"/>

 

Return to main page

2 thoughts on “Lesson 14.4: Read Monster data from an XML file

  1. Hi, Scott.

    We have two cloning functions in GameItem and Monster models, which are GameItem Clone() and Monster GetNewInstance().
    However, their prototypes are created and stored in their factories.

    If factories are responsible for creating/storing prototypes of an object and for returning a clone from the prototype, I think it should be more efficient to manage all relevant codes in the factory.
    Now, factories are just calling the cloning functions of each object instead cloning by itself. In this case, Factory’s work is dependent on cloning codes of each object, while it has all the prototypes inside it.

    Please give your comments.

    1. Hi Charles,

      As we’ve changed the game to build the game objects from an XML file, instead of the hard-coded values in the factories, the factories are doing slightly different work. Now, they’re part factory and part repository for the initial data. When you get to lesson 15.3, you’ll see how we’re making the game more functional. Part of that will be that we stop modifying properties on some objects and start making new objects with the modified values. This is because objects in functional programming are immutable – they don’t change. So, we’re going to have a lot of object creation that’s going to happen outside the factories.

      This also shows why it’s good to keep code small and decoupled. As we change the program, we can start shifting to a different architecture without completely re-writing the solution. The program doesn’t have detailed definitions or requirements, so we’re building the architecture as we progress. The best way I’ve heard it described is that writing a program is like making a 100 mile drive at night. You can’t see all the way to the destination. But, with your headlights on, you can see far enough ahead that you can keep heading to your destination and make any adjustments you need to make on the trip.

      At some point, we’ll probably change the factories to repositories. But, when we do that, I want to have the ability to store the game object information in text files or in a database.

Leave a Reply

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