Press "Enter" to skip to content

Lesson 07.1: Creating Monsters

In this lesson, we will create the game monsters.

Like most of the other game objects, we will use a factory class to create the monsters. When we create a monster object, we also need to give it random loot, for the player to collect after they defeat the monster.

We are also going to add images of the monsters, to display on the game screen.

Step 1: Create the new class Engine\Models\Monster.cs.

This class inherits from BaseNotificationClass, so it can update the UI when the monster’s CurrentHitPoints property changes.

The class also has an ImageName property, which is the image file we will add for the monster in Step 4.

Inside the constructor, we accept the parameters, and use them to populate the properties – including adding the location of the image file for the monster.

Monster.cs
using System.Collections.ObjectModel;
namespace Engine.Models
{
    public class Monster : BaseNotificationClass
    {
        private int _hitPoints;
        public string Name { get; private set; }
        public string ImageName { get; set; }
        public int MaximumHitPoints { get; private set; }
        public int HitPoints
        {
            get { return _hitPoints; }
            private set
            {
                _hitPoints = value;
                OnPropertyChanged(nameof(HitPoints));
            }
        }
        public int RewardExperiencePoints { get; private set; }
        public int RewardGold { get; private set; }
        public ObservableCollection<ItemQuantity> Inventory { get; set; }
        public Monster(string name, string imageName, 
            int maximumHitPoints, int hitPoints, 
            int rewardExperiencePoints, int rewardGold)
        {
            Name = name;
            ImageName = string.Format("/Engine;component/Images/Monsters/{0}", imageName);
            MaximumHitPoints = maximumHitPoints;
            HitPoints = hitPoints;
            RewardExperiencePoints = rewardExperiencePoints;
            RewardGold = rewardGold;
            Inventory = new ObservableCollection<ItemQuantity>();
        }
    }
}

NOTE: If the monsters do not display in the UI, try changing the ImageName line to this:

ImageName = string.Format("pack://application:,,,/Engine;component/Images/Monsters/{0}", imageName);

In some environments (I don’t know which ones), the program has trouble finding the image resource. There is more information about Pack URIs here.

Step 2: Update Engine\Factories\ItemFactory.cs

We need to create some more items in the game world, to add to the monster’s inventory – for the player to loot from them.

For the monster loot items, I’m using a starting ID of 9001 – just to keep all these items grouped, and easier for us to find.

ItemFactory.cs
using System.Collections.Generic;
using System.Linq;
using Engine.Models;
namespace Engine.Factories
{
    public static class ItemFactory
    {
        private static List<GameItem> _standardGameItems;
        static ItemFactory()
        {
            _standardGameItems = new List<GameItem>();
            _standardGameItems.Add(new Weapon(1001, "Pointy Stick", 1, 1, 2));
            _standardGameItems.Add(new Weapon(1002, "Rusty Sword", 5, 1, 3));
            _standardGameItems.Add(new GameItem(9001, "Snake fang", 1));
            _standardGameItems.Add(new GameItem(9002, "Snakeskin", 2));
            _standardGameItems.Add(new GameItem(9003, "Rat tail", 1));
            _standardGameItems.Add(new GameItem(9004, "Rat fur", 2));
            _standardGameItems.Add(new GameItem(9005, "Spider fang", 1));
            _standardGameItems.Add(new GameItem(9006, "Spider silk", 2));
        }
        public static GameItem CreateGameItem(int itemTypeID)
        {
            GameItem standardItem = _standardGameItems.FirstOrDefault(item => item.ItemTypeID == itemTypeID);
            if(standardItem != null)
            {
                return standardItem.Clone();
            }
            return null;
        }
    }
}

Step 3: Create Engine\RandomNumberGenerator.cs

To make the monster loot random, we need a random number generator. The standard random class is not very random. So, we can add this version, which creates numbers more randomly.

If you don’t need a complex random number generator, or if you’re using a project that does not have access to System.Security.Cryptography, you can use the SimpleNumberBetween function, at the bottom of the class.

NOTE: There is a new way .NET suggest to use to generate random numbers. You should be able to still use this for now. But, in a future lesson, we replace this class with a NuGet package that lets us use dice notation for our random number generation.

RandomNumberGenerator.cs
using System;
using System.Security.Cryptography;
namespace Engine
{
    // This is the more complex version
    public static class RandomNumberGenerator
    {
        private static readonly RNGCryptoServiceProvider _generator = new RNGCryptoServiceProvider();
        public static int NumberBetween(int minimumValue, int maximumValue)
        {
            byte[] randomNumber = new byte[1];
            _generator.GetBytes(randomNumber);
            double asciiValueOfRandomCharacter = Convert.ToDouble(randomNumber[0]);
            // We are using Math.Max, and substracting 0.00000000001,
            // to ensure "multiplier" will always be between 0.0 and .99999999999
            // Otherwise, it's possible for it to be "1", which causes problems in our rounding.
            double multiplier = Math.Max(0, (asciiValueOfRandomCharacter / 255d) - 0.00000000001d);
            // We need to add one to the range, to allow for the rounding done with Math.Floor
            int range = maximumValue - minimumValue + 1;
            double randomValueInRange = Math.Floor(multiplier * range);
            return (int)(minimumValue + randomValueInRange);
        }
        // Simple version, with less randomness.
        //
        // If you want to use this version, 
        // you can delete (or comment out) the NumberBetween function above,
        // and rename this from SimpleNumberBetween to NumberBetween
        private static readonly Random _simpleGenerator = new Random();
        public static int SimpleNumberBetween(int minimumValue, int maximumValue)
        {
            return _simpleGenerator.Next(minimumValue, maximumValue + 1);
        }
    }
}

Step 4: Add monster images to Engine\Images\Monsters folder.

Create the new folder, download the images (right-clicking and saving each image below), and add the image files to the new folder.

Rat.png Snake.png GiantSpider.png

After adding the image files to the solution, right-click on each one, select “Properties”, and set:

Build Action = Resource

Copy to Output Directory = Do not copy

Step 5: Create Engine\Factories\MonsterFactory.cs

Now that we have all the pieces, we can create monster objects.

We are going to use a factory to create a new monster object for each battle. By creating a new monster object each time, each monster will have its own random loot.

We want a new Monster object, each time we call the MonsterFactory – like how we need a new, unique Item object, each time we call the ItemFactory.

In the ItemFactory, we created a list of standard items. When we needed a new Item, the factory found the standard Item and called the Clone function, to create the new, unique object.

For the MonsterFactory, I’m using a “switch” statement, so you can see how that works.

The “switch” line (line 10) looks at the monsterID value that was passed in to GetMonster. Then, it looks for the “case” line with a matching value (lines 12, 20, and 28). When it finds the matching value, it runs the code after it, until it gets to a “return” (which returns a value, and does not run any more code in that function), or a “break” line (which goes to the first line of code after the closing line for the “switch” statement – line 38, here).

On line 36, we have “default”. This is the code that should run if there is no “case” statement with a matching value. For this function, that should never happen – because we are passing in the monsterID value. But, if it does happen, we create an ArgumentException object, with a message about the invalid monsterID.

On line 41, we have a function “AddLootItem”. This function accepts parameters of the monster object, the ID of the loot item, and the percentage change that the monster has that loot item.

To make the monster loot random, this function will pick a random number between 1 and 100. If that number is less than, or equal to, the “percentage” parameter, the item will be added to the monster’s Inventory. If the random number is higher than the “percentage” parameter, the item is not added.

MonsterFactory.cs
using System;
using Engine.Models;
namespace Engine.Factories
{
    public static class MonsterFactory
    {
        public static Monster GetMonster(int monsterID)
        {
            switch (monsterID)
            {
                case 1:
                    Monster snake = 
                        new Monster("Snake", "Snake.png", 4, 4, 5, 1);
                    AddLootItem(snake, 9001, 25);
                    AddLootItem(snake, 9002, 75);
                    return snake;
                case 2:
                    Monster rat = 
                        new Monster("Rat", "Rat.png", 5, 5, 5, 1);
                    AddLootItem(rat, 9003, 25);
                    AddLootItem(rat, 9004, 75);
                    return rat;
                case 3:
                    Monster giantSpider = 
                        new Monster("Giant Spider", "GiantSpider.png", 10, 10, 10, 3);
                    AddLootItem(giantSpider, 9005, 25);
                    AddLootItem(giantSpider, 9006, 75);
                    return giantSpider;
                default:
                    throw new ArgumentException(string.Format("MonsterType '{0}' does not exist", monsterID));
            }
        }
        private static void AddLootItem(Monster monster, int itemID, int percentage)
        {
            if (RandomNumberGenerator.NumberBetween(1, 100) <= percentage)
            {
                monster.Inventory.Add(new ItemQuantity(itemID, 1));
            }
        }
    }
}

In lesson 7.2, we will add monsters to locations, and let the player encounter them, when they move to a location that has a monster. After that, in lesson 7.3, we will add combat – letting the player fight monsters.

NEXT LESSON: Lesson 07.2: Adding Monsters to Locations

PREVIOUS LESSON: Lesson 06.2: Using Quests in the Game

4 Comments

  1. Goregan
    Goregan 2022-02-23

    Hi Scott,

    thank you very much for this cool and motivating tutorial 🙂 it really helps to increase my C# knowledge.

    For this lesson I altered the way loot is added to the monster. I added an optional maximum value of the specific item. The standard will be 1 but in case the item could be in the monsters inventory more than one, you could just add the maximum to the function call and it will generate a random item count. Pretty simple I know, but I am a beginner and small steps help I guess 🙂

    here is the code:

    private static void AddLootItem(Monster monster, int itemID, int percentage, int maxCount = 1)
    {
    if (RandomNumberGenerator.NumberBetween(1, 100) 1)
    {
    maxCount = RandomNumberGenerator.NumberBetween(1, maxCount);
    }
    monster.Inventory.Add(new ItemQuantity(itemID, maxCount));
    }
    }

    What do you think?

    • SOSCSRPG
      SOSCSRPG 2022-02-23

      You’re welcome, Goregan 🙂

      Thank you for sharing your code. The website doesn’t handle code in comments very well (it’s to prevent possible security problems), but a random quantity of the loot items is a cool idea. I might add that as new feature in the future.

    • SOSCSRPG
      SOSCSRPG 2022-08-08

      Correct. The new way I generate random numbers is with the RandomNumberGenerator.GetInt32() function in the System.Security.Cryptography namespace.

      We do change from this class to using a NuGet package in a future lesson.

      I updated the lesson with a note about this. Thanks!

Leave a Reply

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