Code Project: Epic Quest! Unit Testing

A staple of TDD (Test Driven Development), although not quite how I’ve implemented it here (as I already have too much code hanging around prior to writing any tests), the humble Unit Test is something I’ve wanted to dedicate a little bit of time to for a while; so here it is.

Inclusion of Unit Tests in a project, from my own personal experience, is something that rarely takes as much precedence as it should. Sometimes they aren’t included at all; other times they are incredibly slim-line to the point of only scratching the surface – This feels like a bit of a shame. I won’t get into my own feelings too much on the matter beyond saying that I’m of the belief that they certainly have a place and add value.

To this end I’ve gone ahead and created a few simple Unit Tests covering some of the foundation classes of the EpicQuest code base as follows:

  • DungeonTests test class – Covering the EpicDungeon class.
  • RoomTests test class – Covering the Room class (and creation of room content).
  • CombatTests test class – Covering a very basic combat scenario which will be fleshed out further in due course.
  • WeaponTests test class – Covering a basic Weapon object creation scenario.
  • ArmourTests test class – Covering a basic Armour object creation scenario.
  • TreasureTests test class – Currently unimplemented (the concept of ‘treasure’ still needs consideration).

As a bit of a visual aid, here’s a snippet from Visual Studio showing the Unit Test project structure and a Class Diagram illustrating the setup:

Unit Test Project Configuration.

Unit Test Project Configuration.

Unit Test Class Diagram.

Unit Test Class Diagram.

One of key things to notice here is that the EpicQuest project has been referenced in order for us to get hold of the types we need to test. A key benefit I’ve gained since inclusion of the tests is that I’ve been forced to compartmentalise my code further to make testing distinct pieces of functionality possible. Some code has also been re-homed fully (i.e. whereby I had methods sitting within classes whereby it didn’t actually make sense for them to live there from a code structure point of view and, therefore, a Unit Testing perspective). A few difficult questions have also been posed in relation to the accessibility of certain members during creation of the test code; issues I wouldn’t have had to consider without travelling down this path. All in all, pretty positive outcomes so far.

So what’s the anatomy of a basic Unit Test, how is it triggered and how can we glean valuable information from the results?

Within Visual Studio at least, the first requirement is a Unit Test project, as the previous screen shot illustrates (EpicQuest.Tests). From here, a great place to begin is by adding a Basic Unit Test template class:

Add New Unit Test Dialog.

Add New Unit Test Dialog.

This will provide the following stub class adorned with the TestClass attribute with a single test method, suitably adorned with the TestMethod attribute. These attributes identify the object/members as test entities for exposure in the Test Explorer window which we’ll see later.

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace EpicQuest.Tests
{
    [TestClass]
    public class UnitTest1
    {
        [TestMethod]
        public void TestMethod1()
        {

        }
    }
}

Test methods, regardless of the functionality being tested, will normally follow a structure that looks like this:

/// <summary>
/// Test method that ensures that the EpicDungeon
/// instance level RemoveDungeonRoom method is working
/// as expected.
/// </summary>
[TestMethod]
public void EpicDungeonRoomRemovalTest()
{
	//Phase one - Arrange (essentially complete any configuration to actually perform the test - In this case 
	//create a dungeon and add a known room instance to it, taking note of the room count prior to removal of the room)
	Room roomToRemove = new Room();

	EpicDungeon dungeon = new EpicDungeon(EpicDungeon.GenerateRandomRoomConfiguration()); //Opted to create a random room configuration to strengthen the test scenario
	dungeon.AddDungeonRoom(roomToRemove);

	int previousRoomCount = dungeon.DungeonRooms.Count;

	//Phase two - Test/Act (remove the room)
	dungeon.RemoveDungeonRoom(roomToRemove);

	//Phase three - Test/Assert (run tests to ensure that the correct end result, after removal of the room, has been achieved)
	Assert.IsFalse(dungeon.DungeonRooms.Contains(roomToRemove), "EpicDungeonRoomRemovalTest (DungeonTests) failed. Test room roomToRemove still found in dungeon.DungeonRooms.");
	Assert.AreEqual<int>((previousRoomCount - 1), dungeon.DungeonRooms.Count, "EpicDungeonRoomRemovalTest (DungeonTests) failed. Room count (dungeon.DungeonRooms.Count) has an unexpected value.");
}

The structure factors in three distinct phases:

  • Arrange. This stage involves configuring any objects and setup as required by the test you plan to perform.
  • Act. Post configuration you actually need to call the functionality you wish to test.
  • Test. Lastly, assertions are run in order to see if the code under test worked as expected.

The MSDN documentation surrounding running Unit Tests through Visual Studio is well written and quite comprehensive. Further information can be found by accessing the following link:

Walkthrough: Creating and Running Unit Tests for Managed Code.

In order to see if the code under test is working as expected the static members of the Assert class come into play (of which there are a few, see the Assert Class MSDN documentation for a full listing).

In the above listing, I’ve opted to firstly use Assert.IsFalse to make a logical test to see if the Room instance I am expecting to be removed by the RemoveDungeonRoom method has actually be taken out of the EpicDungeon instance. Lastly, I’ve called on a generic version (which is awesome!) of Assert.AreEqual to make a secondary check to ensure the remaining count of Room objects in the EpicDungeon is as I would expect, following the previous Room object removal. The Assert static members allow for value types and objects to be passed in for inspection and, in the instances I’ve seen, for a string message to be specified; to be shown if the test fails. You’ll see other examples of Assert methods in action, such as the Assert.IsInstanceOfType, in later examples.

The Assert class and the members on offer are fairly intuitive right off the bat; you’ll probably only need a few minor peeks at the documentation to get underway if you’re familiar with C#.

NOTE: Some of the xml comments and variable names, etc have been tweaked throughout the code at varying times meaning some of the screen shots you see may fall out of line slightly with the code samples. If something is functionally different then I have gone back and altered the screen shots to align with the code.

As there’s not tonnes of code as of yet in the tests I’ve written here’s the full listing of what I have so far:

Dungeon Tests

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using EpicQuest.Models.Dungeon;
using System.Collections.Generic;

namespace EpicQuest.Tests.Dungeon
{
    /// <summary>
    /// Dungeon entity specific Unit Tests
    /// for the Epic Quest game.
    /// </summary>
    [TestClass]
    public class DungeonTests
    {
        /// <summary>
        /// Test method that ensures that the EpicDungeon
        /// object (static) level GenerateRandomRoomConfiguration method 
        /// is working as expected. 
        /// </summary>
        [TestMethod]
        public void EpicDungeonRoomGenerationTest()
        {
            //Generate a random room amount seed and get an actual number of rooms as generated by EpicDungeon.GenerateRandomRoomConfiguration. Obtain the difference
            int roomNumberSeed = new Random().Next(3, 10), roomNumber = EpicDungeon.GenerateRandomRoomConfiguration(roomNumberSeed).Count,
                difference = (roomNumber - roomNumberSeed);

            //Extra debugging
            System.Diagnostics.Trace.WriteLine(string.Format("EpicDungeonRoomGenerationTest roomNumberSeed {0}; roomNumber: {1}; difference: {2}.", roomNumberSeed, roomNumber, difference));

            //The test is successful if the difference is between 0 and 2 (these are the rules that I've specified)
            if (difference < 0 || difference > 2)
            {
                Assert.Fail("EpicDungeonRoomGenerationTest (DungeonTests) failed. Room difference between seed number and actual, generated number out of range. Difference: {0}", difference);
            }
        }

        /// <summary>
        /// Test method that ensures that the EpicDungeon
        /// object constructor is working
        /// as expected.
        /// </summary>
        [TestMethod]
        public void EpicDungeonCreationTestOne()
        {
            //Get a predefined list of rooms and pass them into an EpicDungeon object constructor
            List<Room> testRooms = new List<Room>();
            testRooms.AddRange(new Room[3] { new Room(), new Room(), new Room() });

            EpicDungeon dungeon = new EpicDungeon(testRooms);

            //Test to ensure that dungeon.DungeonRooms is based on our testRooms collection
            Assert.AreEqual(testRooms, dungeon.DungeonRooms, "EpicDungeonCreationTestOne (DungeonTests) failed. Test room array is not represented by dungeon.DungeonRooms (different refs).");
        }

        /// <summary>
        /// Test method that ensures that the EpicDungeon
        /// object constructor is working
        /// as expected (very basic test).
        /// </summary>
        [TestMethod]
        public void EpicDungeonCreationTestTwoBasic()
        {
            //Test the constructor that takes an array of room objects
            Room[] roomArray = new Room[] { new Room(), new Room(), new Room() };

            EpicDungeon dungeon = new EpicDungeon(roomArray);

            //The room array is converted into a list within the Room class - Do a simple count check as a follow up to EpicDungeonCreationTestOne
            Assert.AreEqual<int>(roomArray.Length, dungeon.DungeonRooms.Count, "EpicDungeonCreationTestTwoBasic (DungeonTests) failed. Test array room count differs to dungeon.DungeonRooms.Count.");
        }

        /// <summary>
        /// Test method that ensures that the EpicDungeon
        /// instance level AddDungeonRoom method is working
        /// as expected.
        /// </summary>
        [TestMethod]
        public void EpicDungeonRoomAdditionTest()
        {
            EpicDungeon dungeon = new EpicDungeon(EpicDungeon.GenerateRandomRoomConfiguration());
            
            int previousRoomCount = dungeon.DungeonRooms.Count;

            Room roomToAdd = new Room();
            dungeon.AddDungeonRoom(roomToAdd);

            Assert.IsTrue(dungeon.DungeonRooms.Contains(roomToAdd), "EpicDungeonRoomAdditionTest (DungeonTests) failed. Test Room roomToAdd not found in dungeon.DungeonRooms.");
            Assert.AreEqual<int>((previousRoomCount + 1), dungeon.DungeonRooms.Count, "EpicDungeonRoomAdditionTest (DungeonTests) failed. dungeon.DungeonRooms.Count has an unexpected value.");
        }

        /// <summary>
        /// Test method that ensures that the EpicDungeon
        /// instance level RemoveDungeonRoom method is working
        /// as expected.
        /// </summary>
        [TestMethod]
        public void EpicDungeonRoomRemovalTest()
        {
            //Phase one - Arrange (essentially complete any configuration to actually perform the test - In this case 
            //create a dungeon and add a known room instance to it, taking note of the room count prior to removal of the room)
            Room roomToRemove = new Room();

            EpicDungeon dungeon = new EpicDungeon(EpicDungeon.GenerateRandomRoomConfiguration()); //Opted to create a random room configuration to strengthen the test scenario
            dungeon.AddDungeonRoom(roomToRemove);

            int previousRoomCount = dungeon.DungeonRooms.Count;

            //Phase two - Test/Act (remove the room)
            dungeon.RemoveDungeonRoom(roomToRemove);

            //Phase three - Test/Assert (run tests to ensure that the correct end result, after removal of the room, has been achieved)
            Assert.IsFalse(dungeon.DungeonRooms.Contains(roomToRemove), "EpicDungeonRoomRemovalTest (DungeonTests) failed. Test room roomToRemove still found in dungeon.DungeonRooms.");
            Assert.AreEqual<int>((previousRoomCount - 1), dungeon.DungeonRooms.Count, "EpicDungeonRoomRemovalTest (DungeonTests) failed. Room count (dungeon.DungeonRooms.Count) has an unexpected value.");
        }
    }
}

Room Tests

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using EpicQuest.Models.Dungeon;
using EpicQuest.Interfaces;
using EpicQuest.Models;
using System.Collections.Generic;
using System.Linq;
using EpicQuest.Models.Items;

namespace EpicQuest.Tests.Rooms
{
    /// <summary>
    /// Room entity specific Unit Tests
    /// for the Epic Quest game.
    /// </summary>
    [TestClass]
    public class RoomTests
    {
        /// <summary>
        /// Test method that ensures that the Room
        /// object (static) level GenerateRandomRoomContent method 
        /// is working as expected. 
        /// </summary>
        [TestMethod]
        public void RoomContentGenerationTest()
        {
            //Test that Room.GenerateRandomRoomContent is producing content based on the seed provided
            int roomContentSeed = 2, roomContentNumber = Room.GenerateRandomRoomContent(roomContentSeed).Count, difference = (roomContentNumber - roomContentSeed);

            //Produce some extra debugging output
            System.Diagnostics.Trace.WriteLine(string.Format("RoomContentGenerationTest (RoomTests) roomContentSeed {0}; roomContentNumber: {1}; difference: {2}.", 
                roomContentSeed, roomContentNumber, difference));

            //Ensure the room content number produced is within the expected range (based on the seed)
            if (difference < 0 || difference > 2)
            {
                Assert.Fail("RoomContentGenerationTest (RoomTests) failed. Room content difference between seed number and actual, generated number out of range. Difference: {0}", difference);
            }
        }

        /// <summary>
        /// Test method that ensures that the Room
        /// object constructor is working as expected.
        /// </summary>
        [TestMethod]
        public void RoomContentCreationTestOne()
        {
            //Add some content to a list of type IRoomContent and pass this to the Room constructor
            List<IRoomContent> roomContentList = new List<IRoomContent>();
            roomContentList.AddRange(new IRoomContent[] { new Skeleton(), new Skeleton(), new Kobold(), new Kobold() });

            Room room = new Room(roomContentList);

            //Check to ensure that the content in the newly created Room object is based on the information passed to the constructor
            Assert.AreEqual(roomContentList, room.RoomFeatures, "RoomContentCreationTestOne (RoomTests) failed. Test room content array is not represented by room.RoomFeatures (different refs).");
        }

        /// <summary>
        /// Test method that ensures that the Room
        /// object constructor is working
        /// as expected (very basic test).
        /// </summary>
        [TestMethod]
        public void RoomContentCreationTestTwoBasic()
        {
            //Chuck an array of IRoomContent to the Room constructor
            IRoomContent[] roomContent = new IRoomContent[] { new Kobold(), new Skeleton() };

            Room room = new Room(roomContent);

            //Again, similar to the Dungeon test of a similiar nature, do a simple count of content items and ensure it's correct
            Assert.AreEqual<int>(roomContent.Length, room.RoomFeatures.Count, "RoomContentCreationTestTwoBasic (RoomTests) failed. Test array room Content count differs to room.RoomFeatures.Count.");
        }

        /// <summary>
        /// Test method that ensures that the Room
        /// instance level AddContentToRoom method is working
        /// as expected.
        /// </summary>
        [TestMethod]
        public void RoomContentAdditionTest()
        {
            //Create a new room and added random content (as base content). Take a note of the items of content currently in the room
            Room room = new Room(Room.GenerateRandomRoomContent());

            int currentRoomFeatureCount = room.RoomFeatures.Count;

            //Add a new monster to the room
            IRoomContent kobold = new Kobold(); 
            room.AddContentToRoom(kobold);

            //Ensure the monster has correctly been added to the room
            Assert.IsTrue(room.RoomFeatures.Contains(kobold), "RoomContentAdditionTest (RoomTests) failed. Test room feature kobold not found in room.RoomFeatures.");
            Assert.AreEqual<int>((currentRoomFeatureCount + 1), room.RoomFeatures.Count, "RoomContentAdditionTest (RoomTests) failed. Room feature count (room.RoomFeatures.Count) has an unexpected value.");
        }

        /// <summary>
        /// Test method that ensures that the Room
        /// instance level RemoveContentFromRoom method is working
        /// as expected. 
        /// </summary>
        [TestMethod]
        public void RoomContentRemovalTest()
        {
            //Add a new skeleton to a new room (with some other random content to strengthen the test example). Take a note of the count of room features prior to calling RemoveContentFromRoom
            IRoomContent skeleton = new Skeleton();

            Room room = new Room(Room.GenerateRandomRoomContent());
            room.AddContentToRoom(skeleton);

            int previousRoomFeatureCount = room.RoomFeatures.Count;

            //Run the test, remove the skeleton from the room
            room.RemoveContentFromRoom(skeleton);

            //Ensure that the known skeleton instance has been removed from the room (and that the count of remaining pieces of room content is correct)
            Assert.IsFalse(room.RoomFeatures.Contains(skeleton), "RoomContentRemovalTest (RoomTests) failed. Test room feature skeleton still found in room.RoomFeatures.");
            Assert.AreEqual<int>((previousRoomFeatureCount - 1), room.RoomFeatures.Count, "RoomContentRemovalTest (RoomTests) failed. Room feature count (room.RoomFeatures.Count) has an unexpected value.");
        }

        /// <summary>
        /// Test method that ensures that the Room
        /// instance level RoomClearedOfMonstersTest method 
        /// is working as expected. 
        /// </summary>
        [TestMethod]
        public void RoomClearedOfMonstersTest()
        {
            //Configure some fixed, known pieces of room content and add it to a new room
            IRoomContent skeleton = new Skeleton(), kobold = new Kobold(), treasureItem = new TreasureItem();

            Room room = new Room(skeleton, kobold, treasureItem);

            //Test that the room contains monsters at this point
            Assert.IsFalse(room.RoomClearedOfMonsters, "RoomClearedOfMonstersTest (RoomTests) failed. No monsters detected in the room.");

            //Remove just the fixed, known monsters - Leaving the treasure item intact
            room.RemoveContentFromRoom(skeleton, kobold);

            //Final round of tests. Ensure all monsters have been removed and that only one piece of content remains (the treasure item)
            Assert.IsTrue(room.RoomClearedOfMonsters, "RoomClearedOfMonstersTest (RoomTests) failed. Monsters detected in the room.");
            Assert.AreEqual<int>(room.RoomFeatures.Count, 1, "RoomClearedOfMonstersTest (RoomTests) failed. An unexpected room feature count was detected (room.RoomFeatures.Count).");
            Assert.IsInstanceOfType(room.RoomFeatures.First(), typeof(TreasureItem), "RoomClearedOfMonstersTest (RoomTests) failed. Remaining room feature is not of the expected type. Type detected: {0}",
                room.RoomFeatures.First().GetType().Name);
        }
    }
}
 

Combat Tests

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using EpicQuest.Interfaces;
using EpicQuest.Models;
using EpicQuest.Utility;

namespace EpicQuest.Tests.Combat
{
    /// <summary>
    /// Combat based entity specific Unit Tests
    /// for the Epic Quest game. 
    /// </summary>
    [TestClass]
    public class CombatTests
    {
        /// <summary>
        /// Test method that ensures that the StaticGameActions
        /// object (static) level HandleCombat method 
        /// is working as expected.  
        /// </summary>
        [TestMethod]
        public void CombatTestOne()
        {
            //Setup the aggressor and defender ICombatant supporting objects
            ICombatant aggressor = new Brawler(), defender = new Skeleton();

            //Handle the combat. Take the defenders previous health and obtain the damage done from StaticGameActions.HandleCombat call
            int defenderPreviousHealth = defender.Health, damageDone = StaticGameActions.HandleCombat(aggressor, defender);

            //Test that the defenders health has been subtracted properly (TODO - Handle dead defenders and make sure this works if a defender is overkilled)
            Assert.AreEqual<int>((defenderPreviousHealth - damageDone), defender.Health);
        }
    }
}

Weapon Tests

using System;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using EpicQuest.Models;
using EpicQuest.Models.Dice;

namespace EpicQuest.Tests.Items
{
    /// <summary>
    /// Weapon entity specific Unit Tests
    /// for the Epic Quest game. 
    /// </summary>
    [TestClass]
    public class WeaponTests
    {
        /// <summary>
        /// Test method that ensures that the Weapon
        /// object constructor is working as expected.
        /// </summary>
        [TestMethod]
        public void WeaponTestBasic()
        {
            //Create a new Weapon
            Weapon weaponItem = new Weapon(GameEnums.HeroOffensiveItemType.LegendaryBastardSword);

            //Ensure that the die associated with the object are as expected
            Assert.IsTrue((weaponItem.Dice.Where(die => die is BlueDie).Count() == 1 && weaponItem.Dice.Where(die => die is RedDie).Count() == 1
                && weaponItem.Dice.Count == 2), "WeaponTestBasic (WeaponTests) failed. Incorrect type of die/die count detected.");
        }
    }
}

Armour Tests

using System;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using EpicQuest.Models;
using EpicQuest.Models.Dice;

namespace EpicQuest.Tests.Items
{
    /// <summary>
    /// Armour entity specific Unit Tests
    /// for the Epic Quest game. 
    /// </summary>
    [TestClass]
    public class ArmourTests
    {
        /// <summary>
        /// Test method that ensures that the Armour
        /// object constructor is working as expected. 
        /// </summary>
        [TestMethod]
        public void ArmourTestBasic()
        {
            //Die property is publically accessible??? TO CHECK; Either way create a new, known armour type
            Armour armourPiece = new Armour() { DefensiveItemType = GameEnums.HeroDefensiveItemType.LeatherChestPiece, DefensiveItemTypeSlot = GameEnums.HeroDefensiveItemSlotType.Chest };

            //Ensure the die for this piece of armour are correct
            Assert.IsTrue((armourPiece.Dice.Where(die => die is GreenDie).Count() == 2 && armourPiece.Dice.Count == 2), "ArmourTestBasic (ArmourTests) failed. Incorrect type of die/die count detected.");
        }
    }
}

Treasure Item Tests

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace EpicQuest.Tests.Items
{
    /// <summary>
    /// TreasureItem entity specific Unit Tests
    /// for the Epic Quest game. 
    /// </summary>
    [TestClass]
    public class TreasureTests
    {
        /// <summary>
        /// TO DO.
        /// </summary>
        [TestMethod]
        public void TreasureTestBasic()
        {
            //TO DO, Treasure not fully implemented
        }     
    }
}

So how do you run these tests? It’s pretty damn easy – You just make use of the Test Explorer window in Visual Studio. A massive plus for me is that Unit Test projects, class templates and the Test Explorer are all available with the express edition of Visual Studio; big win! Either type ‘Test Explorer’ into the Quick Launch search bar in the top right hand corner of the Visual Studio UI or navigate to TEST > Windows > Test Explorer on the Visual Studio menu bar:

Accessing the Test Explorer Window.

Accessing the Test Explorer Window.

The Test Explorer window will automatically detect projects in scope that contain members marked with the TestMethod attribute (the classes of course need the TestClass attribute to). The Test Explorer window allows you to run all tests, previously failed tests, individual tests or a specific playlist of tests. The Test Explorer will appear like this in its initial state:

The Test Explorer Window (Initial State).

The Test Explorer Window (Initial State).

Here’s what the Test Explorer looks like after a successful test run. For starters, you get a run-down of how long each test took to run along with a link taking you to the location of the test method in the source code. The image shows an example of some additional debugging output:

Test Additional Output Example.

Test Additional Output Example.

In order to get hold of extra debugging output you can use the Trace.WriteLine static method as indicated in the following code snippet:

/// <summary>
/// Test method that ensures that the Room
/// object (static) level GenerateRandomRoomContent method 
/// is working as expected. 
/// </summary>
[TestMethod]
public void RoomContentGenerationTest()
{
	//Test that Room.GenerateRandomRoomContent is producing content based on the seed provided
	int roomContentSeed = 2, roomContentNumber = Room.GenerateRandomRoomContent(roomContentSeed).Count, difference = (roomContentNumber - roomContentSeed);

	//Produce some extra debugging output
	System.Diagnostics.Trace.WriteLine(string.Format("RoomContentGenerationTest (RoomTests) roomContentSeed {0}; roomContentNumber: {1}; difference: {2}.", 
		roomContentSeed, roomContentNumber, difference));

	//Ensure the room content number produced is within the expected range (based on the seed)
	if (difference < 0 || difference > 2)
	{
		Assert.Fail("RoomContentGenerationTest (RoomTests) failed. Room content difference between seed number and actual, generated number out of range. Difference: {0}", difference);
	}
}

Let’s rig a failure. I’ve simulated an underlying change to the code base that is going to trip up some of our Unit Tests. We receive the following output in the Test Explorer window after running all tests:

Test Explorer Failure Example.

Test Explorer Failure Example.

Two of our test methods fell over and a quick look at the output suggests that the Room class AddContentToRoom instance level method appears to be functioning unexpectedly (the room feature is not being added to the room). This is the state of the source code as it currently stands:

/// <summary>
/// Public method that allows a piece of content/multiple pieces
/// of content to be added to this particular Room.
/// </summary>
/// <param name="features">A single piece of content/array of content pieces to add to the Room.</param>
public void AddContentToRoom(params IRoomContent[] features)
{
	if (features != null && features.Count() > 0)
	{
		foreach (IRoomContent item in features)
		{
			roomFeatures.Add(new Kobold());
		}
	}
}

Looking through this code we can see that the method accepts an array of type IRoomContent named features. After a null check and a count to ensure the array is not empty we enter a simple foreach statement to add the IRoomContent supporting objects to the roomFeatures data field. The roomFeatures.Add statement is however simply adding a new Kobold object to the roomFeatures field list and ignoring the items in the array being passed in. Bad bug; slap on the wrists for introducing that!!! Let’s alter it to this:

/// <summary>
/// Public method that allows a piece of content/multiple pieces
/// of content to be added to this particular Room.
/// </summary>
/// <param name="features">A single piece of content/array of content pieces to add to the Room.</param>
public void AddContentToRoom(params IRoomContent[] features)
{
	if (features != null && features.Count() > 0)
	{
		foreach (IRoomContent item in features)
		{
			roomFeatures.Add(item);
		}
	}
}

Now we are using the foreach item variable correctly and adding this to the roomFeatures collection.

Back in the Test Explorer we are now able to explicitly re-run just the failed tests as shown:

Failed Tests in the Test Explorer Window.

Failed Tests in the Test Explorer Window.

The failed tests run up and we can see that these tests run without error, happy days!

Passed Tests in the Test Explorer Window.

Passed Tests in the Test Explorer Window.

As for Unit Testing, that’s about it for now. The only other small alteration I’ve made to the existing code base at this time is removal of the HandleCombat method from the GameManager class (to aid in Unit Testing/general code structure). This now resides in a new static class called StaticGameActions.

using System;
using EpicQuest.Interfaces;

namespace EpicQuest.Utility
{
    /// <summary>
    /// Public static utility class for the
    /// EpicDungeon game.
    /// </summary>
    public static class StaticGameActions
    {
        /// <summary>
        /// Private method that shows, in principle, how combat
        /// can be handled between two ICombatant based objects.
        /// </summary>
        /// <param name="aggressor">The instigator of the combat represented by a class implementing ICombatant.</param>
        /// <param name="target">The defender represented by a class implementing ICombatant.</param>
        public static int HandleCombat(ICombatant aggressor, ICombatant target)
        {
            //Prepare values representing the dice (random) and values to keep a tally of the offence, defence and difference totals
            int damageTotal = 0, defenceTotal = 0, difference = 0;
            Random rollDie = new Random();

            //Roll attack dice for the aggressor and get a damage total
            aggressor.AttackDice.ForEach(attackDie =>
            {
                damageTotal += (byte)attackDie.DieFaces[rollDie.Next(0, 5)];
            });

            //Roll defence dice for the defencder and get a defence total
            target.DefenceDice.ForEach(defenceDie =>
            {
                defenceTotal += (byte)defenceDie.DieFaces[rollDie.Next(0, 5)];
            });

            //Determine the difference between the damage/defence totals and deal damage to the target if required
            difference = damageTotal - defenceTotal;

            if (difference > 0)
            {
                target.Health -= difference;
            }

            return difference < 0 ? 0 : difference; //Only return non-negative results
        }
    }
}

That’s about it folks for the time being. Unit Testing is an interesting topic that requires some further investigation so I want to bring you another small piece looking at the TestInitialize and TestCleanup attributes and how more in depth tests can be constructed.

In other news, I’ve upgraded my blog account and got a new domain which I’m sure you’ll notice (splashed some cash as promised!). I’ll shortly be, as stated, making a few styling tweaks to the theme in addition to adding a dedicated Facebook page and Twitter feed, so watch this space.

Ciao for now.

Code Project: Epic Quest! Basic Architecture

This project has been moving along at a nice, steady pace; here’s a run-down of the progress and first round of decisions I’ve made surrounding the games architecture.

I don’t plan on showing every single line of code. Instead, I’ll focus on the core structural work I’ve done and just list out details on a few of the classes that I’ve been working on. I’ll happily make the project available at the end of all this should anyone want to take a full look at the code.

Starting off with the very basics, you’ll remember me saying that I used to love Hero Quest as a kid (and to be honest I think I’d have a better appreciation of the game now; I just used to stand over all of the figures and wind my older brother up!). A core concept of Hero Quest was the attack and defence dice, with the amount of dice being rolled dependant on skills, weapons, augmentations and abilities. Here’s my first implementation of Green (weakest), Blue (middle-of-the-road) and Red (bad-ass) Die classes which hang off an abstract, base Die class.

Current Die Class Hierarchy.

Current Die Class Hierarchy.

As I stated before, I tend to create a fairly fleshed out class hierarchy before deciding whether any fat needs to get trimmed. In this case, I removed the AttackDie and DefenceDie classes and Attack/Defence Die class variants for each colour. Instead, I’ve opted for each dice to support ‘Miss’ (will be renamed to ‘Fail’), ‘SinglePoint’, ‘DoublePoint’, ‘TriplePoint’ and ‘CriticalPoint’ results. The ‘Point’ results will be utilised for determining both attack and defence roll values. The Green Die is the weakest of the three and contains the most ‘Miss’ results and only ‘SinglePoint’ attack/defence results. The Blue Die has only two ‘Miss’ results and supports ‘SinglePoint’ and ‘DoublePoint’ result values. Finally, the Red Die represents the strongest attack and defence clout, having (as per the Blue Die) two ‘Miss’ results and ‘DoublePoint’, ‘TriplePoint’ and ‘CriticalPoint’ results. The various weapons and armour will allow the players to roll various amounts of these dice (monsters, although not using items per say, will simply utilise a fixed amount of dice for attack and defence, unless I implement some kind of ability system for them in addition to the heroes).

Here, as an example, is the current code behind the Die base class and the Red Die (just for illustration purposes; the code for the Green Die/Blue Die is of course very similar due to its affiliation with the Die base class).

using EpicQuest.GameEnums;

namespace EpicQuest.Models
{
    /// <summary>
    /// Base class for all Epic Quest Die objects.
    /// </summary>
    internal abstract class Die
    {
        #region Abstract Members

        /// <summary>
        /// Each Die must implement its own array of
        /// DieFace enum values (i.e. the possible results
        /// supported by the Die in question).
        /// </summary>
        internal abstract DieFace[] DieFaces { get; }

        #endregion Abstract Members
    }
}
using EpicQuest.GameEnums;

namespace EpicQuest.Models.Dice
{
    /// <summary>
    /// Internal Sealed Class representing the strongest 
    /// dice type in Epic Dungeon. This will only be assigned
    /// to the strongest weapons/armour in the game.
    /// </summary>
    internal sealed class RedDie : Die
    {
        #region Private Read Only Data Fields

        //Represents the Red Die 'faces' (aka the possible results from 'rolling' the die)
        private readonly DieFace[] dieFaces;

        #endregion Private Read Only Fields

        #region Overriden Properties

        /// <summary>
        /// Allow access to the die faces (results)
        /// that make up this dice.
        /// </summary>
        internal override DieFace[] DieFaces
        {
            get 
            {
                return dieFaces;
            }
        }

        #endregion Overriden Properties

        #region Constructor

        /// <summary>
        /// RedDie Constructor that configures
        /// the results available on this Die.
        /// </summary>
        internal RedDie()
        {
            dieFaces = new DieFace[6] { DieFace.Miss, DieFace.DoublePoint, DieFace.TriplePoint, DieFace.Miss, DieFace.TriplePoint, DieFace.CriticalPoint };
        }

        #endregion Constructor
    }
}

Before I go any further….

Tangent alert!!! For anyone who doesn’t know what Hero Quest is, or just wants a refresher on how awesome that game was, check out the 1991 commercial (I was 7; imagine how insane this was!).

Shameless plug also for Board James (aka the Angry Video Game Nerd), here’s a link to his Hero Quest episode for kicks.

So, I could comfortably sit here and fill (I mean FILL!) this post with videos about board games, like this, that I’d happily sit and burn hours playing…but I’d quite like for everyone to continue reading these posts, so I’ll rein that urge right back in (for now)! I’m not promising anything though; I could well go full geek again with zero notice.

Dice are now in tow…Good start I hear you say! Well, we need something that will actually use the Dice – To that end I bring you a Weapon and Armour class.

Current Weapon/Armour Class Hierarchy.

Current Weapon/Armour Class Hierarchy.

In the current state, the Weapon class is a little less fleshed-out than Armour class. These classes implement a handful of very lightweight interfaces supporting the following concepts:

  • The object supports a List of Die objects (i.e. we have secured a contract stating any object using the IRollsDice interface will support this kind of member)
  • The object should be treated as a piece of ‘treasure’ in its own right. This is represented by supporting a GoldValue integer property.
  • The Weapon and Armour classes support independent interfaces enforcing support for Weapon/Armour Types (i.e. Item Types and which ‘Slots’ Armour fills).

Below is the full implementation, well the current implementation anyway, of both of these classes.

using EpicQuest.GameEnums;
using EpicQuest.Interfaces;
using EpicQuest.Models.Dice;
using System;
using System.Collections.Generic;
using System.Linq;

namespace EpicQuest.Models
{
    /// <summary>
    /// Internal Class (not yet sealed, not sure of the plans here)
    /// representing a piece of Armour in Epic Quest.
    /// </summary>
    internal class Armour : IHeroArmour, ITreasure
    {
        #region Constant Members

        //Constants that keep a track of the level 'boundaries' for item types (based on the HeroDefensiveItemType underlying byte value).
        //This system does indeed involve some manual maintenance of the enum types/these values but the engine code found in the 
        //GetDefensiveItemMappings method should take care of the rest
        private const int levelOneMax = 6, levelTwoMax = 15, levelThreeMax = 24, levelFourMax = 26, levelFiveMax = 28, levelMax = 5;

        #endregion Constant Members

        #region Private Data Fields

        //A list of 'dice' that get rolled (in defence) for any hero 'wearing' this Armour
        private List<Die> dice = new List<Die>();

        //Enum values that represent the Armour Type and what 'Body Slot' (i.e. Head, Chest) the Armour takes up
        private HeroDefensiveItemType defensiveItemType = HeroDefensiveItemType.None;
        private HeroDefensiveItemSlotType defensiveItemSlotType = HeroDefensiveItemSlotType.None;

        #endregion Private Data Fields

        #region ITreasure Interface Support (Properties)

        /// <summary>
        /// Return the GoldValue of this particular
        /// piece of Armour (currently fixed at 5).
        /// </summary>
        public int GoldValue
        {
            get
            {
                return 5;
            }
        }

        #endregion ITreasure Interface Support (Properties)

        #region IHeroArmour Interface Support (Properties)

        /// <summary>
        /// Provides access to the Armour Type
        /// associated with this particular piece of Armour.
        /// </summary>
        public HeroDefensiveItemType DefensiveItemType
        {
            get
            {
                return defensiveItemType;
            }
            set
            {
                defensiveItemType = value;

                //Reset the Dice associated with this piece of Armour (based on Type)
                Dice = GetDiceBasedOnDefensiveItemType(value);
            }
        }

        /// <summary>
        /// Provides access to the Armour Type Slot
        /// associated with this particular piece of Armour.
        /// </summary>
        public HeroDefensiveItemSlotType DefensiveItemTypeSlot
        {
            get
            {
                return defensiveItemSlotType;
            }
            set
            {
                defensiveItemSlotType = value;
            }
        }

        #endregion IHeroArmour Interface Support (Properties)

        #region IRollsDice Interface Support (Properties)

        /// <summary>
        /// Provides access to the Defence Dice
        /// associated with this particular piece of Armour.
        /// </summary>
        public List<Die> Dice
        {
            get
            {
                return dice;
            }
            set
            {
                dice = value;
            }
        }

        #endregion IRollsDice Interface Support (Properties)

        #region Static Data Fields (Armour Mappings)

        //Private static backing dictionary (for all classes to use) that provides a mapping 'table' on Armour Types to Armour Slots (and the level of these Armour Types)
        private static Dictionary<KeyValuePair<HeroDefensiveItemType, HeroDefensiveItemSlotType>, int> armourMappings;

        #endregion Static Data Fields (Armour Mappings)

        #region Static Properties (Armour Mappings)

        /// <summary>
        /// Allows access to the static Armour Mapping
        /// dictionary so other classes can view Armour Type,
        /// Armour Slot to Level mappings (so Armour can be placed into
        /// the right slots on a Hero/to check the hero is of the
        /// appropriate level to wear a piece of Armour.
        /// </summary>
        internal static Dictionary<KeyValuePair<HeroDefensiveItemType, HeroDefensiveItemSlotType>, int> ArmourMappings
        {
            get
            {
                return armourMappings;
            }
            private set
            {
                armourMappings = value;
            }
        }

        #endregion Static Properties (Armour Mappings)

        #region Static Constructor

        /// <summary>
        /// Static constructor that fires on first use of this class
        /// to setup Armour Types/Slots and Level mappings.
        /// </summary>
        static Armour()
        {
            armourMappings = GetDefensiveItemMappings();
        }

        #endregion Static Constructor

        #region Internal Static Methods
        
        /// <summary>
        /// Internal static method that returns a 'Slot Type'
        /// based on an Armour Type.
        /// </summary>
        /// <param name="itemType">The Armour Type to inspect.</param>
        /// <returns>The Slot Type this piece of Armour belongs to.</returns>
        internal static HeroDefensiveItemSlotType GetSlotTypeBasedOnArmourType(HeroDefensiveItemType itemType)
        {
            switch (itemType)
            {
                case HeroDefensiveItemType.ClothTunic:
                case HeroDefensiveItemType.LeatherChestPiece:
                case HeroDefensiveItemType.NoviceMagicianRobe:
                case HeroDefensiveItemType.NoviceRobeOfSkulls:
                case HeroDefensiveItemType.AdeptMagicianDragonSkinRobe:
                case HeroDefensiveItemType.AdeptRobeOfDemonSkulls:
                case HeroDefensiveItemType.ChainMailChestPiece:
                case HeroDefensiveItemType.DragonSkinChestPiece:
                case HeroDefensiveItemType.MasterworkPlateChestPiece:
                case HeroDefensiveItemType.BloodStainedMithralArmour:
                case HeroDefensiveItemType.BloodStainedMithralRobe:
                    {
                        return HeroDefensiveItemSlotType.Chest;
                    }
                case HeroDefensiveItemType.LeatherHelm:
                case HeroDefensiveItemType.ChainMailHelm:
                case HeroDefensiveItemType.MasterworkPlateHelm:
                    {
                        return HeroDefensiveItemSlotType.Head;
                    }
                case HeroDefensiveItemType.LeatherGloves:
                case HeroDefensiveItemType.ChainMailGloves:
                case HeroDefensiveItemType.MasterworkPlateGloves:
                    {
                        return HeroDefensiveItemSlotType.Hands;
                    }
                case HeroDefensiveItemType.LeatherBoots:
                case HeroDefensiveItemType.ChainMailBoots:
                case HeroDefensiveItemType.MasterworkPlateBoots:
                    {
                        return HeroDefensiveItemSlotType.Feet;
                    }
                case HeroDefensiveItemType.LeatherPants:
                case HeroDefensiveItemType.ChainMailPants:
                case HeroDefensiveItemType.MasterworkPlatePants:
                    {
                        return HeroDefensiveItemSlotType.Legs;
                    }
                case HeroDefensiveItemType.SilverNecklace:
                case HeroDefensiveItemType.GoldNecklace:
                    {
                        return HeroDefensiveItemSlotType.Necklace;
                    }
                case HeroDefensiveItemType.SilverRing:
                case HeroDefensiveItemType.GoldRing:
                    {
                        return HeroDefensiveItemSlotType.Ring;
                    }
                case HeroDefensiveItemType.None:
                default:
                    {
                        return HeroDefensiveItemSlotType.None;
                    }
            }
        }

        /// <summary>
        /// Internal static method that returns the 'Dice' that
        /// can be used based on the type of Armour specified.
        /// </summary>
        /// <param name="itemType">The Armour Type to inspect.</param>
        /// <returns>A List of Dice (Die) objects that can be rolled based on the Armour Type specified.</returns>
        internal static List<Die> GetDiceBasedOnDefensiveItemType(HeroDefensiveItemType itemType)
        {
            List<Die> defensiveDice = new List<Die>();

            switch (itemType)
            {
                default:
                case HeroDefensiveItemType.ClothTunic:
                case HeroDefensiveItemType.LeatherHelm:
                case HeroDefensiveItemType.LeatherGloves:
                case HeroDefensiveItemType.LeatherBoots:
                case HeroDefensiveItemType.LeatherPants:
                case HeroDefensiveItemType.SilverNecklace:
                case HeroDefensiveItemType.SilverRing:
                    {
                        defensiveDice.Add(new GreenDie());
                    }
                    break;
                case HeroDefensiveItemType.LeatherChestPiece:
                case HeroDefensiveItemType.GoldNecklace:
                case HeroDefensiveItemType.GoldRing:
                    {
                        defensiveDice.AddRange(new Die[] { new GreenDie(), new GreenDie() });
                    }
                    break;
            }

            return defensiveDice;
        }

        #endregion Internal Static Methods

        #region Private Static Methods

        /// <summary>
        /// Private static method that configures (and returns a dictionary representing)
        /// Armour Type to Armour Slot mappings (with a value denoting what level a hero
        /// needs to be to wear a piece of Armour).
        /// </summary>
        /// <returns>A dictionary that represents Armour Type to Armour Slot mappings (with level information).</returns>
        private static Dictionary<KeyValuePair<HeroDefensiveItemType, HeroDefensiveItemSlotType>, int> GetDefensiveItemMappings()
        {
            Dictionary<KeyValuePair<HeroDefensiveItemType, HeroDefensiveItemSlotType>, int> mappings = new Dictionary<KeyValuePair<HeroDefensiveItemType, HeroDefensiveItemSlotType>, int>();

            //Temporary try/catch (put in during development as this piece of code evolves)
            try
            {
                //There are currently '5' (or whatever levelMax is set to) levels of Armour Items present in the game - Loop 5 times to capture all Armour Types
                for (int level = 1; level <= levelMax; level++)
                {
                    //Get values in the HeroDefensiveItemType enum and get the ones belong to the current level (based on for loop)
                    Enum.GetValues(typeof(HeroDefensiveItemType)).Cast<HeroDefensiveItemType>().Where(item => GetLevelConditionFilter(level, item)).ToList<HeroDefensiveItemType>().ForEach(item =>
                    {
                        //Store a reference, in the mappings dictionary, to the Armour Type, the Slot Type the Armour belongs to (via the GetSlotTypeBasedOnArmourType method) and the level this
                        //item belongs to (represented by the level variable)
                        HeroDefensiveItemSlotType slotType = GetSlotTypeBasedOnArmourType(item);
                        mappings.Add(new KeyValuePair<HeroDefensiveItemType, HeroDefensiveItemSlotType>(item, slotType), level);   
                    });
                }
            }
            catch (Exception ex)
            {
                //Write errors to the console (to be removed) - No need to interrupt the flow of the game for now (broken mappings = broken game, so the player would need to close anyway)
                Console.WriteLine(ex.Message);
            }

            return mappings;
        }

        /// <summary>
        /// Private static method that returns a boolean determining in the 
        /// Armour Type specified belongs to the current level value passed in.
        /// </summary>
        /// <param name="level">The level to check against (does the Armour specified belong to this level).</param>
        /// <param name="item">The Armour Type to check.</param>
        /// <returns>If the Armour Type belongs to the specified level then true, otherwise false. An Armour Type of HeroDefensiveItemType.None will always result in false.</returns>
        private static bool GetLevelConditionFilter(int level, HeroDefensiveItemType item)
        {
            //When using this filter an Armour Type of None should never be included, return false
            if (item == HeroDefensiveItemType.None)
            {
                return false;
            }

            //Only return true if an Armour Types underlying byte type falls between the level min/max limits specified (by constants in this class)
            switch (level)
            {
                default:
                case 1:
                    {
                        return ((byte)item > 0 && (byte)item <= levelOneMax);
                    }
                case 2:
                    {
                        return ((byte)item > levelOneMax && (byte)item <= levelTwoMax);
                    }
                case 3:
                    {
                        return ((byte)item > levelTwoMax && (byte)item <= levelThreeMax);
                    }
                case 4:
                    {
                        return ((byte)item > levelThreeMax && (byte)item <= levelFourMax);
                    }
                case 5:
                    {
                        return ((byte)item > levelFourMax && (byte)item <= levelFiveMax);
                    }
            }
        }

        #endregion Private Static Methods
    }
}
using EpicQuest.GameEnums;
using EpicQuest.Interfaces;
using EpicQuest.Models.Dice;
using System.Collections.Generic;

namespace EpicQuest.Models
{
    /// <summary>
    /// Internal Class (not yet sealed, not sure of the plans here)
    /// representing a Weapon in Epic Quest.
    /// </summary>
    internal class Weapon : ITreasure, IHeroWeapon
    {
        #region Private Data Fields

        //A list of 'dice' that get rolled (in attack) for any hero 'using' this Weapon
        private List<Die> dice = new List<Die>();

        //An Enum value that represent the Weapon Type being used
        private HeroOffensiveItemType offensiveItemType = HeroOffensiveItemType.Fists;

        #endregion Private Data Fields

        #region IRollsDice Interface Support (Properties)

        /// <summary>
        /// Provides access to the Attack Dice
        /// associated with this particular Weapon.
        /// </summary>
        public List<Die> Dice
        {
            get
            {
                return dice;
            }
            set
            {
                dice = value;
            }
        }

        #endregion IRollsDice Interface Support (Properties)

        #region ITreasure Interface Support (Properties)

        /// <summary>
        /// Return the GoldValue of this particular
        /// Weapon (currently fixed at 5). 
        /// </summary>
        public int GoldValue
        {
            get
            {
                return 5;
            }
        }

        #endregion ITreasure Interface Support (Properties)

        #region IHeroWeapon Interface Support (Properties)

        /// <summary>
        /// Provides access to the Weapon Type
        /// associated with this particular Weapon.
        /// </summary>
        public HeroOffensiveItemType OffensiveItemType
        {
            get 
            {
                return offensiveItemType; 
            }
	        set 
	        {
                offensiveItemType = value;

                //Reset the Dice associated with this Weapon (based on Type)
                Dice = GetDiceBasedOnOffensiveItemType(offensiveItemType);
	        }
        }

        #endregion IHeroWeapon Interface Support (Properties)

        #region Constructor

        /// <summary>
        /// Constructor for the Weapon Class
        /// that just sets the Weapon Type currently.
        /// </summary>
        /// <param name="itemType">The Weapon Type for this particular Weapon.</param>
        public Weapon(HeroOffensiveItemType itemType = HeroOffensiveItemType.Fists)
        {
            OffensiveItemType = itemType;
        }

        #endregion Constructor

        #region Internal Static Methods

        /// <summary>
        /// Internal static method that, based on the Weapon Type
        /// provided, returns the appropriate Attack Dice.
        /// </summary>
        /// <param name="itemType">The Weapon Type to inspect.</param>
        /// <returns>The dice that can be rolled based on the Weapon Type specified.</returns>
        internal static List<Die> GetDiceBasedOnOffensiveItemType(HeroOffensiveItemType itemType)
        {
            List<Die> offensiveDice = new List<Die>();

            switch (itemType)
            {
                default:
                case HeroOffensiveItemType.Fists:
                case HeroOffensiveItemType.RustyDagger:
                case HeroOffensiveItemType.RustyMace:
                case HeroOffensiveItemType.RustyShortSword:
                    {
                        offensiveDice.Add(new GreenDie());
                    }
                    break;
                case HeroOffensiveItemType.RustyBastardSword:
                    {
                        offensiveDice.AddRange(new Die[] { new GreenDie(), new GreenDie() });
                    }
                    break;
                case HeroOffensiveItemType.SilverHexDagger:
                case HeroOffensiveItemType.MagicMissile:
                    {
                        offensiveDice.AddRange(new Die[] { new GreenDie(), new BlueDie() });
                    }
                    break;
                case HeroOffensiveItemType.FlameTorch:
                case HeroOffensiveItemType.Freeze:
                case HeroOffensiveItemType.FineBarbedWireMace:
                case HeroOffensiveItemType.JaggedSword:
                    {
                        offensiveDice.AddRange(new Die[] { new GreenDie(), new GreenDie(), new BlueDie() });
                    }
                    break;
                case HeroOffensiveItemType.Disolve:
                case HeroOffensiveItemType.LegendaryBastardSword:
                    {
                        offensiveDice.AddRange(new Die[] { new BlueDie(), new RedDie() });
                    }
                    break;
            }

            return offensiveDice;
        }

        #endregion Internal Static Methods
    }
}

The key point that I want to focus in on at the moment is that each class implements an interface that states that they ‘support’ dice.


#region IRollsDice Interface Support (Properties)

/// <summary>
/// Provides access to the Defence Dice
/// associated with this particular piece of Armour.
/// </summary>
public List<Die> Dice
{
	get
	{
		return dice;
	}
	set
	{
		dice = value;
	}
}

#endregion IRollsDice Interface Support (Properties)

In addition to this, the classes have fully fledged members to determine what combinations of dice are associated with the Weapon and Armour types available (and what slots the Armour Types relate to).

At this point, we have dice to roll to reflect offensive and defensive abilities as well as Weapons and Armour that utilise these dice. Now we have these objects ready for use in our game we really need to code the hero characters to use these items. Below is another starter definition for our character based classes:

Current Character Class Hierarchy.

Current Character Class Hierarchy.

To back this class diagram up, here is a first implementation of the Hero base class and one example of a semi-bulked out Brawler class. Keep in mind this is for pure illustration purposes only; some of this code, particularly the property for defence dice, is a little nightmarish in its current form! However, I’ve resolved to show you things as they evolve and if they are, for want of a better word, a bit ‘crap’ to start with I’d rather show you and just mention it along the way. It essentially boils down to this being an iterative process and some pieces of code will always simply be in play for conceptual purposes (especially at this stage).

using EpicQuest.Interfaces;
using System;
using System.Collections.Generic;

namespace EpicQuest.Models
{
    /// <summary>
    /// Base class for all hero types.
    /// </summary>
    internal abstract class Hero : GameCharacter, ICombatant
    {
        #region Abstract Properties (ICombatant Interface Support)

        /// <summary>
        /// The heros health (overriden for each Hero).
        /// </summary>
        public abstract int Health { get; set; }
        
        /// <summary>
        /// The dice this hero can roll for defence
        /// (based on abilities/armour etc). Must be
        /// overriden for each particular hero.
        /// </summary>
        public abstract List<Die> DefenceDice { get; }
        
        /// <summary>
        /// The dice this hero can roll for offence
        /// (based on weapons/abilities). Must be 
        /// overriden for each particular hero.
        /// </summary>
        public abstract List<Die> AttackDice { get; }

        #endregion Abstract Properties (ICombatant Interface Support)

        #region Abstract Methods

        /// <summary>
        /// Abstract member that must be overriden for each
        /// particular hero (and will refresh the attack/defence
        /// dice state for a particular hero).
        /// </summary>
        public abstract void RefreshDiceState();

        #endregion Abstract Methods
    }
}
using System.Collections.Generic;

namespace EpicQuest.Models
{
    /// <summary>
    /// Implementation of the Brawler hero type.
    /// </summary>
    internal sealed class Brawler : Hero
    {
        #region Private Data Fields

        //The characters starting health value
        private int health = 10;

        //Fields that denote a heroes head and chest slot armour types and a base weapon for this hero to use. This is implemented in its current for for test purposes only
        private Armour headSlot = new Armour() { DefensiveItemType = GameEnums.HeroDefensiveItemType.LeatherHelm, DefensiveItemTypeSlot = GameEnums.HeroDefensiveItemSlotType.Head };
        private Armour chestSlot = new Armour() { DefensiveItemType = GameEnums.HeroDefensiveItemType.ChainMailChestPiece, DefensiveItemTypeSlot = GameEnums.HeroDefensiveItemSlotType.Chest };
        private Weapon heroWeapon = new Weapon(GameEnums.HeroOffensiveItemType.FineBarbedWireMace);

        #endregion Private Data Fields

        #region ICombatant (abstract overrides) Interface Support

        /// <summary>
        /// Access to the characters health (to reduce 
        /// during combat for example).
        /// </summary>
        public override int Health
        {
            get
            {
                return health;
            }
            set
            {
                health = value;
            }
        }

        /// <summary>
        /// Access to this characters defence dice
        /// for rolling during combat.
        /// </summary>
        public override List<Die> DefenceDice
        {
            get
            {
                //THIS IS NOT PERMANENT - FOR DEMONSTRATION PURPOSES ONLY (NIGHTMARISH CODE ALERT - WOULDN'T WANT A NEW LIST EACH TIME)
                List<Die> currentDefenceDice = new List<Die>(headSlot.Dice.Count + chestSlot.Dice.Count);
                currentDefenceDice.AddRange(headSlot.Dice);
                currentDefenceDice.AddRange(chestSlot.Dice);

                return currentDefenceDice;
            }
        }

        /// <summary>
        /// Access to this characters defence dice
        /// for rolling during combat.
        /// </summary>
        public override List<Die> AttackDice
        {
            get
            {
                return heroWeapon.Dice;
            }
        }

        /// <summary>
        /// Overriden method that will inspect this heroes 
        /// state and refresh the dice associated with attack/defence
        /// based on the items in scope.
        /// </summary>
        public override void RefreshDiceState()
        {
            //TODO
        }

        #endregion ICombatant (abstract overrides) Interface Support
    }
}

There are still quite a few details to take care of here, but this very simple class structure enables us to set-up and test a combat scenario, which I’ll take you through in a moment. Before moving on however, I want to illustrate two other parts of the class diagram (although I’m not going to go through the details yet) showing you the enumerations currently written and other miscellaneous parts of the code base. We’ll talk about these areas of the code base in future posts regardless.

Current Enum Structure.

Current Enum Structure.

Current Misc Class Hierarchy.

Current Misc Class Hierarchy.

Finally, as an introduction to the piece of combat code previously mentioned, here is a phase one representation of a Dungeon and Room class. The plan here is to provide a Dungeon that contains a random number of rooms and pieces of content (classes implementing IRoomContent) for the adventurers to interact with. Clearing all of the rooms, by killing monsters and collecting treasure, outlines the win condition for the game.

Current Dungeon/Room Class Hierarchy.

Current Dungeon/Room Class Hierarchy.

using EpicQuest.Interfaces;
using System;
using System.Collections.Generic;
using System.Linq;

namespace EpicQuest.Models.Dungeon
{
    /// <summary>
    /// Internal Sealed class that represents the Rooms
    /// in a Dungeon (filled with monsters and treasure).
    /// </summary>
    internal sealed class Room : IUniqueItem
    {
        #region Private Data Fields

        //A Rooms unique reference (should a room need to be uniquely identified) and a List of Room Content
        private readonly Guid uniqueRef = Guid.NewGuid();
        private List<IRoomContent> roomFeatures = new List<IRoomContent>();

        #endregion Private Data Fields

        #region Properties

        /// <summary>
        /// Access to this Rooms Unique Reference.
        /// </summary>
        public Guid UniqueRef
        {
            get
            {
                return uniqueRef;
            }
        }

        /// <summary>
        /// Helper property that checks to see if any 
        /// remaining Room Content is of type Monster.
        /// </summary>
        internal bool RoomClearedOfMonsters
        {
            get
            {
                if (roomFeatures.Any(feature => feature is Monster))
                {
                    return false;
                }

                return true;
            }
        }

        /// <summary>
        /// Access to a Rooms Content Features.
        /// </summary>
        internal List<IRoomContent> RoomFeatures 
        { 
            get
            {
                return roomFeatures;
            }
            private set
            {
                if (value != null && value.Count > 0)
                {
                    roomFeatures = value;
                }
            }
        }

        #endregion Properties

        #region Constructors

        /// <summary>
        /// Room constructor that allows a Room to be populated
        /// with a single piece of content (or an array of content).
        /// </summary>
        /// <param name="features">The content to place in the Room (single piece or an array of).</param>
        internal Room(params IRoomContent[] features)
            : this (features.ToList())
        {

        }

        /// <summary>
        /// Room constructor that allows a Room to be populated
        /// with a List of content.
        /// </summary>
        /// <param name="features">A list of content to place in the Room.</param>
        internal Room(List<IRoomContent> features)
        {
            if (features != null && features.Count > 0)
            {
                RoomFeatures = features;
            }
        }

        #endregion Constructors

        #region Internal Methods

        /// <summary>
        /// Internal method that allows a piece of content/multiple pieces
        /// of content to be added to this particular Room.
        /// </summary>
        /// <param name="features">A single piece of content/array of content pieces to add to the Room.</param>
        internal void AddContentToRoom(params IRoomContent[] features)
        {
            if (features != null && features.Count() > 0)
            {
                foreach (IRoomContent item in features)
                {
                    roomFeatures.Add(item);
                }
            }
        }

        /// <summary>
        /// Internal method that allows a piece of content/multiple pieces
        /// of content to be removed from this particular Room.
        /// </summary>
        /// <param name="features">>A single piece of content/array of content pieces to removed from the Room.</param>
        internal void RemoveContentFromRoom(params IRoomContent[] features)
        {
            if (features != null && features.Count() > 0)
            {
                foreach (IRoomContent item in features)
                {
                    roomFeatures.Remove(item);
                }
            }
        }

        #endregion Internal Methods

        #region Enumerator

        /// <summary>
        /// Enumerator that allows external classes
        /// to more easily iterate over the Room Features in this
        /// Room (depending on a developers preferences).
        /// </summary>
        /// <returns>The RoomFeatures Enumerator.</returns>
        public List<IRoomContent>.Enumerator GetEnumerator()
        {
            return RoomFeatures.GetEnumerator();
        }

        #endregion Enumerator

        #region Internal Static Methods

        /// <summary>
        /// Internal static helper method that generates and returns a random
        /// List of Room Content.
        /// </summary>
        /// <param name="contentAmountSeed">A seed to help determine the amount of pieces to generate.</param>
        /// <returns>A random List of Room Content.</returns>
        internal static List<IRoomContent> GenerateRandomRoomContent(int contentAmountSeed)
        {
            //CURRENTLY HARD CODED TO RETURN TWO MONSTERS AS CONTENT (COMBAT TEST)
            return new List<IRoomContent>(new IRoomContent[] { new Kobold(), new Skeleton() });
        }

        #endregion Internal Static Methods
    }
}
using System;
using System.Collections.Generic;
using System.Linq;

namespace EpicQuest.Models.Dungeon
{
    /// <summary>
    /// Class that represents a dungeon that adventurers
    /// can 'quest' through with rooms containing things
    /// for the heroes to contend with/collect and interact with.
    /// </summary>
    internal sealed class EpicDungeon
    {
        #region Private Data Fields

        //Represents the rooms that make this dungeon up
        private List<Room> dungeonRooms = new List<Room>();

        /// <summary>
        /// Access to the Rooms in this Dungeon
        /// (to outside classes).
        /// </summary>
        internal List<Room> DungeonRooms
        {
            get
            {
                return dungeonRooms;
            }
            private set
            {
                dungeonRooms = value;
            }
        }

        #endregion Private Data Fields

        #region Constructors

        /// <summary>
        /// Constructor that allows a Dungeon to be created
        /// based on an array of Room or comma separated 
        /// Room objects (individual instances).
        /// </summary>
        /// <param name="newRooms">Rooms that populate this Dungeon with.</param>
        internal EpicDungeon(params Room[] newRooms)
            : this (newRooms.ToList())
        {

        }

        /// <summary>
        /// Constructor that populates this Dungeon with 
        /// the supplied List or Rooms.
        /// </summary>
        /// <param name="newRooms">A List of Rooms to populate this Dungeon with.</param>
        internal EpicDungeon(List<Room> newRooms)
        {
            if (newRooms != null && newRooms.Count > 0)
            {
                DungeonRooms = newRooms;    
            }
        }

        #endregion Constructors

        #region Internal Methods

        /// <summary>
        /// Internal method that allows a Room/Rooms to
        /// be added to this Dungeon.
        /// </summary>
        /// <param name="rooms">A single Room or an array of Rooms to add.</param>
        internal void AddDungeonRoom(params Room[] rooms)
        {
            if (rooms != null && rooms.Count() > 0)
            {
                foreach (Room room in rooms)
                {
                    DungeonRooms.Add(room);
                }
            }
        }

        /// <summary>
        /// Internal method that allows a Room/Rooms to
        /// be removed from this Dungeon.
        /// </summary>
        /// <param name="rooms">A single Room or an array of Rooms to remove.</param>
        internal void RemoveDungeonRoom(params Room[] rooms)
        {
            if (rooms != null && rooms.Count() > 0)
            {
                foreach (Room room in rooms)
                {
                    DungeonRooms.Remove(room);
                }
            }
        }

        #endregion Internal Methods

        #region Enumerator

        /// <summary>
        /// Enumerator that allows external classes
        /// to more easily iterate over the Rooms in this
        /// Dungeon (depending on a developers preferences).
        /// </summary>
        /// <returns>The DungeonRooms Enumerator.</returns>
        public List<Room>.Enumerator GetEnumerator()
        {
            return DungeonRooms.GetEnumerator();
        }

        #endregion Enumerator

        #region Internal Static Methods

        /// <summary>
        /// Internal static helper method that generates a List
        /// of Room objects (based on the seed values provided)
        /// and Room content.
        /// </summary>
        /// <param name="roomAmountSeed">A seed that helps determine the amount of Rooms.</param>
        /// <param name="roomContentSeed">A seed that helps determine the amount of Room Content (in each Room).</param>
        /// <returns>A List of fully populated Rooms based on the seeds specified.</returns>
        internal static List<Room> GenerateRandomRoomConfiguration(int roomAmountSeed = 5, int roomContentSeed = 3)
        {
            List<Room> dungeonRooms = new List<Room>();

            //Get a Room count based on the seed
            Random randomRoomCountObj = new Random();
            int thisGameRoomCount = randomRoomCountObj.Next(roomAmountSeed, (roomAmountSeed + 2));

            //Generate Rooms and add Random content
            for (int i = 0; i < thisGameRoomCount; i++)
            {
                dungeonRooms.Add(new Room(Room.GenerateRandomRoomContent(roomContentSeed)));
            }

            return dungeonRooms;
        }

        #endregion Internal Static Methods
    }
}

The key thing to note here is that Rooms support content and these content pieces can be any class that supports IRoomContent. Currently, our monsters support this interface so they can exist within Rooms. When a hero encounters a monster within a Room combat will be initiated. Combat based classes will support the ICombatant interface and, as an early conceptual piece, I’ve authored and tested the following method to get a feel for how combat may work between two ICombatant compatible objects. Here we are seeing dice and character classes in action.

/// <summary>
/// Private method that shows, in principle, how combat
/// can be handled between two ICombatant based objects.
/// </summary>
/// <param name="aggressor">The instigator of the combat represented by a class implementing ICombatant.</param>
/// <param name="target">The defender represented by a class implementing ICombatant.</param>
private void HandleCombat(ICombatant aggressor, ICombatant target)
{
    //Prepare values representing the dice roll capabilities (random) and values to keep a tally of the offence, defence and difference totals
    int damageTotal = 0, defenceTotal = 0, difference = 0;
    Random rollDie = new Random();

    //Roll attack dice for the aggressor and get a damage total
    aggressor.AttackDice.ForEach(attackDie =>
        {
            damageTotal += (byte)attackDie.DieFaces[rollDie.Next(0, 5)];
        });

    //Roll defence dice for the defender and get a defence total
    target.DefenceDice.ForEach(defenceDie =>
        {
            defenceTotal += (byte)defenceDie.DieFaces[rollDie.Next(0, 5)];
        });

    //Determine the difference between the damage/defence totals and deal damage to the target if required
    difference = damageTotal - defenceTotal;

    if (difference > 0)
    {
        target.Health -= difference;
    }
}

This has been somewhat of a massive mind dump of information, early concepts and basic implementations. In future posts I’ll be narrowing down my focus on each part of the application and implementing classes more fully as required. At the end of this week I’ll also be tallying up the votes for the ‘which monster to include in the game’ poll; so vote if you haven’t already.

Bye for now.

Code Project: Epic Quest! Starting up…

Howdy all,

As promised (somewhat belatedly!) I’m going to undertake a small coding project that, hopefully anyway, fits around my current commitments and the incredible weight of jobs I need to do before my impending wedding.

The plan, in the long term, will be to revisit Unity 3D as well as to continue the journey and forays into other technologies that I’m less familiar with. For now however, I just fancy (whether the results are good or bad) coding.

To that end I’m going to do a series of posts on creating an RPG/Adventure (classic D&D dice roller) packaged into a simple console application.

The snapshot below shows a set of stub classes, interfaces and other utility files that I’ve mocked up to start development on the project. This is all subject to change of course but I always find it relatively helpful to plan ahead and start drawing up interfaces and base classes upfront. These elements can always be removed later if it is discovered that there is a touch of over-engineering going on, therefore leading to a need to tighten the reins on a project and move to a simpler model. All of the code is presently tucked into the application project; I’ve opted to create no additional class libraries at this time (unless some interesting, reusable code comes out of this that I want to keep under source control for later use).

Epic Quest Solution

Epic Quest Solution

Most of the files are currently empty with the exception of some static utility methods and the following code in the core GameManager class.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using EpicQuest.GameEnums;
using EpicQuest.Models;
using EpicQuest.Utility;

namespace EpicQuest.Manager
{
    /// <summary>
    /// Epic Quest core GameManager class
    /// that handles general game state.
    /// </summary>
    internal class GameManager
    {
        #region Private Data Fields

        //Private List representing the party members involved with this adventure
        private List<Hero> partyMembers = new List<Hero>();

        #endregion Private Data Fields

        #region Main Game Loop Method

        /// <summary>
        /// Main Game Loop 'Start' method
        /// called from the Main entry point method of the 
        /// Program class (for this executable).
        /// </summary>
        internal void RunMainGameLoop()
        {
            //Debug information (method entered)
            Debug.WriteLine(Utils.MethodDebugMsg());

            //Game startup initialisation logic
            GameStartup();

            //Always run the game loop at least once (currently, break when user types EXIT)
            do
            {
                //Start game message
                Console.WriteLine();
                Console.Write("Do you want to venture into the dungeon? Type EXIT and hit ENTER to flee in terror or simply press ENTER to continue: ");

                //Does the user want to quit...
                if (Utils.InterpretUserInput(Console.ReadLine()) == GameAction.ExitGame)
                {
                    break;
                }

                //Clear the current game state (i.e. Party Members)
                ClearGameState();

                //Create a new Party of heroes
                CreateQuestParty();
            } 
            while (true);

            //RunMainGameLoop method exit (confirmed via the output window)
            Debug.WriteLine(Utils.MethodDebugMsg(DebugMsgType.MethodLeft));
        }

        #endregion Main Game Loop Method

        #region Private GameManager Helper Methods

        /// <summary>
        /// Private helper method that contains all of the Epic 
        /// Quest game initialisation logic (on start).
        /// </summary>
        private void GameStartup()
        {
            //Write out a formatted string (in a red text colour) that represents the Games Title
            Console.ForegroundColor = ConsoleColor.Red;
            Console.WriteLine(Utils.WriteGameTitle());

            //Reset the console to it's default colour scheme
            Console.ResetColor();
        }

        /// <summary>
        /// Private helper method that can be called to clear
        /// down the Epic Quest game state.
        /// </summary>
        private void ClearGameState()
        {
            partyMembers.Clear();
        }

        /// <summary>
        /// Private helper method (to be refactored/potentially moved) that
        /// allows the creation of a Party of heroes (currently allows 3 members).
        /// </summary>
        private void CreateQuestParty()
        {
            //Input help information - print to the console
            Console.WriteLine();
            Console.WriteLine("Create your Party (BRW = Brawler, CLR = Cleric, MAG = Mage, NEC = Necromancer, THF = Thief)...");

            //Loop three times and allow the creation of the relevant heroes
            for (int i = 0; i < 3; i++)
            {
                //Create a new hero (based on user selection)
                Console.WriteLine();
                Console.Write("Choose your hero number {0}: ", i);
                Hero newHero = null;

                switch (Utils.InterpretUserInput(Console.ReadLine()))
                {
                    case GameAction.ClericChosen:
                        {
                            newHero = new Cleric();
                        }
                        break;
                    case GameAction.MageChosen:
                        {
                            newHero = new Mage();
                        }
                        break;
                    case GameAction.NecromancerChosen:
                        {
                            newHero = new Necromancer();
                        }
                        break;
                    case GameAction.ThiefChosen:
                        {
                            newHero = new Thief();
                        }
                        break;
                    case GameAction.BrawlerChosen:
                    default:
                        {
                            newHero = new Brawler();
                        }
                        break;
                }

                //Safety check - Only add the hero if it's set correctly
                if (newHero != null)
                {
                    partyMembers.Add(newHero);
                    Console.WriteLine("{0} added to the Party.", newHero.GetType().Name);
                }
            }
        }

        #endregion Private GameManager Helper Methods
    }
}

Nothing overly ‘classy’ (bad pun, sorry!) here to kick us off. I just wanted a stub class with the main game loop placed in to give us a template. The few take away points here are that all of the hero classes (Brawler, Cleric, Mage, etc) all feed off a, currently very lightweight, ‘Hero’ base class. A simple List of type Hero is serving as a way to keep track of the different adventurers as of this iteration; this is stored within the GameManager which may well change.

A very useful language feature worth mentioning here, which comes in especially handy when programming WPF applications and working with the MVVW design pattern, is the CallerMemberName attribute. In this case I’m using it as a debug mechanism via a call to Utils.MethodDebugMsg. This attribute essentially exposes a very simple way to gain access to the calling method/properties name in a called method (without using the archaic StackFrame class).

/// <summary>
/// Internal static utility method that consumes a string method name and  
/// formats it as a short debug message (denoting when we enter or exit a method).
/// </summary>
/// <param name="msgType">The message type (have we entered or exited a method for example) as a enum. Defaulted to DebugMsgType.MethodEntered.</param>
/// <param name="methodName">The calling method name as a string</param>
/// <returns>A formatted string representing the method called (depending on the debug message type).</returns>
internal static string MethodDebugMsg(DebugMsgType msgType = DebugMsgType.MethodEntered, [CallerMemberName] string methodName = "")
{
    //If a valid method name is not provided simply return an empty string
    if (string.IsNullOrEmpty(methodName))
    {
        return string.Empty;
    }

    //Use the DebugMsgType provided to return a formatted string (Have we entered/exited the given method name). Default = string.Empty
    switch (msgType)
    {
        case DebugMsgType.MethodEntered:
            {
                return string.Format("Entered the {0} method.", methodName);
            }
        case DebugMsgType.MethodLeft:
            {
                return string.Format("Exited {0} method.", methodName);
            }
        default:
            {
                return string.Empty;
            }
    }
}

CallerMemberNameAttribute MSDN Reference

As far as architecture goes this is pretty much it for the time being.

Concept wise, I envisage a game with the following mechanics:

  • 5 Hero classes with different skills and attributes.
  • A ‘Dice’ rolling system supporting 3 different types of die (Green, Blue and Red).
  • The concept of attack and defence dice (anyone remember Hero Quest!? Would like a spin-off of that).
  • Various monsters to take on.
  • Various weapons, armour and items to interact with and the ability to sell and purchase (via a simple vendor system).
  • Basic sounds plugged in to accompany the game play.

Running the application, as it stands (with a few utility methods firing that I haven’t shown here yet), gives the following output in the console window:

Epic Quest Console Window.

Epic Quest Console Window.

You can tell you’re a developer when everything is zero indexed (hero number 0 come on down!), I probably need to change that!

As usual, comments and thoughts welcome; drop me a line if anything crosses your mind. As a little muck around, and as I’m still getting a feel for the various word press features available (the dream being to chuck some money at this later in the year), I’ve included a poll here to allow readers to pick a monster to be included in the game. Push buttons! You know you want to. Cheers all.

Event/Delegate based Notifications Manager in Unity 3D (10 minute prototype!)

Working backwards (a little anyway, what developer worth his salt doesn’t wander from task to task in a strange order after all!), and with a view to getting a new FPS project up and running as rubber-stamped in my last post, I decided to head towards something I could achieve. Namely, a tidier NotificationsManager class that didn’t rely on using Unity’s in-built object messaging functionality (and its heavy dose of reflection) and instead picked a different approach. You’ve got to do something when a heavy dose of insomnia kicks in!

Based on my own thoughts and a 2 minute trawl online this is what I’ve come up with. This definitely falls into the realm of prototype but I think something along this line could certainly have legs. So without further ado here’s a run-down of the sample project and code I’ve just cooked up, most likely with a view to improve upon once a project gets off the ground.

The game built involves collecting spheres to gain points; 50 points and you win! AAA right there I think you’ll agree! We pull the following components together to form the basic structure/functionality of this game:

  • A GameManager Class (the core class handling central game interactions).
  • A NotificationsManager class (the object responsible for handling events).
  • A PlayerController Class (handling player-centric logic/interactions).
  • A SphereScript Class (simply triggers an event to increment the players score on collision).

The Game World is structured like this:

Game World Configuration.

Game World Configuration.

As with the FPS project I created and reviewed in my previous post, this sample will utilise a core GameManager class. Only one instance of this class will exist per game (I’ve taken steps to ensure this will be a singleton object) and it will ultimately control access to the NotificationsManager directly (for other game objects to call) and handle the game win condition, aka collecting enough spheres. As a cheeky extra, which wouldn’t normally live in this class, a small piece of GUI logic has been added to render the player score in the top left hand corner of the screen.

using UnityEngine;

/// <summary>
/// The core test project GameManager.
/// </summary>
[RequireComponent(typeof(NotificationsManager))]        //A GameManager (when added in the Unity Editor) will automatically attach a NotificationsManager script (if one does not exist)
public class GameManager : MonoBehaviour
{
    #region Private Data Fields

    //A reference to the Player Controller object (on the First Person controller)
    private PlayerController playerCont = null;

    //A cheeky reference to a rectangle (to draw a score on the screen in the OnGUI method)
    private Rect screenRect = new Rect(2.0f, 2.0f, 45.0f, 30.0f);

    #endregion Private Data Fields

    #region Private Static Data Fields

    //Our core, singleton, GameManager instance
    private static GameManager gameManagerInstance = null;
    
    //A singleton instance (accessible through this class) of the NotificationsManager
    private static NotificationsManager notifications = null;

    #endregion Private Static Data Fields

    #region Public Static Properties

    /// <summary>
    /// Global access to a singleton GameManager class (for all game
    /// objects to utilise during play).
    /// </summary>
    public static GameManager GameManagerInstance 
    {
        get
        {
            //Create a new GameObject and add a GameManager script to it if it doesn't already exist (allocating this to the gameManagerInstance field)
            if (gameManagerInstance == null)
            {
                gameManagerInstance = new GameObject("GameManager").AddComponent<GameManager>();
            }

            //Return the instance for global use
            return gameManagerInstance;
        }
    }

    /// <summary>
    /// Global access to a singleton NotificationsManager class (for all game
    /// objects to utilise during play).
    /// </summary>
    public static NotificationsManager Notifications 
    {
        get
        {
            //Set the private notifications field to reference the NotificationsManager script on the GameManagerInstance.
            if (notifications == null)
            {
                notifications = GameManagerInstance.GetComponent<NotificationsManager>();
            }

            //Return the instance for global use
            return notifications;
        }
    }

    #endregion Public Static Properties

    #region Awake/Start

    /// <summary>
    /// In Unity horrific things happen when you use constructors. To that end, 
    /// ensure that only one GameManager comes into existence at game start.
    /// </summary>
    void Awake()
    {
        if (gameManagerInstance != null && gameManagerInstance.GetInstanceID() != GetInstanceID())
        {
            DestroyImmediate(gameObject);
        }
        else
        {
            gameManagerInstance = this;
            DontDestroyOnLoad(gameObject);
        }
    }

    /// <summary>
    /// After Awake, when we've establised one instance of this class for global use, ensure 
    /// that this object has a valid reference to a player object (to read out the score to 
    /// game screen). Register for the OnAllSpheresCollected event.
    /// </summary>
    void Start()
    {
        playerCont = GameObject.FindGameObjectWithTag("Player").GetComponent<PlayerController>();

        //As we've registered for the OnAllSpheresCollected event we don't need to check the player score
        //in the Update method (although we could as we have a PlayerController reference). When a sphere is touched it's 
        //collider will trigger an event picked up in this class (minimising the amount of processing we have to do each frame)
        Notifications.OnAllSpheresCollected += Notifications_OnAllSpheresCollected;
    }

    #endregion Awake/Start

    #region OnGUI

    /// <summary>
    /// Very rudimentary GUI operation. Write 
    /// the player score to the screen.
    /// </summary>
    void OnGUI()
    {
        if (playerCont != null)
        {
            GUI.Label(screenRect, new GUIContent(playerCont.Score.ToString()));
        }
    }

    #endregion OnGUI

    #region Event Handlers

    /// <summary>
    /// Pick up on the NotificationsManager triggering the 
    /// OnAllSpheresCollected event. In this case, this is the 
    /// win condition for the game (simply close and exit).
    /// </summary>
    /// <param name="sender">The component that triggered the event (PlayerController).</param>
    void Notifications_OnAllSpheresCollected(Component sender)
    {
        //We could interrogate the state of the Component here if we needed to (aka the player controller).

#if UNITY_EDITOR
        UnityEditor.EditorApplication.isPlaying = false;
#else
        Application.Quit();
#endif
    }

    #endregion Event Handlers
}

Beforehand, our NotificationsManager maintained a list of strings/components and used the strings (as defined event names) to call the SendMessage method on applicable objects. The GameManager listed above begins to hint at a slightly different approach in this regard. Notice, within the Start method, how an event is being registered (against the NotificationsManager OnAllSpheresCollected event) instead with a handler being exposed within this class; making this class a listener for this event. The Notifications_OnAllSpheresCollected event, which could easily have been an anonymous method (using a lambda expression) has been mocked up to simply end the game.

A NotificationsManager component, as before, is still marked as required and will be added whenever a GameManager script is added to a GameObject within the Unity Editor. Below is an illustration of how the GameManager object is configured in the Unity Editor; showing the scripts added for reference:

Game Manager Configuration.

Game Manager Configuration.

Let’s get into the real beef (not that there is much ‘beef’ in this project, it’s more of a low-fat implementation really!) of the changes made to the NotificationsManager. The tweaked mock-up relies on a single delegate outlining a method that has no return type, which I could expand on later down the line if necessary, and that supports a single argument of type ‘Component’ (most likely another script).

Two public events are then configured to allow objects to register as listeners for the ‘sphere collected’ and ‘all spheres collected’ events. In the first iteration listed here I haven’t given consideration yet to how this should ultimately work (i.e. should these be instance or object level based, aka static – Perhaps even considering the whole class declaration), it’s bare bones. As the NotificationsManager exists as a singleton in the previous project (is instance based), and I only want to prove a concept, having these as non-static and accessible at the instance level works ok for now and allows appropriate access through the GameManager instance.

Finally, two public methods allow objects to ‘post’ notifications to any listening objects, provided the event handler is not null.

using UnityEngine;

/// <summary>
/// Test implementation of a NotificationsManager class
/// using an event/delegate foundation.
/// </summary>
public class NotificationsManager : MonoBehaviour
{
    #region Delegates

    /// <summary>
    /// A delegate representing (outlining) a method that has no return type
    /// and takes a single component as an argument (would have sufficed for the entirety
    /// of the last project I created).
    /// </summary>
    /// <param name="sender">A game component object (a component part of a game object).</param>
    public delegate void ComponentEventHandler (Component sender);

    #endregion Delegates

    #region Component Event Handlers

    /// <summary>
    /// Allows registration for the OnSphereCollected event.
    /// </summary>
    public event ComponentEventHandler OnSphereCollected;

    /// <summary>
    /// Allows registration for the OnAllSpheresCollected
    /// event (essentially our 'win' condition).
    /// </summary>
    public event ComponentEventHandler OnAllSpheresCollected;

    #endregion Component Event Handlers

    #region Public Event Triggers

    /* The meat of the NotificationsManager lies here */

    /// <summary>
    /// Trigger an event on all listeners 
    /// registered with the OnSphereCollected event.
    /// </summary>
    /// <param name="sender">A game component object (a component part of a game object).</param>
    public void SphereCollected(Component sender)
    {
        //Only trigger the event if something is registered
        if (OnSphereCollected != null)
        {
            OnSphereCollected(sender);
        }
    }

    /// <summary>
    /// Trigger an event on all listeners 
    /// registered with the OnAllSpheresCollected event. 
    /// </summary>
    /// <param name="sender">A game component object (a component part of a game object).</param>
    public void AllSpheresCollected(Component sender)
    {
        //Only trigger the event if something is registered
        if (OnAllSpheresCollected != null)
        {
            OnAllSpheresCollected(sender);
        }
    }

    #endregion Public Event Triggers
}

The NotificationsManager is inherently tied to the GameManager and exists on the same GameObject, in the Unity Editor, as the Game Manager.

We’ve started to see the concept of how an event/delegate system could work in Unity for listening/posting objects. The player and sphere objects and associated scripts should hopefully prove the concept in a very basic sense.

Starting with the PlayerController class; the implementation simply allows for a players score to be recorded and the object to be marked as a listener for any object triggering the OnSphereCollected event. This is taken care of by the logic found with the Start initialisation method and the Notifications_OnSphereCollected event handler. The event handler, triggered by a sphere on collision, will just increment the players score by 10 points. If the players score hit 50 points or above then an AllSpheresCollected event can be triggered for all listening objects.

using UnityEngine;

/// <summary>
/// Class representing the player (state, etc).
/// </summary>
public class PlayerController : MonoBehaviour
{
    #region Unity Inspector Public Variables

    /// <summary>
    /// The Players Score. Made public to surface in the 
    /// Unity Editor for fiddling, prevents encapsulation that drives me
    /// a little nuts. Oh well, sigh :o(
    /// </summary>
    public int Score = 0;

    #endregion Unity Inspector Public Variables

    #region Start

    /// <summary>
    /// PlayerController initialistion logic. In this case, simply
    /// register this class as a listener of the OnSphereCollected event
    /// (so we can increment the score and delegate win condition handling to the 
    /// GameManager (as this would likely have more stuff to do, aka further level loading, etc)).
    /// </summary>
    void Start()
    {
        GameManager.Notifications.OnSphereCollected += Notifications_OnSphereCollected; 
    }

    #endregion Start

    #region Event Handlers

    /// <summary>
    /// OnSphereCollected event handler (will be triggered by
    /// a sphere when it's collider comes into contact with the player).
    /// </summary>
    /// <param name="sender"></param>
    void Notifications_OnSphereCollected(Component sender)
    {
        //Debug (check sender object type for reference)
        if (sender != null)
        {
            Debug.Log(sender.GetType());
        }

        //Increment score
        Score += 10;

        //Game won
        if (Score >= 50)
        {
            //It might be more than the GameManager that needs to know about this - This call would take care of all registered objects (listeners)
            GameManager.Notifications.AllSpheresCollected(this);
        }
    }

    #endregion Event Handlers
}

In addition, notice the little bit of debugging listed in the Notifications_OnSphereCollected event handler. We’ll see this in action a little later on. The Player, First Person Controller, GameObject configuration is as follows:

Player Configuration.

Player Configuration.

The last component we need is something that will trigger our NotificationsManager.SphereCollected method; in turn triggering the relevant event to move the game forward (and push the player towards the AllSpheresCollected event that will end the game). The following class definition completes the puzzle.

using UnityEngine;

/// <summary>
/// Class representing Sphere interaction logic.
/// </summary>
public class SphereScript : MonoBehaviour
{
    #region OnTriggerEnter Event

    /// <summary>
    /// OnTriggerEnter event that handles collisions between the object
    /// attached to this script on other colliders.
    /// </summary>
    /// <param name="other">A reference to the colliding object.</param>
    void OnTriggerEnter(Collider other)
    {
        //If this sphere comes into contact with something that isn't the player then return
        if (!other.CompareTag("Player"))
        {
            return;
        }

        //Trigger the sphere collected event and hide this game object
        GameManager.Notifications.SphereCollected(this);
	    gameObject.SetActive(false);
    }

    #endregion OnTriggerEnter Event
}

When a sphere collides with another object the objects tag is inspected. If the object turns out to be the player we can post a notification to the public method found on the NotificationsManager class to trigger the appropriate event (sphere collected) on all listening objects. The final configuration of the sphere GameObject looks like this:

Sphere Configuration.

Sphere Configuration.

Proof is in the pudding so they say so let’s spin the game up and see if we can:

  1. Physically ‘collect’ sphere objects and see the players score increment by 10 points (proving that the OnSphereCollected event is being handled).
  2. Verify that the game ends when the players score is equal to or greater than 50 points (proving that the OnAllSpheresCollected event is being handled).
  3. Verify we receive appropriate debug information during the game.

So, the game fires up and our sphere collecting quest begins!

We’ve toiled hard and finally triumphed; the first sphere has been discovered! Our score is currently 0:

Before sphere 'collection'.

Before sphere ‘collection’.

Let’s pick that sucker up. Walking towards the sphere increments our points score by 10 (and triggers debug information as expected):

After sphere 'collection'.

After sphere ‘collection’.

Collecting 5 spheres, equaling 50 points, ends the game. Hoorah!

I call that a successful test of a 10 minute prototype. I’ve got a strong feeling that, under load, this will definitely perform better than the SendMessage approach and this is still very much open to refinement; given a few hours to think about it I’ll probably overhaul everything. It’s possible to bypass the events entirely and just use raw delegates or setup interface based solutions. However, this seems like a nice step up from the implementation I originally had introduced to me during the previous project.

I’ll definitely plod my way over to Blender shortly and get working on content creation but I’m happy that, given a short amount of time, I can come up with modest solutions to improve upon the backbone components of any proposed game engine I plan to write. I wanted to share the most simple implementation I could think of; this is by no means a complex or definitive example, but I hope it proves a point. If anyone reading does have any suggestions or links to ‘you should do it this way, it’s the best’ documentation or resources then I’d be happy to hear about it.

Signing off!

Forays in Unity 3D Game Development…For sh*ts and giggles

Good evening all,

I hope everyone has been having a super, duper New Year so far. In the process of getting back into the swing of things at work and preparing for my impending big day (well, I say impending, June – Nevertheless I’m sure it’ll come around quickly!) I’ve been spending little pockets of time looking at game development. Specifically, rooting around in Unity 3D in the efforts to produce an FPS shooter.

I ended up with a small wedge of cash for Christmas so I did what anyone nowadays does (not save for the wedding, cough cough) and splurged on a few development books. In addition to a book on AI programming and shaders this little number also crossed my path:

Pro Unity Game Development with C#

The content has been pretty damn good by all accounts and the resources provided have been top notch. The parts that have particularly caught my eye in my short time with this so far have been in handling event notifications between game objects and general asset management (i.e. the import and configuration of meshes created in Blender/Maya). The events manager, as implemented here, is an incredibly simple configuration that consists of the following NotificationsManager class:

using UnityEngine;
using System.Collections.Generic;
using System.Linq;

/// <summary>
/// Represents our core Event Handling Class for the 
/// CMOD FPS game.
/// </summary>
public class NotificationsManager : MonoBehaviour
{
    #region Private Data Fields

    //Private dictionary that tracks listeners for given event types
    private Dictionary<string, List<Component>> listeners = new Dictionary<string, List<Component>>();

    #endregion Private Data Fields

    #region Public Methods

    /// <summary>
    /// Ties the provided listener object (component) to the specified
    /// event type. If the event type is not yet being handled then a new dictionary 
    /// entry is created for the event type (notification name) and a new list of components
    /// instantiated ready for additions as required.
    /// </summary>
    /// <param name="sender">The component to be notified of a given event.</param>
    /// <param name="notificationName">The event to tie to the provided listener object.</param>
    public void AddListener(Component sender, string notificationName)
    {
        //Check to see if this notification (event) type is currently stored locally. If not, create a new dictionary entry for it
        if (!listeners.ContainsKey(notificationName))
        {
            listeners.Add(notificationName, new List<Component>());
        }

        //Tie a listener object to the given notification (event) type
        listeners[notificationName].Add(sender);
    }

    /// <summary>
    /// Allow specific listeners to unregistered themselves for a given
    /// event/notification type.
    /// </summary>
    /// <param name="sender">The object that no longer needs to listen for the given event.</param>
    /// <param name="notificationName">The event/notification type to be removed from.</param>
    public void RemoveListener(Component sender, string notificationName)
    {
        //Debug.Log("Removing listeners");

        //See if the notification type is supported currently. If not, then return
        if (!listeners.ContainsKey(notificationName))
        {
            return;
        }

        //Remove 'all' references that match (by instance id) for the given notification type
        listeners[notificationName].RemoveAll(li => li.GetInstanceID() == sender.GetInstanceID());
    }

    /// <summary>
    /// Allow for an event 'poster' to trigger a named method (based on the notification
    /// name) on all listening objects.
    /// </summary>
    /// <param name="sender">The poster who has latched onto an event in the first instance.</param>
    /// <param name="notificationName">The event/notification name (ties to a method name on listening objects).</param>
    public void PostNotification(Component sender, string notificationName)
    {
        //If there are no references based on the notification name then simply return (no work to do)
        if (!listeners.ContainsKey(notificationName))
        {
            return;
        }

        //Notify each, relevant, object of that a specific event has occurred
        listeners[notificationName].ForEach(li =>
        {
            if (li != null)
            {
                li.SendMessage(notificationName, sender, SendMessageOptions.DontRequireReceiver);
            }
        });
    }

    /// <summary>
    /// Removes redundant listeners (to cover scenarios where objects might be removed
    /// from the scene without detaching themselves from events).
    /// </summary>
    public void RemoveRedundancies()
    {
        //Create a new dictionary (ready for an optimised list of notifications/listeners)
        Dictionary<string, List<Component>> tempListeners = new Dictionary<string, List<Component>>();
        
        //Iterate through the notification/listener list and removing null listener objects. only keep a notification/listener dictionary entry if one or more
        //listening objects still exist for a given notification
        listeners.ToList().ForEach(li =>
        {
            li.Value.RemoveAll(listObj => listObj == null);

            if (li.Value.Count > 0)
            {
                tempListeners.Add(li.Key, li.Value);
            }
        });

        //Set the listener dictionary based on the optimised/temporary dictionary
        listeners = tempListeners;
    }
    
    /// <summary>
    /// Removes all listener dictionary references.
    /// </summary>
    public void ClearListeners()
    {
        listeners.Clear();
    }
    
    #endregion Public Methods
}

Using a centralised game manager class (that holds a reference to a single instance of the NotificationsManager class) it’s been surprisingly quick to setup a solid event based system. I want to tweak this implementation however for future projects. As you can see here, this class revolves around a dictionary of type string/Component. This allows for classes registering as ‘listeners’ for a given event to provide a reference to themselves (the component) along with a string representing an event name (which matches a public method implemented on the class registering as a listener). For this to work, posting notifications relies on calling the SendMessage method on listening objects, a process which relies heavily on reflection based on my studies. In a small scale project where limited amounts of messages will be getting passed around this will perform fine (and this has been the case thus far for me). In the long run however (maybe my next project), it seems like a better approach will be to define interfaces and perhaps use a dedicated system structured around delegates. High levels of reflection are not going to cut the mustard when the levels of notifications being passed around start hitting even modest levels.

As far as handling the content pipeline and getting stuck into Lightmapping, Navigation Meshes (for AI Pathfinding) and the construction of Colliders when importing assets the instruction provided has been incredibly easy to follow.

Sprites are provided with the book assets and with a couple of basic scripts it was fairly simple to get a billboard sprite enemies up and running (with flip-book style animations). I’ve worked with 3D models in the past (using the Unity Mecanim system) and have produced rigged and animated models but I’ve really enjoyed utilising some 2D animation techniques (quite a bit of the 2D support now in Unity is quite new so it’s always great to skim over new material like this).

Unity UI.

Unity UI.

The enemies themselves use a basic FSM (Finite State Machine) to manage Patrol, Chase and Attack states as shown below. I’ve defined the class as abstract with the coroutines that handle each enemy state made virtual (to allow certain enemies to override the behaviour as necessary). Surfacing variables in the Unity Editor requires them to be public (and they can’t be properties), something that is driving me a little nuts – Treating as a nuance for the time being:

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

/// <summary>
/// Abstract base class that outlines the 
/// concept of an enemy.
/// </summary>
public abstract class Enemy : MonoBehaviour
{
    #region Public Enums

    /// <summary>
    /// Enum state for the enemy FSM.
    /// </summary>
    public enum EnemyState
    {
        Patrol = 0,
        Chase = 1,
        Attack = 2
    }

    /// <summary>
    /// Enum for enemy types.
    /// </summary>
    public enum EnemyType
    {
        Drone = 0,
        ToughGuy = 1,
        Boss = 2
    }

    #endregion Public Enums

    #region Unity Inspector Public Variables

    /// <summary>
    /// The enemies type.
    /// </summary>
    public EnemyType Type = EnemyType.Drone;

    /// <summary>
    /// Active enemy state (defaulted to patrol).
    /// </summary>
    public EnemyState ActiveState = EnemyState.Patrol;

    /// <summary>
    /// The custom ID of this enemy.
    /// </summary>
    public int EnemyID = 0;

    /// <summary>
    /// Represents the enemies current health.
    /// </summary>
    public int Health = 100;

    /// <summary>
    /// Attack Damage - amount of damage the enemy
    /// deals to the player.
    /// </summary>
    public int AttackDamage = 10;

    /// <summary>
    /// Recovery delay in seconds after launching an attack.
    /// </summary>
    public float RecoveryDelay = 1.0f;

    /// <summary>
    /// Total distance in Unity Units from current position 
    /// that agent can wander when patrolling.
    /// </summary>
    public float PatrolDistance = 10.0f;

    /// <summary>
    /// Total distance the enemy must be from player, in Unity units, before
    /// chasing them (entering chase state).
    /// </summary>
    public float ChaseDistance = 10.0f;

    /// <summary>
    /// Total distance enemy must be from the player before
    /// attacking them.
    /// </summary>
    public float AttackDistance = 0.1f;

    #endregion Unity Inspector Public Variables

    #region Protected Fields

    //Reference to the active PlayerController component for the player
    protected PlayerController playerCont = null;

    //Enemy cached transform
    protected Transform thisTranform = null;

    //Reference to the player transform
    protected Transform playerTransform = null;

    //The Nav Mesh attached to this enemy (for pathfinding)
    protected NavMeshAgent agent = null;

    #endregion Protected Fields

    #region Start

    /// <summary>
    /// Initialisation logic for an enemy.
    /// </summary>
    protected virtual void Start()
    {
        //Retrieve the Nav Mesh Agent for this enemy (cache it)
        agent = GetComponent<NavMeshAgent>();

        //How about? Get reference to player controller by using the controller 'tag'
        playerCont = GameObject.FindGameObjectWithTag("Player").GetComponent<PlayerController>();

        //Get player transform
        playerTransform = playerCont.transform;

        //This enemies transform
        thisTranform = transform;

        //Set default state
        ChangeState(ActiveState);

        GameManager.Notifications.AddListener(this, "SaveGamePrepare");
        GameManager.Notifications.AddListener(this, "LoadGameComplete");
    }

    #endregion Start

    #region Public Methods

    /// <summary>
    /// Change AI State.
    /// </summary>
    /// <param name="ActiveState"></param>
    public void ChangeState(EnemyState state)
    {
        //Maybe consider checking the state? Has it changed?

        //First, stop all currently running AI processing
        StopAllCoroutines();

        //Set new state and activate it
        ActiveState = state;

        //Start coroutines and in each case notify the game object in case we want to handle state change (might not just be here, perhaps in other components)
        switch (ActiveState)
        {
            case EnemyState.Attack:
                {
                    StartCoroutine(AiAttack());
                    SendMessage("Attack", SendMessageOptions.DontRequireReceiver);
                    return;
                }
            case EnemyState.Chase:
                {
                    StartCoroutine(AiChase());
                    SendMessage("Chase", SendMessageOptions.DontRequireReceiver);
                    return;
                }
            case EnemyState.Patrol:
                {
                    StartCoroutine(AiPatrol());
                    SendMessage("Patrol", SendMessageOptions.DontRequireReceiver);
                    return;
                }
            default:
                {
                    return; //Nothing else to do, return
                }
        }
    }

    /// <summary>
    /// Prepare data to save this enemy.
    /// </summary>
    /// <param name="sender">The component sender object.</param>
    public void SaveGamePrepare(Component sender)
    {
        //Create a reference for this enemy
        LoadSaveManager.GameStateData.DataEnemy thisEnemy = new LoadSaveManager.GameStateData.DataEnemy();
        
        //Fill in data for the current enemy
        thisEnemy.EnemyID = EnemyID;
        thisEnemy.Health = Health;
        thisEnemy.PosRotScale.X = thisTranform.position.x;
        thisEnemy.PosRotScale.Y = thisTranform.position.y;
        thisEnemy.PosRotScale.Z = thisTranform.position.z;
        thisEnemy.PosRotScale.RotX = thisTranform.localEulerAngles.x;
        thisEnemy.PosRotScale.RotY = thisTranform.localEulerAngles.y;
        thisEnemy.PosRotScale.RotZ = thisTranform.localEulerAngles.z;
        thisEnemy.PosRotScale.ScaleX = thisTranform.localScale.x;
        thisEnemy.PosRotScale.ScaleY = thisTranform.localScale.y;
        thisEnemy.PosRotScale.ScaleZ = thisTranform.localScale.z;
        
        //Add this enemy to the list
        GameManager.StateManager.GameState.Enemies.Add(thisEnemy);
    }

    /// <summary>
    /// Prepare data to load this enemy.
    /// </summary>
    /// <param name="sender">The component sender object.</param>
    public void LoadGameComplete(Component sender)
    {
        //Cycle through enemies and find matching ID
        List<LoadSaveManager.GameStateData.DataEnemy> enemies = GameManager.StateManager.GameState.Enemies;

        //Reference to this enemy
        LoadSaveManager.GameStateData.DataEnemy thisEnemy = null;

        for (int i = 0; i < enemies.Count; i++)
        {
            if (enemies[i].EnemyID == EnemyID)
            {
                //Found enemy. Now break break from loop
                thisEnemy = enemies[i];
                break;
            }
        }

        //If we can't find this enemy then it must have been destroyed on save
        if (thisEnemy == null)
        {
            DestroyImmediate(gameObject);
            return;
        }
          
        //We've got this far so load the enemy data
        EnemyID = thisEnemy.EnemyID;    //This is set from the inspector so not much point in reloading, keeping with the book code however
        Health = thisEnemy.Health;
            
        //Set position, rotation and scale (position done with warp)
        agent.Warp(new Vector3(thisEnemy.PosRotScale.X, thisEnemy.PosRotScale.Y, thisEnemy.PosRotScale.Z));
        thisTranform.localRotation = Quaternion.Euler(thisEnemy.PosRotScale.RotX, thisEnemy.PosRotScale.RotY, thisEnemy.PosRotScale.RotZ);
        thisTranform.localScale = new Vector3(thisEnemy.PosRotScale.ScaleX, thisEnemy.PosRotScale.ScaleY, thisEnemy.PosRotScale.ScaleZ);
    }

    #endregion Public Methods

    #region Coroutines

    /// <summary>
    /// AI method that handles attack behaviour for the enemy.
    /// Can exit this state and enter either patrol or chase.
    /// </summary>
    /// <returns>IEnumerator.</returns>
    public virtual IEnumerator AiAttack()
    {
        //Stop agent, ready for a new instruction
        agent.Stop();

        //Elapsed time, to calculate strike intervals (set to recovery delay so an attack is possible immediately after the enemy closes distance)
        float elapsedTime = RecoveryDelay;

        //Loop forever while in the attack state
        while (ActiveState == EnemyState.Attack)
        {
            //Update elapsed time
            elapsedTime += Time.deltaTime;

            //Check distances and state exit conditions
            float distanceFromPlayer = Vector3.Distance(thisTranform.position, playerTransform.position);

            //If outside of chase range, then revert to patrol
            if (distanceFromPlayer > ChaseDistance)
            {
                ChangeState(EnemyState.Patrol);
                yield break;
            }

            //If outside of attack range, then change to chase
            if (distanceFromPlayer > AttackDistance)
            {
                ChangeState(EnemyState.Chase);
                yield break;
            }

            //Make strike
            if (elapsedTime >= RecoveryDelay)
            {
                elapsedTime = 0;
                SendMessage("Strike", SendMessageOptions.DontRequireReceiver);
            }

            //Wait until the next frame
            yield return null;
        }
    }

    /// <summary>
    /// AI method that handles attack behaviour for the enemy.
    /// Can exit this state and enter either attack or patrol.
    /// </summary>
    /// <returns>IEumerator.</returns>
    public virtual IEnumerator AiChase()
    {
        //Stop agent, ready for a new instruction
        agent.Stop();

        //Whilst we are in the chase state then loop forever
        while (ActiveState == EnemyState.Chase)
        {
            //Set destination to the player
            agent.SetDestination(playerTransform.position);

            //Check the distance between the enemy and the player to look for state changes
            float distanceFromPlayer = Vector3.Distance(thisTranform.position, playerTransform.position);

            //If within attack range then alter to that state
            if (distanceFromPlayer < AttackDistance)
            {
                ChangeState(EnemyState.Attack);
                yield break;
            }

            //Enemy is out of range to chase, revert to the patrol state
            if (distanceFromPlayer > ChaseDistance)
            {
                ChangeState(EnemyState.Patrol);
                yield break;
            }

            //Wait until the next frame
            yield return null;
        }
    }

    /// <summary>
    /// AI method that handles attack behaviour for the enemy.
    /// Can only enter the chase state from here (once the distance
    /// to the player closes sufficiently).
    /// </summary>
    /// <returns>IEnumerator.</returns>
    public virtual IEnumerator AiPatrol()
    {
        //Stop agent, ready for a new destination
        agent.Stop();

        //Loop forever whilst in the patrol state
        while (ActiveState == EnemyState.Patrol)
        {
            //Get random location destination on the map (somewhere within a sphere with a radius based on PatrolDistance (center at zero))
            Vector3 randomPosition = (Random.insideUnitSphere * PatrolDistance);

            //Add as offset from current position
            randomPosition += thisTranform.position;

            //Get nearest valid position (on the Nav Mesh)
            NavMeshHit hit;
            NavMesh.SamplePosition(randomPosition, out hit, PatrolDistance, 1);

            //Set destination for this enemy
            agent.SetDestination(hit.position);

            /*
             * Set distance range between object and destination to classify as 'arrived' +
             * Set timeout before new path is generated (as a fail safe, destination too far or enemy could be having difficulty reaching the location) +
             * Create a elapsed time float to measure against the timeout
            */
            float arrivalDistance = 2.0f, timeOut = 5.0f, elapsedTime = 0;

            //Wait until the enemy reaches the destination or times-out, then get the new position
            while (Vector3.Distance(thisTranform.position, hit.position) > arrivalDistance && elapsedTime < timeOut)
            {
                //Update elapsed time
                elapsedTime += Time.deltaTime;

                //Can only enter chase (once the distance closes sufficiently). Can then move to other FSM states from there
                if (Vector3.Distance(thisTranform.position, playerTransform.position) < ChaseDistance)
                {
                    ChangeState(EnemyState.Chase);
                    yield break;
                }

                yield return null;
            }
        }
    }

    #endregion Coroutines
}

Coroutines (that have a return type of IEnumerator) are a useful feature within the Unity engine that allow you to mimic asynchronous behaviour. When previously using Unity, I would definitely be guilty of cramming all of the logic into the Update method (run every frame) which invariably lead to performance issues. Using an event based system and coroutines it’s been possible (in most cases) to abstract heavy lifting code out of areas where it gets hit every frame and is instead called when required (or as a side-line to the main thread).

This class also showcases the use of a NavMeshAgent. During the patrol state (where the most interesting implementation lies) a location is ‘sampled’ and then the nearest, valid position found on the scene Navigation Mesh is chosen as the enemy destination. When the destination is reached, within a certain tolerance range (time out also provided just in case), the enemy marches on his way to the next generated destination (until the player is found and a different state is initiated or the enemy is killed). This works surprisingly well.

I’ve got a book that talks through more advanced scenarios using Behaviour Trees which I’ll try and implement in a future project.

In all honesty, I think that’s probably where I’m headed. The assets provided in this book allow the rapid creation of a modular level environment and to this end the internet has been scoured for tutorials on creating this in Blender (including environment texturing and UV creation). I’ve picked a direction for this blog at long last, hoorah! A preliminary promise of sorts then…I aim to cover (not all strictly code based but indulge me!):

  1. Stick with the FPS formula and create a new project from scratch.
  2. Undertake some 3D content creation in Blender (modular environments and UV creation). I don’t have a clue what I’m doing here so I expect that to be damn funny to witness!
  3. Keep with the 2D sprite setup for enemies, perhaps making a set of 5 or 6 enemy types and at least 3 weapon types.
  4. Make a handful of 3D game world assets using Blender.
  5. Implement an enhanced NotificationsManager in Unity.
  6. Get all of the content into Unity and make a multi-level FPS shooter.
  7. Investigate the new UI components in Unity (to build a dynamic GUI of some sort).
  8. Produce my own sound content. I’ve tried this already via some online music/sound effect creation tools, the results were not good. If my brother is reading this he’ll know that I’ve never been all that musical and is probably cringing at the thought of anything knocked up by me in this department!

Big promises eh! Let’s see what gets delivered. I’m not expecting an AAA experience to fall out of the dredges of my file system and arrive on the doorstep of steam, this will just be for fun!

I’ll leave you with a few screenshots taken from the (pretty much finished) product. Enjoy!

Gun-play in action.

Gun-play in action.

Getting beaten around the head by an enemy drone.

Getting beaten around the head by an enemy drone.

Options GUI.

Options GUI.

Collecting cash before getting gunned down.

Collecting cash before getting gunned down.