Design Docs - Thinking about Inventory - Part 1

I've beene resuming some hobby projects I've put in my backburner for a while now. Most of my time has been split with focusing intensely on NesThing since March 2019, and other life things that have happened since then such as getting married, and finding a new job. It's been really busy.

I still have dreams to make a video game. I was trying pretty hard up until January 2019, but I had to put everything aside because again, life happens. 😄

I'm not trying to get back into the swing of things and have been thinking about a design for an inventory system for a potential JRPG. Nothing silly and quickly done like the last time I had attempted one. I'll try a little harder this time... I swear!

The discussion is going to be pretty limited in scope. I'm actually still thinking about everything as I am writing this. I mean, I can wait for a design to be solidified before I type anything up, but I think it's nice for everyone to sort of read my thoughts in "real-time" as I go through all this material in my mind.

I'm only going to go as far as how the inventory system will be architected, and there won't be any code for now, at least in this post.

The Inventory System

Anyway, inventory for a JRPG! Let's talk about that. When I say JRPG, I'm usually referencing games in the style of Chrono Trigger, Final Fantasy, etc. It's not necessarily limited to 16-bit Super Nintendo ones, but basically anything similar to these. These RPGs generally have a common inventory management system in which this article describes as the Rule of 99.

Chrono Trigger Inventory

Chrono Trigger Inventory (Super Nintendo)

 

Final Fantasy 8 Inventory

Final Fantasy VIII Inventory (PlayStation)

 

The Rule of 99 is an inventory system that generally best fits a JRPG due to the following criteria:

  1. Items are all shared within members of a party.
  2. There will be many different classes of items which can be equipped, or applied onto a character. In addition, there will be many types of items within these classes.
  3. The number of items in which the party carry do not dictate the behavior of the the character within the game. For example, having more items does not affect the movement speed of a character.
  4. Large quantities of a specific item is needed to successfully progress through an area of the game. For example, think dungeons, boss battles, etc.
  5. The user interface will be, or is intended to be text heavy when it comes to managing the inventory. (Listing, organizing, using, dropping items, etc.).

Most importantly, the most attractive criteria for me is that:

  1. It is generally straightforward to implement a Rule of 99 system once a design has been established.

The last point is precisely what I am trying to achieve. The goal is to be able to come up with a reasonable architecture for a Rule of 99 inventory system, so that implementation and maintenence will eventually be manageable.

I am also trying to avoid overengineering, but at the same time, avoid complexity on how effects of inventory actions are applied within the game. Sounds kind of a lot of pie-in-the-sky stuff. Can I really do it?

I don't know, but at least try! 😄

Requirements

Generally, item sin JRPGs tend to fall into a few categories:

  • Consumables - healing, ingredients, one time buffs, ammo, etc.
  • Equippables - armor, weapons, accessories
  • Keys - not literally "keys" but items which are unique to the game and are required for proper progression

Thinking in an object-oriented manner, we can somehow model all this into a generic Item class and have each individual type dervice from this type. At least, that's the temptation.

Instead, I am going to try a different approach. I am going to explain the system from the top-down. What I intend to do is explain the system from how items are managed first, and eventually progress to the specifics on how each individual "concrete" item behaves.

I believe that an inventory system can be abstracted into a "fat" manager class that can be instantiated to handle requests generated through game actions. These requests are handled using character information to perform operations on the character using a requested item.

Here are some basic requirements I can think of:

  1. Players should have their character be able to use an item within the party's inventory.
  2. Players should also be able to drop items from the party's inventory.
  3. Players should also be able to place items into their inventory through loot, purchase, or as rewards such as quest achievements, or gifts.
  4. The UI should tell the player what items are in the inventory. How many of each item? What does it do? These are some things that can be answered through the inventory manager.

If we think about all these operations, then it makes sense that our inventory manager must contain some internal registry of all the items in the inventory.

Item Registry

The registry can be modeled as a Dictionary, or map-like data structure that can use the item name as a key to reference all other pertinent information about the item within the context of the party.

An ItemName type can simply be a class with a ToString method to obtain the proper key name.

Here is an example in very pseudo-pseudo code. Yes I know I said no code earlier, but pseudo-code doesn't count, right? Okay, I just couldn't help myself.

class ItemName {
	public string ToString() { ... }
}

Each reference to the registry, given a key can give us baack an ItemData object. For now, let's say that the ItemData just gives us back the quantity of that item available in the inventory along with a factoryInstance to create an instance of that item available for consumption.

class ItemData {
	public int Quantity() { ... }
	public ItemFactory FactoryInstance() { ... }
}

Since we have a factoryInstance that can be referenced, we can expose some sort of get method within this factory to generate an instance of some item which can be applied onto a character.

Alright, now that we have some sort of "globally" accessible space in which all characters in a party can reach into to manipulate items, let's now think about how interactions between the player and inventory system can work. We want the characters to process these items intelligently.

Actions

To keep the scope simple and small, let's say the player is only allowed to perform the following actions on items in the game:

  1. Use an item. Characters can use an item on temshelves, or on another character, and at a specific location on that character. For example, equipping an item will need the location of the destination character to be applied.
bool Use(ItemName name, Character srcChar, Character destChar, BodyLocation destLoc)
  1. Drop an item. Characters can drop an item not begin equipped, or heled onto by another character from the inventory. This can be something akin to dropping the item to organize the inventory, or the action of selling an item in town.
bool Drop(ItemName name)
  1. Restore an item into the inventory. In case if the item can be held onto by another character, the item can be placed back into the inventory. This can be an action such as unequipping the item from a character. We can also "undo" any statistic effects on the character if the item had served that purpose.
bool Restore(ItemName name, Character srcChar)
  1. Acquire an item. Items can be bought, found, stolen, etc. We want to be able to register the item into the inventory.
bool Acquire(ItemName name)

Querying

Aside from all the actions, The player should know on-demand what is in the inventory. The UI can invoke a request to obtain item information within that inventory. But what does that information consist for each individual item? We can make an ItemInfo class that exposes that:

class ItemInfo {
	public ItemName Name() { ... }
	public int Quantity() { ... }
	public string Description() { ... }
}

Exposing a simple QueryAll API definition to give us back a list of ItemInfo contained within the registory is sufficient for now.

The InventoryManager Class

Putting all this information together, we now have a basic InventoryManager class which can be initialized to manage the inventory of the game.

class InventoryManager {
	private Dictionary<ItemName, ItemData> registry;
	public List<ItemInfo> QueryAll() { ... }
	public bool Use(ItemName name, Character srcChar, Character destChar, BodyLocation destLoc) {... }
	public bool Drop(ItemName name) { ... }
	public bool Restore(ItemName name, Character srcChar) { ... }
	public bool Acquire(ItemName name) { ... }
	
	// Utility
	public bool HasItem(ItemName name) { ... }
	public bool CanUse(ItemName name, Character destChar) { ... }
}

Of course, all this is flexible and can possibly change. Things haven't been finalized yet such as the return values for some of these methods.

Looking at the above, there are still a few things we need. As you can see, our instances of Character are pretty tightly coupled with our inventory management system. I think this is ok... for now.

IItem Interface

The item factory instance's purpose is to instantiate a concrete implementation of some IItem object. For now, let's just say an item can be verified for a character through:

boolean CanApply(Character destChar)

and applied through:

boolean Apply(Character destChar, BodyLocation destLoc)

We now have a simple interface:

interface IItem {
	boolean CanApply(Character destChar);
	boolean Apply(Character destChar, BodyLocation destLoc);
}

We'll assume the following pseudo-code when consuming the item:

method call: srcChar.UseItem(itemName, destChar, destLoc)

	if(inventoryManager.HasItem(itemName) && 
		inventoryManager.CanUse(itemName, destChar)) {
		inventoryManager.Use(this, destChar, destLoc);
		return true;
	}
	return false;

Should a character need to place an item back, we need to just implement some sort of logic liek this:

method call: srcChar.RestoreItem(itemName)

	inventoryManager.Restore(itemName);

Well now, we've hit a problem! Restoring an item needs to not only place the item back into the inventory, but the item instance itself needs to have logic that will perform an action after being placed back into the inventory. Maybe having a hook to be invoked by the inventory manager helps?

bool AfterRestore(Character destChar)

Then the updated interface can be like?

interface IItem {
	boolean CanApply(Character destChar);
	boolean Apply(Character destChar, BodyLocation destLoc);
	bool AfterRestore(Character destChar);
}

Yes, the above is still a WIP in my head, but for now, let's leave it as that.

Closing Thoughts

That's about it for now. Some outstanding questions are:

  1. How are the item factory instances supposed to generate item instances? Where do the parameters come from to construct these instances?
  2. Equipping items is still unclear. Do characters maintain a "subset" of the inventory in their object instances. Is it tied to a body location key within the character instances?
  3. The events that take place after an item has been used, dropped, restored, acquired, etc still isn't super clear. How does it all work at the implementation level?

I'll take some time to think about all these, and in the next post, we'll explore together some of the possible solutions to answer those questions.