We are going to build the start of an inventory system. To encapsulate the concept of items in our game, we are going to define a GameItem
model class. This class will have the base properties of all GameItems
. Then, we will define a derived class for Weapon
. Weapon
is a specialized item that also has the ability to deal damage.
Defining Game Item
Let’s create a new GameItem.cs file in SimpleRPG.Game.Engine and Models folder.
namespace SimpleRPG.Game.Engine.Models
{
public class GameItem
{
public GameItem(int itemTypeID, string name, int price)
{
ItemTypeID = itemTypeID;
Name = name;
Price = price;
}
public int ItemTypeID { get; set; }
public string Name { get; set; } = string.Empty;
public int Price { get; set; }
public virtual GameItem Clone() =>
new GameItem(ItemTypeID, Name, Price);
}
}
It’s a simple model class with 3 properties and a constructor to set the properties. However, we also create a Clone
method that creates a deep copy of this type. We’re going to use this in the GameItem
factory because players will get multiple instances of items throughout the game.
Next we create the Weapon
class, which inherits from GameItem
(learn more about C# inheritance here). This means that Weapon
has the same properties and methods as GameItem
, but also adds its own constructor and properties.
Let’s add another file to the SimpleRPG.Game.Engine project in the Models folder:
namespace SimpleRPG.Game.Engine.Models
{
public class Weapon : GameItem
{
public Weapon(int itemTypeID, string name, int price, int minDamage, int maxDamage)
: base(itemTypeID, name, price)
{
MinimumDamage = minDamage;
MaximumDamage = maxDamage;
}
public int MinimumDamage { get; set; }
public int MaximumDamage { get; set; }
public override GameItem Clone() =>
new Weapon(ItemTypeID, Name, Price, MinimumDamage, MaximumDamage);
}
}
We add two properties that manage how much damage the weapon can do. And notice the constructor now takes 5 parameters. The first three (shared with the GameItem
class) are used in calling the base constructor. The last two are set to weapon’s properties. This let us extend the GameItem
with more properties which is useful for sharing behavior in base classes.
Also, the Clone
method now clones a Weapon, rather than a GameItem
because we need to account for the additional properties. Notice the override
attribute on the method, that means this Clone
method is called rather than the GameItem.Clone
base method for items of the Weapon
type.
Inheritance is a power and sometimes dangerous concept in object-orient programming languages, like C#. Learn about it and it’s uses, so that you are sure when to apply it, and when it’s being misused.
The GameItem Factory
As we did earlier with the World
object (Lesson 2.9), we are going to create use the Factory Method design pattern to provide a way for create items when they are needed.
In the SimpleRPG.Game.Engine project and Factories folder, create a ItemFactory
class.
using SimpleRPG.Game.Engine.Models;
using System.Collections.Generic;
using System.Linq;
namespace SimpleRPG.Game.Engine.Factories
{
internal static class ItemFactory
{
private static List<GameItem> _standardGameItems = new List<GameItem>
{
new Weapon(1001, "Pointy Stick", 1, 1, 2),
new Weapon(1002, "Rusty Sword", 5, 1, 3)
};
public static GameItem? CreateGameItem(int itemTypeID)
{
var standardItem = _standardGameItems.FirstOrDefault(i => i.ItemTypeID == itemTypeID);
return standardItem?.Clone();
}
}
}
The CreateGameItem
method finds the specified game item by Id and then returns a new instance of that item. If the GameItem
isn’t found in the list, then this method will return a null item. The standardItem?.Clone()
notation means call the Clone
method only if the standardItem
is not null. If it is null, then the method just returns null.
ItemFactory Testability
We made the ItemFactory
internal (as we did with WorldFactory
) because we don’t want to expose the internals of how we create objects to external code. However, we also want to test ItemFactory
to ensure that it works as we expect, but we don’t want to make that class public just to be tested. So to enable our unit tests, we are going to make the internal class visible to our test project using the InternalsVisibleTo
attribute.
In the SimpleRPG.Game.Engine project, we’re going to create the AssemblyInfo.cs file. That file will just have our assembly attributes.
using System.Runtime.CompilerServices;
[assembly:InternalsVisibleTo("SimpleRPG.Game.Engine.Tests")]
The assembly:
part means the attribute applies to the assembly/library, not to a type in the library. InternalsVisibleTo
is an attribute that gives other assemblies access to internal types in this library. And the name we pass into the attribute is the unit test full project name. After building, our test project can now reference the ItemFactory
class.
InternalsVisibleTo
is a dangerous attribute. It allows us to open up our library to the use of internal types and opens up having breaking changes of internal implementation code when we think it is safe to change. But this attribute is very helpful in testing some components. But it should never be used to give non-test libraries access to internal code. If you need to reference an internal type in another library, then that’s usually a sign that the type should be public. So use it to enable testing, and question if you see it used for anything else.
Now we can write ItemFactory tests, like these:
using SimpleRPG.Game.Engine.Factories;
using System;
using System.Collections.Generic;
using System.Text;
using Xunit;
namespace SimpleRPG.Game.Engine.Tests.Factories
{
public class ItemFactoryTests
{
[Fact]
public void CreateGameItem_WithValidItemTypeId()
{
// arrange
// act
var item = ItemFactory.CreateGameItem(1001);
// assert
Assert.NotNull(item);
Assert.Equal(1001, item.ItemTypeID);
Assert.Equal("Pointy Stick", item.Name);
Assert.Equal(1, item.Price);
}
[Fact]
public void CreateGameItem_WithInvalidItemTypeId()
{
// arrange
// act
var item = ItemFactory.CreateGameItem(1);
// assert
Assert.Null(item);
}
}
}
As we wrap up this lesson, we now have a couple of different game item types defined, and an item factory to create instances of items whenever they are needed in the game. Next we will look at the initial player inventory.
One thought on “Lesson 3.1: Game Items and Factory”