Press "Enter" to skip to content

Lesson 12.3: Making the Action class more flexible with an interface

In this lesson, we’ll make changes to let us create different types of actions that we can assign to different items.

Step 1: Create the interface file Engine\Actions\IAction.cs

To create an interface, right-click on the Actions folder in Solution Explorer. Select Add -> New Item -> Visual C# Items -> Code -> Interface. Name the interface IAction.cs (that first letter is an upper-case “i”). Interfaces are not required to start with a capital “I” – but it’s a common practice.

Create an interface in Visual Studio

Currently, the datatype of the GameItem.Action property is “AttackWithWeapon” – our only action class. However, we want to create other actions for different GameItems.

All the action classes will have two things in common:

  1. An Execute function that accepts actor and target parameters
  2. An OnActionPerformed event to report the action’s results
We need a way to put any action object into the GameItem’s Action property. We’ll do this by defining an “interface”.

In this situation, “interface” is not related to a user interface. Instead, it is like a contract, or set of requirements. It says, “all action classes must have these properties, events, and functions”.

We can use this interface as a datatype. We can store any object that implements an interface into a variable/property/parameter that uses the interface as its datatype.

So, instead of having an Action property whose datatype is “AttackWithWeapon”, we’ll set its datatype to “IAction” – the interface that defines what an action class needs to have and do.

On lines 8 and 9, we add the two things that must exist in every action class: the OnActionPerformed event and the Execute function. Notice that they do not have a scope. That’s because everything defined in an interface must be public – at least, in C# interfaces.

IAction.cs
using System;
using Engine.Models;

namespace Engine.Actions
{
    public interface IAction
    {
        event EventHandler<string> OnActionPerformed;
        void Execute(LivingEntity actor, LivingEntity target);
    }
}

Step 2: Modify Engine\Actions\AttackWithWeapon.cs

On line 6, add “: IAction”, to the class. This declares that the AttackWithWeapon class implements all the properties/events/functions listed in the IAction interface – which it already does.

Now, just like with a base class, the datatype of any AttackWithWeapon object is either “AttackWithWeapon” or “IAction”.

AttackWithWeapon.cs
using System;
using Engine.Models;

namespace Engine.Actions
{
    public class AttackWithWeapon : IAction
    {
        private readonly GameItem _weapon;
        private readonly int _maximumDamage;
        private readonly int _minimumDamage;

        public event EventHandler<string> OnActionPerformed;

        public AttackWithWeapon(GameItem weapon, int minimumDamage, int maximumDamage)
        {
            if(weapon.Category != GameItem.ItemCategory.Weapon)
            {
                throw new ArgumentException($"{weapon.Name} is not a weapon");
            }

            if(_minimumDamage < 0)
            {
                throw new ArgumentException("minimumDamage must be 0 or larger");
            }

            if(_maximumDamage < _minimumDamage)
            {
                throw new ArgumentException("maximumDamage must be >= minimumDamage");
            }

            _weapon = weapon;
            _minimumDamage = minimumDamage;
            _maximumDamage = maximumDamage;
        }

        public void Execute(LivingEntity actor, LivingEntity target)
        {
            int damage = RandomNumberGenerator.NumberBetween(_minimumDamage, _maximumDamage);

            if(damage == 0)
            {
                ReportResult($"You missed the {target.Name.ToLower()}.");
            }
            else
            {
                ReportResult($"You hit the {target.Name.ToLower()} for {damage} points.");
                target.TakeDamage(damage);
            }
        }

        private void ReportResult(string result)
        {
            OnActionPerformed?.Invoke(this, result);
        }
    }
}

Step 3: Modify Engine\Models\GameItem.cs

Change the Action property’s datatype to “IAction” (line 18) and change the datatype of the constructor’s “action” parameter to “IAction”.

This means we can pass in any object that implements the IAction interface and store it in the Action property. For weapons, we’ll pass in AttackWithWeapon. When we add healing objects, we’ll create a new Heal action class that implements IAction, and we can pass in a Heal action object.

GameItem.cs
using Engine.Actions;

namespace Engine.Models
{
    public class GameItem
    {
        public enum ItemCategory
        {
            Miscellaneous,
            Weapon
        }

        public ItemCategory Category { get; }
        public int ItemTypeID { get; }
        public string Name { get; }
        public int Price { get; }
        public bool IsUnique { get; }
        public IAction Action { get; set; }

        public GameItem(ItemCategory category, int itemTypeID, string name, int price,
                        bool isUnique = false, IAction action = null)
        {
            Category = category;
            ItemTypeID = itemTypeID;
            Name = name;
            Price = price;
            IsUnique = isUnique;
            Action = action;
        }

        public void PerformAction(LivingEntity actor, LivingEntity target)
        {
            Action?.Execute(actor, target);
        }

        public GameItem Clone()
        {
            return new GameItem(Category, ItemTypeID, Name, Price, IsUnique, Action);
        }
    }
}

Step 4: Run the unit tests and game, to ensure it still works.

Leave a Reply

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