Design Docs - Thinking about Inventory - Part 7

What Have I Been Up To?

Wow, it has been quite some time since the last time I sat down and thought about video games. Haha, just kidding! I've thought almost about nothing else, but video games!

Although these posts are design documents for a game I'm contributing to, I really like using some of the time I spend writing these articles to write about some of the things that happen in my life. You know, like a real journal. ๐Ÿ˜„

Yeah, a journal!

So what has happened since Chapter 6? Well, my new job is going well! I love working on the product, which I have become a huge fan of. It's awesome because I've found that it has helped me time manage myself at work with some of the productivity features paired with the already pretty cool email client.

The last time I can remember enjoying dog-fooding a product I work on professionally is Skype. I still use Skype to this day even after leaving the team, and even Microsoft. It has gotten a lot better over the years, and the emoticons are some of the best I have seen in a messaging application.

I also attended the Portland Retro Gaming Expo for the second time in October 19, 2019.

The first time around was way back in 2016. My wife and I had flown to Portland from Boise, Idaho to attend then.

This time was different in that I had intention of going to the expo to pick up a lot of old video game strategy guides and magazines for the purpose of research, study, and a bit of personal nostalgia.

I also picked up a few video games and hardware. They were mostly Pokemon related. I've been having retro-Pokemon fever lately, so I decided to satisfy some of that craving by picking up a few Gen1 and Gen2 games.

Our Portland Retro Gaming Expo pickups!

Our Portland Retro Gaming Expo Pickups

 

To add to all this, I also purchased two Gameboy Advance SPs to serve as beater systems for my wife and I. This is something I have never known about before, but there were actually two variants of the Gameboy Advance SP. I only found this out after purchasing a Pearl Blue SP ($75) and Black SP ($55).

Up until recently, I had assumed that the Pearl editions of the Gameboy Advance SP were generally more expensive simply due to having it been a "special edition" from a marketing aspect, and of course, being a later release in the console's life.

However, there is actually more to the story! The night of the expo, my wife had decided to try out a game with the Pearl Blue SP we had bought earlier in the day.

As I walked by her to go to the bathroom, I noticed that the colors of the game were amazingly good -- even for modern standards.

I stopped for a second, and told her "Wow, those are some really good graphics. They're more colorful than I remember!"

She responded with "I know right?!?! It's sooooo pretty."

I just mumbled "Probably because we're so used to playing old Gameboy games now... Those are a lot older than the Gameboy Advance games." Life continued on.

It wasn't actually until we got back to California which I noticed something strange.

As I was cleaning both the Gameboy Advance SP handhelds, I had noticed that the Black one had displayed the games exactly the way I had remember seeing them as a kid.

I had a feeling something was up! The Pearl Blue machine seemed to have had a better screen! I did some research and confirmed that my eyes were definitely not deceiving me.

There were two variants of the GBA SP handhelds made. The AGS-001, the "normal dim screen", and the AGS-101, the "even brighter screen!" model. The AGS-101 model was released later in 2005, and as a result, isn't as common. Note that in 2005, the Nintendo DS had already been released and was considered to be the "latest and greatest"

Here is a comparison of the two:

Comparison between Pearl Blue and Black GBA screens.

Comparison between Pearl Blue and Black GBA screens.

Interesting, huh? Being the awesome husband that I am, I've let my wife have the better model to enjoy. ๐Ÿ˜Ž

Aside from pickups, I had also attended a talk by Brett Weiss, and Kelsey Lewin. It was a Q&A panel about writing, and I enjoyed the talk. I came out motivated to continue improving my writing skills. One day, I hope to publish a book relating to all my struggles in software development. Maybe these articles will be included. Who knows? ๐Ÿ€

Definitely an unexpected surprise was that I had answered a random trivia question from Brett and Kelsey to win a book! I even got their autographs!

My signed book

My signed book from Brett and Kelsey. The SNES Omnibus Vol. 2

By the way, the trivia question was "What was the last official game relased for the Sega Genesis?" -- Psh! Easy! "Frogger!"

Aside from the Portland Retrogaming Expo, my wife and I had also spent a few days road-tripping, and camping across Northern California and through Oregon. It was awesome, but I'm definitely sick of evergreen and pine! Maybe next time, I'll go to Utah, or Arizona for a change in geology and landscape. In my opinion, being in the Pacific Northwest, all the green, trees and marine layer does get a bit old after a while.

This particular camp site was tolerable ๐Ÿ˜„ as it was on a beach cliff up in Eureka, CA.

Our campsite

Our campsite

Back to the JRPG Inventory System

You're definitely not just reading this post to know about my personal life, right? I'm sure there is still much interest in developing the JRPG Inventory System even further!

The goal of this discussion today is to build out a possible solution to handling items which trigger events in a game. I'd like to term this sort of pattern the key-lock pattern.

Key-Lock Pattern

One item is the key, and the lock is the entity which the item can be applied on.

Some use cases in which I can think of using this pattern in a JRPG:

  • An item to open a door, treasure chest, or dungeon.
  • An item which can be used to start, or finish a game event, or quest.
  • An item which can be applied onto a party member to trigger an event.
  • Or, an item which can be applied onto an enemy to fulfill a task, trigger an event, deal damage, etc.

Basically, there are a lot of uses here with this pattern. The difficulty in designing and implementing such a feature in an inventory system comes from just defining a consistent architecture which can be generalized to allow the least maintenance possible.

A Possible Solution

Although I haven't touched any of the code I have written in a few weeks now, i Have been thinking about a possible solution to approaching this key-lock feature in the current implementation of the JRPG Inventory System.

One approach that has been repeatedly surfacing in my mind has been a publisher-subscriber architecture. From the fuzzy thoughts in my head:

  • Items serving as keys are the publishers.
  • Any entity within the game which derives from a lock is the subscriber.

The items which do the publishing will generate specific events in which once produced, can be handled by entities who have registered themselves to listen for these generated events, and consume them.

In most JRPG games, key items are pre-defined. Therefore, it is a good assumption that since they are not procedurally generated, resources can be allocated to allow these items to be created early on in the game.

Assuming that the instances of these items already exist, the entities can register themselves onto the key items. This approach thereby makes them subscribers to the Apply method within the BaseItem implementation.

Let's picture it this way:

Entity registration

Entities can register themselves against an item so that they can receive events from the item.

 

The key item then maintains a list of these registered subscribers who implement a common interface.

An example interface of such a subscriber can be:

interface IItemSubscriber 
{
	void Publish();
}

When it is time for the item to be used, the BaseItem instance can iterate through its subscriber list, taking each subscriber and invoking the internal Publish method provided by them.

foreach (var subscriber in subscribers)
{
	subscriber.Publish();
}

You will notice now that it is really up to the subscriber to use and manage some state, or game state itself. The item isn't really responsible for controlling how the game plays out. It is merely used as a messenger to deliver an event to the entity for signaling.

Since it is the case that the entity is handling the event which is produced by the BaseItem::Apply method through its invocation of Publish, then we can also think of the Publish method as a handler.

If we can think about it this way, then it would be handy to have our key items publish back some payload to these subscribers. Perhaps maybe a Dictionary<string, object>? Let's try that:

interface IItemSubscriber
{
	void Publish(Dictionary<string, object> message);
}

We just now have to remember that items can now send a payload to the subscribers. How do we do that without disrupting some of the foundational design we have made until now to keep items very dynamic?

Well, I believe we can definitely allow this feature to be built by the magic of object oriented programming!

Key items which are currently described by our current item schema definition to procedurally generate items can include a special property called PublishHandler property. The intention of this property is to define a handler class which can be instantiated at run-time. An easy approach to that is by the use of .NET reflection.

The PublishHandler will implement another common interface with a method which can handle construction of the messages which will be eventually be passed to the subscribers as a payload.

interface IPublishHandler
{
	Dictionary<string, object> GetMessage();
}

Now, we can implement various types of IPublishHandler classes which many different types of items can share and re-use to generate and pass message payloads to entities to process events and control state.

Ideally, we only want key items holding usable instance of an IPublishHandler in our item schema definition. An easy way to ensure this is to define an IsKeyItem boolean field to indicate whether, or not an item serves as a key item, or normal item.

The field can be useful to be checked against and then control the flow of execution of generating a message through the IPublishHandler and passing it onto the subscribers.

Apply()
{
	... standard code
	
	if(IsKeyItem && publishHandler != null) 
	{
		var message = publisherHandler.GetMessage();
		foreach (var subscriber in subscribers)
		{
			subscriber.Publish(message);
		}
	}
}

A visualization helps, so here's an example sequence diagram on how execution may occur:

Sequence Diagram

Sequence diagram of a typical flow

 

Implementation

Now, it comes time to implement these ideas into code. This feature is small, but the capabilities it can bring to a game is huge!

Getting the Code

Our jrpg-inventory-system code will include the new changes under the ch07 tag within the GItHub repository. Also, if you have follwed from the last chapter, all code will be easily ported into Unity. I won't really discuss that part here since we have already been on that adventure previously. The process will be similar this time around as last.

Again, as usual, for the lazy, the direct link to the repository is here: https://github.com/urbanspr1nter/jrpg-inventory-system. ๐Ÿš€

Plan

In addition to implementation, I will also write some unit tests to simulate a couple of game scenarios.

  • A card key (item) which unlocks a door (entity) which is currently locked.

    • A basic demonstration of the key-lock pattern. Basically, using an item triggers a call to a subscriber to change its own internal state.
  • A soda pop (item) to give to a thirsty guard who is blocking a door and is refusing to move until his thirst is quenched. (Pokemon inspired)

    • A more complex demonstration where giving a key item to an NPC causes the entire game state to change. The game state here is the subscriber.

A thirsty guard. Remember this?

A thirsty guard. Remember this?

Code

First, let's implement our basic interfaces: IItemSubscriber and IPublishHandler. These are easy since we have already established their purposes earlier on:

IItemSubscriber.cs
using System.Collections.Generic;
namespace InventorySystem.Items
{
    public interface IItemSubscriber
    {
        void Publish(Dictionary<string, object> message);
    }
}
IPublishHandler.cs
using System.Collections.Generic;
namespace InventorySystem.Items
{
    public interface IPublishHandler
    {
        Dictionary<string, object> GetMessage();
    }
}

Now, we need to update our POCO class which represents the item schema definition to include the two properties:

  1. IsKeyItem - (boolean) Is the current item a key item?
  2. PublishHandler - (string) The name of the publish handler class to instantiate.

Under InventorySystem.PgItems.Item, we can add the two properties.

Item.cs
using System.Linq;
using System.Collections.Generic;
using System;

namespace InventorySystem.PgItems
{
    public class Item
    {
        public string Name { get; set; }
        public bool IsKeyItem { get; set; }
        public string PublishHandler { get; set; }
        public List<ItemClassEdge> ItemClass { get; set; }
        public List<Property> Properties { get; set; }
        public double Value { get; set; }
        public string BodyPart { get; set; }

        public override bool Equals(object obj)
        {
            var that = obj as Item;

            if (that == null)
            {
                return false;
            }

            if (!that.Name.Equals(Name))
            {
                return false;
            }
            if (that.IsKeyItem != IsKeyItem)
            {
                return false;
            }

            if (that.PublishHandler != null && PublishHandler != null)
            {
                if (!that.PublishHandler.Equals(PublishHandler))
                {
                    return false;
                }
            } else if (that.PublishHandler != null || PublishHandler != null)
            {
                return false;
            }

            if (!that.Value.Equals(Value))
            {
                return false;
            }
            if (!that.BodyPart.Equals(BodyPart))
            {
                return false;
            }
            if (!that.ItemClass.SequenceEqual(ItemClass))
            {
                return false;
            }
            if (!that.Properties.SequenceEqual(Properties))
            {
                return false;
            }

            return true;
        }

        public override int GetHashCode()
        {
            return HashCode.Combine(
                Name,
                IsKeyItem,
                PublishHandler,
                ItemClass,
                Properties,
                Value,
                BodyPart
            );
        }
    }
}

BaseItem Updates

The BaseItem class also needs to be updated with a couple of new methods and code which will make use of the new information being loaded from the item definition.

Since the code of BaseItem.cs is rather large, I am going to try and avoid doing a massive copy-and-paste here, and actually just fly over the changes involved in a series of list items. If you'd like to see the complete code file, the repository will have all the details there.

  • Add references to 2 new instances of IPublishHandler and a collection of subscribers of List<IItemSubscriber>.
private IPublishHandler publishHandler;
private List<IItemSubscriber> subscribers;
  • Add a getter called IsKeyItem to mirror the property found in the item definition.
public bool IsKeyItem
{
  get
  {
  	return ItemClassDefinition.IsKeyItem;
  }
}
  • Add a getter called PublishHandlerName to mirror the property found in the item definition.
public string PublishHandlerName
{
  get
  {
  	return ItemClassDefinition.PublishHandler;
  }
}
  • In the BaseItem constructor, create a new List<IItemSubscriber> and assign it to the internal list of subscribers.
subscribers = new List<IItemSubscriber>();
  • In the BaseItem constructor, create a new instance IPublishHandler defined by the PublishHandlerName within the item definition.
if (IsKeyItem && PublishHandlerName != null)
  publishHandler = (IPublishHandler)Activator.CreateInstance(
    Type.GetType($"InventorySystem.Items.{PublishHandlerName}"),
    new object[] { }
  );

This one involves a bit of reflection magic. ๐Ÿ˜„ The intention is to only construct the instance if the item is a key item and if the PublisherHandlerName exists. Remember, the PublisherHandlerName is the class implementation of the IPublishHandler -- the class which will construct the message payload to be passed to the subscriber upon Publish.

  • The ability to Register and Unregister subscribers from the item.
public void Register(IItemSubscriber subscriber)
{
  var existing = subscribers.Find(s => s == subscriber);

  if(existing == null)
  {
  	subscribers.Add(subscriber);
  }
}

public void Unregister(IItemSubscriber subscriber)
{
	var existing = subscribers.Find(s => s == subscriber);

	if(existing != null)
	{
		subscribers.Remove(subscriber);
	}
}

The two methods Register and Unregister are very simple. They just maintain the subscribers list. The only thing to really note here is that we do not want to add the same subscriber if it is already in the list, and we also don't want to attempt to remove a subscriber from the list if they are not registered.

  • Publishing events from the item to the subscribers. We need to modify the Apply method to notify all subscribers that usage of the key item has occurred.
public bool Apply(Character targetChar)
{
  ItemClassDefinition.Properties
  	.ForEach(p => ApplyCharacterProperty(targetChar, p));

  if(IsKeyItem)
  {
  	var message = publishHandler.GetMessage();
  	foreach (var subscriber in subscribers)
  	{
  		subscriber.Publish(message);
  	}
  }

	return true;
}

Unit Tests

Looks like we're mostly done with the implementation. Let's write a couple of scenarios to test with. We need to add a couple of test items so we can actually write our unit tests. Within Items.json, include the following items within the dataset:

[
  // ... 
  {
    "Name": "Card Key",
    "ItemClass": [],
    "Properties": [],
    "Value": 0,
    "BodyPart": "Default",
    "IsKeyItem": true,
    "PublishHandler": "UnlockDoorHandler"
  },
  {
    "Name": "Soda Pop",
    "ItemClass": [],
    "Properties": [],
    "Value": 0,
    "BodyPart": "Default",
    "IsKeyItem": true,
    "PublishHandler": "DrinkHandler"
  }
  // ...
]

You'll see that we have created two new items, Card Key, and Soda Pop. We'll need to now implement two additional handlers to generate messages to be passed to any subscribers using these items.

I was kind of lazy here, and ended up dumping the IPublishHandler classes into the InventorySystem.Items namespace. For sure this is something which can be refactored in the future.

First, within the InventorySystem.Items namespace, create a new file called UnlockDoorHandler.cs. The implementaiton will be:

UnlockDoorHandler.cs
using System.Collections.Generic;
namespace InventorySystem.Items
{
    public class UnlockDoorHandler : IPublishHandler
    {
        public Dictionary<string, object> GetMessage()
        {
            return new Dictionary<string, object>
                {
                    { "result", false }
                };
        }
    }
}

Now, still under the InventorySystem.Items namespace, create another file called DrinkHandler.cs.

DrinkHandler.cs
using System.Collections.Generic;
namespace InventorySystem.Items
{
    public class DrinkHandler : IPublishHandler
    {
        public Dictionary<string, object> GetMessage()
        {
            return new Dictionary<string, object> {
                { "result", false }
            };
        }
    }
}

The two files above will serve as the "handlers" for whenever the events of using a Card Key and Soda Pop occur.

We are pretty much set here with scaffolding the important pieces to bootstrap our test. Now, let's write our test!

TestKeyItemUsage

Our new test file will be called TestKeyItemUsage.cs. We'll first modify the TestUtilities.cs class to include the new items to test with. As per usual, the file can be referenced from the repository here.

https://github.com/urbanspr1nter/jrpg-inventory-system/blob/master/InventorySystem/InventorySystemTests/TestKeyItemUsage.cs

TestUtilities.cs
public static ItemName CardKey = new ItemName("Card Key");
public static ItemName SodaPop = new ItemName("Soda Pop");

...
  
public BaseItem CardKeyItem()
{
  var cardKeyItemDef = items.Find(i => i.Name.Equals(CardKey.ToString()));

  return new BaseItem(cardKeyItemDef);
}

public BaseItem SodaPopItem() 
{
  var sodaPopItemDef = items.Find(i => i.Name.Equals(SodaPop.ToString()));

  return new BaseItem(sodaPopItemDef);
}
...

These just basically give back the specific items needed to construct a BaseItem instance.

Testing the Card Key

Now, within TestKeyItemUsage.cs, we will internally create a GlobalGameState variable to emulate a global state of some game.

private static Dictionary<string, object> GlobalGameState = 
	new Dictionary<string, object>();

Also, we'll include some other variables you're probably very familiar with now to bootstrap the test.

private InventoryManager inventoryManager;
private Party party;
private TestUtilities testUtils;
private IDbReader itemsDbReader;

Let's write the Card Key test first. Here's how it will work.

  • There is a door which is locked. Only a card key can open it.
  • You receive a card key, and use it against the door.
  • The door should now be unlocked.

Let's the TestCardKey test method to satisfy those requirements.

In order to do so, we will also need to create an entity to serve as a subscriber to the Card Key item. Create an inner class within the TestKeyItemUsage class called SuperSecureDoor. This inner-class can be our subscriber for the test.

private class SuperSecureDoor : IItemSubscriber
{
  public bool Locked { get; private set; } = true;

  private string LockStatus()
  {
    return Locked ? "LOCKED" : "UNLOCKED";
  }

  public void Publish(Dictionary<string, object> message)
  {
    Console.WriteLine($"The door is currently {LockStatus()}");
    Console.WriteLine("Attempting to unlock...");

    if (Locked)
      Locked = (bool)message["result"];

    Console.WriteLine($"... The door is {LockStatus()}!");
  }
}

It is a basic class which simulates a door object. The door has a Locked property to indicate its status of whether or not it is LOCKED, or UNLOCKED. Using the card key against it will unlock the door when its Publish method is called.

So let's write the test!

[Fact]
public void TestCardKey()
{
  Console.WriteLine("--- Card Key Test --- ");

  itemsDbReader = new ItemsDbReader();
  testUtils = new TestUtilities(itemsDbReader.ReadData<Item>(DbConstants.ItemsDbFile));

  inventoryManager = new InventoryManager();
  party = new Party(inventoryManager);

  Character terra = new Character("Terra");
  party.AddMember(terra.Name, terra);

  var cardKey = testUtils.CardKeyItem();
  var subscriber = new SuperSecureDoor();

  Assert.True(subscriber.Locked);

  cardKey.Register(subscriber);

  party.ReceiveItem(cardKey, ItemReceiveAction.Reward);
  party.SetActiveCharacter(terra.Name);

  party.UseItem(TestUtilities.CardKey, terra);

  Assert.False(subscriber.Locked);
}

Pretty simple. All we are doing here is seeding the inventory manager with the Card Key item included. Then we create our party with our favorite character, Terra. Once we have done so, we will then create a SuperSecureDoor and register it against the Card Key item.

The door is checked to see if it is locked.

When Terra uses the card, the Apply method of the Card Key invokes the publish calls to trigger the SuperSecureDoor to flip its Locked status to UNLOCKED. Running the test with Console.WriteLine produces:

--- Card Key Test --- 
The door is currently LOCKED
Attempting to unlock...
... The door is UNLOCKED!

This is the most basic illustration of the key-lock pattern in use. Our second test will be more complex with the notion of global game state being modified.

The Thirsty Guard

Maybe some of you will remember this type of scenario. It was inspired by the Generation 1 Pokemon games (Red, Blue, Yellow). When entering Saffron City initially, the player is not allowed to pass due to the guard being thirsty.

This triggers a quest which the player must go buy a drink for the guard so that he can quench his thirst and allow the player to pass through. The player goes to Celadon City, buys some sort of drink, and then gives it to the guard. The guard then has his thirst quenched, and finally lets the player pass through to Saffron City.

We can do something similar here. To make things interesting, let's leverage some global game state to keep track of whether, or not the guard has had his thirst quenched.

Within the TestKeyItemUsage constructor, initialize the global game state.

public TestKeyItemUsage()
{
  GlobalGameState["GuardOnDuty"] = true;
}

Also, we will need to create another inner class called SodaPopDrinker in which can handle the state change of whether or not the guard is on duty.

private class SodaPopDrinker : IItemSubscriber
{
  public void Publish(Dictionary<string, object> message)
  {
    GlobalGameState["GuardOnDuty"] = (bool)message["result"];
  }
}

We will also need to create the Guard NPC. It is just a class which extends the existing Character class. The only new thing here is the DutyStatus method which we can use to detect whether or not the guard is still on duty. It will just read from the global game state.

private class Guard : Character {
  public Guard(string name) : base(name)
  {
  }

  public bool DutyStatus()
  {
    if ((bool)GlobalGameState["GuardOnDuty"])
    {
      Console.WriteLine("I'm too thirsty to move...");
    }
    else
    {
      Console.WriteLine("Ah, that hits the spot. Oh you need to get in? Sure, I'll move!");
    }

    return (bool)GlobalGameState["GuardOnDuty"];
  }
}

Finally, let's write our test.

[Fact]
public void TestSodaPop()
{
  Console.WriteLine("--- Soda Pop Test --- ");

  itemsDbReader = new ItemsDbReader();
  testUtils = new TestUtilities(itemsDbReader.ReadData<Item>(DbConstants.ItemsDbFile));

  inventoryManager = new InventoryManager();
  party = new Party(inventoryManager);

  Character terra = new Character("Terra");
  party.AddMember(terra.Name, terra);

  Guard guard = new Guard("Guard");

  var sodaPop = testUtils.SodaPopItem();
  var subscriber = new SodaPopDrinker();

  Assert.True(guard.DutyStatus());

  sodaPop.Register(subscriber);

  party.ReceiveItem(sodaPop, ItemReceiveAction.Purchase);
  Assert.Equal(1, party.QueryFor(TestUtilities.SodaPop).Quantity);

  party.SetActiveCharacter(terra.Name);
  party.UseItem(TestUtilities.SodaPop, guard);

  Console.WriteLine("You gave a drink to the thirsty guard on duty.");
  Assert.False(guard.DutyStatus());

  Assert.Equal(0, party.QueryFor(TestUtilities.SodaPop).Quantity);
}

In summary, a Guard NPC is created, but is not added to the party. Terra receives a Soda Pop, and interacts with the guard. The guard says he is thirsty. The player then applies the Soda Pop onto the guard which ultimately triggers the guard to flip the global gamer state of being on duty to false. This then signals that the guard is no longer thirsty.

Running the test, the output is shown below.

--- Soda Pop Test --- 
I'm too thirsty to move...
You gave a drink to the thirsty guard on duty.
Ah, that hits the spot. Oh you need to get in? Sure, I'll move!

Conclusion

Well, I can say that I am quite satisfied with what has been done over the last seven chapters of writing about a JRPG Inventory System. In the span of all these discussions we have been able to code a Rule of 99 JRPG Inventory System from scratch with the following features:

  • Standard JRPG-esque inventory management: apply items onto party members, receive items, sell items, equip and unequip tools, armor, and weapons
  • Allow items to be procedurally generated for almost infinite different types of items which all have different status effects upon use.
  • Create special items which can trigger different types of events throughout the game -- altering game state, or internal state which can allow for more immersive and non-linear quests.

We have been able to get all that we have planned done. What's the overall picture like now?

UML of the JRPG Inventory System

Full Resolution

I can say that I am pretty proud of this work. Thanks for being patient, and I hope that you have enjoyed reading my thoughts and adventures in creating a JRPG Inventory System.

So what's next? It's not over, right? Of course not.

I'm thinking about starting a new Design Doc series for Character Management. Who wants to read that? Hah! I don't care, I'm going to write about it anyway!

See you soon!