Making a Hack’n’slash #13: Creating items, filling the inventory & stacking

Let’s continue our Hack’n’slash and start working on our inventory system!

This article is also available on Medium.

In the previous tutorial, we started to work on our inventory system. This RPG component is inspired by the Diablo hack’n’slash games and it is a core element of our game. In the end, it will also be linked to the loot system (so that we can add items to the inventory that are taken from the various enemies and monsters we kill) and the equipment system (so that the hero’s stats and abilities change if we loot and use more powerful items).

But, for now, we’re not ready to create and link those other systems yet. At the moment, we’ve just prepared the UI for the inventory panel:

Today, we need to add some logic so that we can define items (i.e. the item data that describes the various weapons, armours or components we want to appear in the game) and fill the hero’s inventory with those while making sure to stack them as much as possible…

Defining the items with ScriptableObjects

To begin with, let’s create some items for our game! To do this, we’re going to have the same mindset as for enemies: we will use Scriptable Objects to have various “reference assets” in our project that we can then “instantiate” as actual items.

So, basically, we’ll have:

  • an InventoryItemData C# class to specify which properties an item has
  • an instance of this class as a Scriptable Object; for example, the “Apple” asset, that determines the name, code, icon, etc. for all apple items
  • one or more instances of this Scriptable Object in game; for example, we could have 10 apples in our inventory

Let’s work through this step by step to better understand this chain of instances 🙂

Preparing the base C# class

Ok – first of all, we need to create this InventoryItemData class. It will inherit from the ScriptableObject class and determine the fields all items should have at minimum: a unique code, a display name, an icon, a weight and a price.

Note: about the price – for now, I’ll keep things simple and consider that we have just one “gem” currency, so there isn’t any conversion between copper, silver and gold coins for example.

There is not much news here, the script is very similar to the EnemyData we made previously:

Now that we have this script, and again because we set up a [CreateAssetMenu] attribute, we can create new instances to have some item data:

This data class is very basic, but it already allows us to define a grand variety of items – thanks to the power of Scriptable Objects! 😉

But we’re not done yet with defining our items properties…

Adding item rarities

As I mentioned last time, another feature I want for my items is the notion of rarity; like in Diablo or World of Warcraft, the world is filled with more or less rare (and therefore powerful) items that can easily be ranked, compared and traded.

More precisely, I want to have the same rarity levels as in World of Warcraft, except for the gray trash items:

  • common (colour: ‘white’): those items are pretty much everywhere – they’re usually not worth a lot, but you’ll find a lot of them during your journey and ultimately they’re better off sold to a merchant in exchange for gems
  • uncommon (colour: ‘green’): as you travel, chances are you’ll also encounter slightly more powerful items – those are not heroic-grade yet, but they’re always a bit better than common ones
  • rare (colour: ‘blue’): from time to time, you might find a rare item that is worth even more and probably has better characteristics overall – you’re starting to deal with real stuff, there!
  • epic (colour: ‘purple’): a reward for your patience – those items won’t show up often, but they’re absolutely worth the wait 🙂
  • legendary (colour: ‘orange’): with great power, comes great rareness – those extra-mighty items are the amazing loot every adventurer dreams of… but not many get!

What I call “rare” here, from our developer’s point of view, roughly means: “which has a very low chance of being dropped by enemies”. We won’t get into the whole details of drop rates and items in-game creation for now (we’ll explore this later on when we design and implement the looting system), but this idea of rarity is at the core of most of Diablo-like games.

Note: if you want more info on how these games use rarity and low-drop rates to hook us in, have a look at this video I made on the topic! 😉

What I call “powerful”… well, that’s up to the type of item we’re dealing with – we’ll discuss this shortly! But all in all, it’s about boosting the stats, adding some extra abilities, etc., so that the item makes your hero more apt to fight the bigger enemies.

Since we have a list of possible values for rarity, we’re going to implement it using a C# enum. This way, we won’t run the risk of misspelling something and we’ll be able to quickly check the available rarities.

We can also define the associated colours in a static C# Dictionary (so that it’s accessible directly from the other classes).

By default, items will take the first value in the list, i.e. the “Common” rarity.

Adding item types

But of course, not all items can fully defined with just those properties! If you think about weapons, or armours, you’ll quickly find some extra stats that you’ll want to add to the item data (e.g. the base attack damage, the defence score…).

So for now, let’s assume we have four types of items: the weapons, the armours, the components and the misc(ellaneous). I can already define them with another enum and add this info to my InventoryItemData properties:

But now: how can we implement the various item types and have those extra properties where needed?

Well – this is actually a good opportunity to use C# inheritance feature and have our InventoryItemData be a parent class for several other more “specialised” classes. This way, we’ll have a bunch of shared properties (and potentially behaviour) thanks to this “common ancestor”, but we’ll also have a way of setting up some type-specific data.

For example, let’s take care of the weapons first.

Here, I want to create a new C# class, WeaponItemData, that inherits from the InventoryItemData class, and that extends its properties and logic. In particular, I want my weapons to have some base damage value, a variability on this damage and a chance of critical strike.

Also, I obviously want these items to have the ItemType.Weapon type, which I can set in the class’s constructor.

Important note: make sure that you define this class in a C# script with the same name (i.e. WeaponItemData.cs), otherwise Unity will not be able to create Scriptable Objects from it!

Thanks to this new “Scriptable Object template”, I can now create items that are specialised as weapons and that have those extra fields:

Another really cool thing is that we can define type-specific behaviours, for example here some functions to compute the weapon’s actual damage from its base damage, its damage variability and its critical strike chance.

Of course, it’s pretty much the same for the armours: we need to create an ArmorItemData class that inherits from the InventoryItemData, auto-sets the item’s type, defines some extra fields and optionally has some additional methods, too:

For the component items, I haven’t decided on specific fields or logic yet; so the ComponentItemData class will simply change the default for the item’s type:

Finally, the misc items will just be instances of the base class, InventoryItemData, with no further specialisation.

With all this ready, we can now super-easily create item Scriptable Objects and populate our game data with various weapons, armours, components and other miscellaneous objects!

As usual, you can check out the Github repo of this series for more details 🚀

Implementing a basic inventory

At this point, we can prepare a set of items… but we can’t actually see any of these in-game! So the next step is to start coding up a simple inventory to store and show (instances of) these items 🙂

For now, I’ll ignore stacking and focus on filling the slots and displaying the updated inventory panel in the UI.

Setting up an inventory

First, let’s create a new C# script, the InventoryManager, and add it to our “MANAGER” object (the one we created in a previous tutorial to hold our AddressablesLoader script instance). Since we have just one hero, and the entire game revolves around him, we can sort of “confound” what is hero- and game state-dependent; so we can use this global manager object for our hero inventory as well.

The InventoryManager will handle both the logic and the UI – so we’ll want to keep track of the data in the hero’s inventory, and we’ll also want to sync the UI panel to match this data.

For the logic part, we’ll mostly rely on a C# Dictionary data structure: this will allow us to store items in (potentially) non-contiguous slots without having to artificially fill the empty slots with some null values. So the trick will be to map slot indices to slot data, by having Dictionary key-value pairs of intInventorySlot type.

This InventorySlot is a custom class that we’ll use to easily pair together the item data and the amount of instances in this slot:

Since it’s used just by this script, we can define the class inside the InventoryManager and keep it private – good data encapsulation like this insures that other unrelated scripts don’t access things they’re not supposed to, and it makes the whole codebase more robust.

Note: for those a bit more familiar with C#, we could use a struct or a tuple but those aren’t mutable so it will complicate things when we just want to change the amount of items in one slot 😉

The inventory itself is then, as we said before, a Dictionary that uses this new type, and that we’ll fill thanks to an AddItem() method:

By default, we’ll add one instance of the given item but we can also pass in a specific amount.

Adding items

Now – remember that we’re ignoring the item stacking mechanics in this section. In other words, to add an item, we have to handle two cases:

  • if the item is not yet in the inventory Dictionary: we create a new InventorySlot and put it in the first available slot
  • if the item is already in the inventory Dictionary: we increase its amount

In both cases, we’ll also have to update the UI accordingly.

This logic is actually pretty straight-forward to translate in C#. We just have to make sure that, if items of the same type were already there, we don’t add the item twice by filling the pre-existing slot and adding a new one; to do this, I’ll simply reset the amount to add to 0 when I’m done filling an already-existing slot.

Now, all that’s left to do is to take care of the UI!

Besides adding a few references to UI elements in my Canvas, I’ll define two functions, _UpdateGridItems() and _SetGridItem() to re-update either the entire set of slots or just one specific slot (specified by its index in the grid).

Again, the logic here is fairly easy: the _SetGridItem() will change the icon’s sprite, the text and the rarity overlay if need be; then, the _UpdateGridItems() will simply call this method over the full grid.

I’ll also code an _UnsetGridItem() method to be able to quickly “clean” the inventory UI before re-filling it.

Note: of course, don’t forget to actually drag the UI object for the _itemsGrid to the script in the Inspector 😉

Then, the AddItem() method will call the _SetGridItem() function when need be:

Testing this out

We’re not fully done with adding items – we still need to take into account the weight of the items, for example. But, for now, let’s test this first version! 🙂

To be able to open and close my panel, I’ll need to define some shortcut. I’ll add it directly in my Update() method – this is just for the tests right now, we’ll see in an upcoming episode how to toggle the panel with cross-platform inputs 🙂

So let’s say I want to show or hide the panel whenever I press the <I> key. This means I just need a reference to my inventory panel game object, and I’ll check for the key in the Update() (using the new input system):

Now, I can press the key to toggle the panel on or off… but it’s empty!

We’re going to fake some loot by adding a few “test items” references, and populating our inventory with various amounts at the start:

You can pick whichever item you want among the different InventoryItemData (and derived-classes) Scriptable Objects you prepared 🙂

And here we are! If we run the game and press the <I> key, we can now show or hide the panel and see the various items inside it with the icon, the amount (if there is more than one) and the rarity overlay if it’s not a common item.

Stacking items of the same type

The last thing I want to discuss today is how we can handle stacking on our objects. This is something very frequent with inventories that are based on slots, like this: depending on the type of the item, you can only have up to a given amount in one inventory slot, and then the rest of the items have to be stored in one or more other slots.

This is a (loose) way of making the inventory a bit more realistic – usually, small or light items like food, plants, etc. can be stacked into quite big piles while heavy or big objects such as weapons or armours take one slot per instance.

Let’s add this feature to our inventory!

Updating the item data classes

The first step is to add a property to our items to know how much can be stacked before creating a new slot:

For the weapons and the armours, I’ll auto-set this property to 1 by default (but it can of course be changed easily for “special cases”).

Now, the Inspector allows me to quickly define my stack sizes:

Making stacks when we add items

Then, we need to use this property when we add items to know whether the stack is already full or not, and whether or not we have to create new slots.

The overall logic will remain the same: we’ll differentiate between the case where items of the same type are already present in the inventory, and where there is no other items of the same type. However, we’ll also need to try and “distribute” the items into stacks so that the slot amounts don’t overshoot this new maxStackSize threshold.

I want the stacking to follow these two rules:

  • I’ll always try and fill the pre-existing slots first
  • when I fill a new slot, I’ll choose the first available cell

Distributing items into stacks isn’t too hard – I can simply take my total amount and my stack size, and then gradually “transfer” the amount into a list of (integer) slot amounts:

Each integer in this list is the count associated with one slot. For example, if my _DistributeItems() function outputs a list with {20, 20, 17}, then it means I should end up with one slot holding 20 instances of my items, a second (new) slot again holding 20 instances and finally a third (new) slot holding 17 instances.

The first slot can either be a pre-existing one (if there were already items of the same type) or a new slot as well.

To make sure I get the right index for the new slots (i.e. the first available cell), I’ll also need a util function. Indeed, this index is not necessarily the _inventory.Count because I’ll want the players to be able to drop and/or reorganise items, so there might be “holes” in the items grid… which means I need to go through the grid and check which of my slots is the first to be empty!

Finally, thanks to those two functions, I can update my AddItem() method and inject my stacking feature! The only thing to keep in mind is that you can’t iterate through and modify a Dictionary at the same time; so I have to create a copy of my original slots so that I can modify the actual _inventory variable in the loop.

If I re-run my game with the same test items as before, you see that now the swords are spread into multiple slots, because they have a maxStackSize of 1:

Conclusion

Today, we’ve continued our inventory system – we can now define items, add them to the inventory and stack them!

Next time, we’ll keep going and see how to navigate through this grid of items both on PC and console…

Leave a Reply

Your email address will not be published.