Slot Based Inventory in Unity and UNET – Part 1

September 11, 2017 9:23 am Published by

Table of contents

  1. Requirements
  2. Introduction to the series
  3. Before we start: design patterns and decoupling
  4. BasicItem
  5. BasicContainer
  6. EntityInventory
  7. ContainerWindow
  8. Conclusion and downloads

1.Requirements

Throughout this tutorial, at least basic familiarity with the following aspects of the Unity engine is assumed:

2. Introduction to the series

One of the most important systems inside an RPG game is the inventory. While creating RogueCraft, I’ve rewritten it so many times, it’s still giving me nightmares. Mostly, because It’s extremely hard to predict exactly what you’ll need from the get-go. Here, more than anywhere else you need to adhere to design patterns and decouple, decouple, decouple… (if you don’t know what these terms mean, read Chapter 3).

In this tutorial we will learn  how to write a complete slot-based inventory system for a basic RPG game, with main features being:

  1. It should sync up in multiplayer (UNET)
  2. We will allow containers inside containers
  3. The same classes will be used for items in-game

In this part we will learn about design patterns and write our basic item, container and UI classes.

3. Before we start: design patterns and decoupling

The term that is crucial to us is decoupling, which means the need to limit the degree of dependency between one class and another. Why is it a good thing? Imagine a single class for your player’s inventory, named PlayerInventory. It has a method OnUse, called when a player double-clicks the item. Let’s say that after some time, you want to add an NPC with their own equipment. So what do you do? Write a MonsterInventory class which has the same logic as PlayerInventory but doesn’t reference the UI elements? That is some bad programming right there, as you should always follow the rule of never repeating yourself.

Another, real-life example. In RogueCraft, I wrote the basic item class (BasicItem) together with the logic for the player backpack (UI Container). At that point I hadn’t even thought about the way items would be handled in the game-world (represented by the WorldItem class).

Each item had a variable that represented a customizable color. When a player applied some dyes, it would change that variable and paint the sprite accordingly in the UI. Ok, but what happens when the same dye is applied to an item in the game-wrold and not in the player backpack? It should color its model, and not the sprite. I had to write a method OnChangeDetailColorInTheWorld in BasicItem that would point to the WorldItem class for a reference to the model. This is a prime example of coupling, increasing the depndency between all these classes. Something that should be avoided because basic classes should be self-contained and not concerned with the logic of the UI system, AI, Networking code, etc.

At first, it might not be obvious why such approach would be benefitial. Why should you bother? For basic applications it would be acceptable to write a single class that would handle literallly everything, but when in the next parts of this tutorial we will write mechanics for containers within containers, inventory windows for your pets, equippable items – it will become evident, trust me!

How to fix it? How to write decoupled code? With design patterns.

Now, a design pattern is a solution to a problem that is reapplicable to other situations. It’s a way of approaching good programming standards and your code’s architecture. And as I said before, since an inventory system in an RPG is a complex web of interconnected classes, we should really familiarize ourselves with the best possible practices of organizing our code and expanding its flexibility and reusability.

Here is a good rundown on design patterns in Unity.

The way we will decouple our inventory system is through the use of an Observer Pattern and UnityEvents. The observer pattern lets you inform the clients of the class (classes which use more basic objects, for example PlayerBackpack would be a client of BasicItem) about changes or events without a direct reference in the basic class. Using the example above, our BasicItem class would have a customizable color property that would invoke OnDetailColorChange event, to which the UI Container and WorldItem components would listen, and color the sprite or the model respectively without contaminating the basic class with their logic.

4. BasicItem class

Let’s start by creating a new project in Unity and set up the following directory structure for our scripts:

Inside that folder let’s create a BasicItem script that will define the basic properties of every item through the BasicItem class (plain class, not MonoBehaviour). You can see that we use properties (getters and setters) together with UnityEvents to implement the observer pattern. When each property changes, it triggers an appropriate event that would be later used by the clients of the class. Let’s start with the name of the item:

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

// The class that holds all the definitions of an item and events invoked when these change
public class BasicItem {

    #region properties

    private string name; // the name of the item
    public UnityEvent OnNameChange = new UnityEvent(); // the event invoked when the property changes
    public string Name // getters and setters of the property which trigger the event
    {
        get { return name; }
        set { name = value;  OnNameChange.Invoke(); }
    }

    #endregion

}

Now let’s add more properties of the item: its graphic, max amount, current amount and durability.

private string name; // the name of the item
public UnityEvent OnNameChange = new UnityEvent(); // the event invoked when the property changes
public string Name // getters and setters of the property which trigger the event
{
    get { return name; }
    set { name = value;  OnNameChange.Invoke(); }
}

private string graphic; // a string that holds the name of the graphical representation of the item: Sprite in the UI or the model in game-world
public UnityEvent OnGraphicChange = new UnityEvent(); 
public string Graphic 
{
    get { return graphic; }
    set { graphic = value; OnGraphicChange.Invoke(); }
}

private ushort maxAmount; // number that represents the maximum amount of an item in one stack (like in Minecraft)
public UnityEvent OnMaxAmountChange = new UnityEvent();
public ushort MaxAmount
{
    get { return maxAmount; }
    set { maxAmount = value; OnMaxAmountChange.Invoke(); }
}

private ushort amount; // number that represents amount of an item in one stack
public UnityEvent OnAmountChange = new UnityEvent();
public ushort Amount
{
    get { return amount; }
    set { amount = value; OnAmountChange.Invoke(); }
}

private float durability; // number 0-1 that represents the durability
public UnityEvent OnDurabilityChange = new UnityEvent();
public float Durability
{
    get { return durability; }
    set { durability = value; OnDurabilityChange.Invoke(); }
}

The next step is to add basic constructors, so our finished BasicItem class should look like this:

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

// The class that holds all the definitions of an item and events invoked when these change
public class BasicItem {

    #region properties

    private string name; // the name of the item
    public UnityEvent OnNameChange = new UnityEvent(); // the event invoked when the property changes
    public string Name // getters and setters of the property which trigger the event
    {
        get { return name; }
        set { name = value;  OnNameChange.Invoke(); }
    }

    private string graphic; // a string that holds the name of the graphical representation of the item: Sprite in the UI or the model in game-world
    public UnityEvent OnGraphicChange = new UnityEvent(); 
    public string Graphic 
    {
        get { return graphic; }
        set { graphic = value; OnGraphicChange.Invoke(); }
    }

    private ushort maxAmount; // number that represents the maximum amount of an item in one stack (like in Minecraft)
    public UnityEvent OnMaxAmountChange = new UnityEvent();
    public ushort MaxAmount
    {
        get { return maxAmount; }
        set { maxAmount = value; OnMaxAmountChange.Invoke(); }
    }

    private ushort amount; // number that represents amount of an item in one stack
    public UnityEvent OnAmountChange = new UnityEvent();
    public ushort Amount
    {
        get { return amount; }
        set { amount = value; OnAmountChange.Invoke(); }
    }

    private float durability; // number 0-1 that represents the durability
    public UnityEvent OnDurabilityChange = new UnityEvent();
    public float Durability
    {
        get { return durability; }
        set { durability = value; OnDurabilityChange.Invoke(); }
    }

    #endregion

    #region constructors 
    
    // Basic constructor without parameters
    public BasicItem()
    {
        Amount = 1;
        MaxAmount = 64;
        Name = "Unnamed Item";
        Graphic = "Unknown";
        Durability = 1.0f;
    }

    // Main constructor with basic parameters
    public BasicItem(string itemName, string itemGraphic)
    {
        Amount = 1;
        MaxAmount = 64;
        Name = itemName;
        Graphic = itemGraphic;
    }

    #endregion

}

We have created the BasicItem class that follows the observer pattern, now it’s the time to write the first client of that class, a simple container.

5. BasicContainer

While BasicItem represents a single item, BasicContainer will represent a collection of items, be it a player’s inventory, a bag with more items in it, or an NPC’s equipment. Again, following the observer pattern, the BasicContainer class will be a client of BasicItem, making use of all the events that we defined as triggered in the BasicItem setters. Let’s start by creating BasicContainer script inside the Scripts/Inventory folder . As with BasicItem, BasicContainer is a plain class and not a MonoBehaviour as it won’t be directly added to game objects as a component. To hold references to the items present in the container, we need to define a Dictionary, with its key being an integer representing the slot index. We allso add a variable representing the total number of slots present in that container and an event that will be invoked when the inventory changes (this will be later used by the clients of BasicContainer, for example to redraw the inventory window when the player picks up an item):

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

public class BasicContainer {

    #region events

    public UnityEvent OnInventoryChange = new UnityEvent(); // an event that is invoked whenever the container changes (added, deleted items, etc.)

    #endregion

    #region private

    private Dictionary<int, BasicItem> items = new Dictionary<int, BasicItem>(); // collection of all items present in the container, the key being the slot index
    public int slotAmount = 20; // total number of slots in the container

    #endregion

}

 

BasicContainer will be responsible for the logic of a simple container: adding items, deleting them, or moving them between containers. To make that easier, let’s define some helper fuctions to find empty slots, clear slots, find items with a given name, or items of a given slot index:

#region helperfunctions

// Finds the first empty Slot and returns its index, or -1 when there is no free slot
public int FindEmptySlot()
{

    for (int i = 0; i < slotAmount; i++)
    {
        if (!items.ContainsKey(i))
        {
            return i;
        }
    }

    return -1;
}

//Finds the first item of a given name, or returns null if there is no such item
public BasicItem FindFirstItemOfName(string itemName)
{
    BasicItem item = null;

    foreach (KeyValuePair<int, BasicItem> entry in items)
    {
        if (entry.Value.Name == itemName)
        {
            item = entry.Value;
            return item;
        }

    }

    return item;
}

// Returns the item of a given slot index, or null if such item doesn't exist
public BasicItem GetItemOfSlotIndex(int slotIndex)
{
    BasicItem item = null;

    if (items.TryGetValue(slotIndex, out item))
    {
        return item;
    }

    return item;
}

// Clears the slot and its entry in the dictionary
public void ClearItemOfSlot(int slotIndex)
{
    BasicItem item = null;

    if (items.TryGetValue(slotIndex, out item))
    {
        items.Remove(slotIndex);
        item = null;
        OnInventoryChange.Invoke();
    }
}

#endregion

For now, we will add the ability to add just a single stack of an item to the container; it must first find an empty slot and add a reference to the BasicItem class to the inventory dictionary and invoke the OnInventoryChange event:

#region inventory manipulation

// Adds an item as a single stack, and returns true if it was successful
public bool AddItemAsASingleStack(BasicItem item)
{
    int iEmptySlot = FindEmptySlot();
    if (iEmptySlot == -1) return false; // if there are no free slots, do nothing
    else
    {
        items.Add(iEmptySlot, item);
        OnInventoryChange.Invoke();
        return true;
    }
}

#endregion

Let’s assume something changes the item properties while it’s residing inside the container, for example the item’s durability when a player uses that item. How can we communicate that change to the clients of the BasicContainer class (for example, the player inventory window)? We need to hook up the events from the BasicItem class to trigger the OnInventoryChange event of BasicContainer, whenever we add or manipulate an item in the inventory. Let’s write a method for hooking these up:

// Through this method, whenever an item properties change the container invokes the OnInvenotryChange event for its clients
private void HookUpItemEventsToContainer(BasicItem item)
{
    // Remove previous listeners if they exists
    item.OnNameChange.RemoveAllListeners();
    item.OnMaxAmountChange.RemoveAllListeners();
    item.OnGraphicChange.RemoveAllListeners();
    item.OnDurabilityChange.RemoveAllListeners();
    item.OnAmountChange.RemoveAllListeners();

    item.OnNameChange.AddListener(delegate { OnInventoryChange.Invoke(); });
    item.OnMaxAmountChange.AddListener(delegate { OnInventoryChange.Invoke(); });
    item.OnGraphicChange.AddListener(delegate { OnInventoryChange.Invoke(); });
    item.OnDurabilityChange.AddListener(delegate { OnInventoryChange.Invoke(); });
    item.OnAmountChange.AddListener(delegate { OnInventoryChange.Invoke(); });
}

Now add the HookUpItemEventsToContainer(item); inside the AddItemAsSingleStack method, right before it’s added to the dictionary.

The final thing we need to do is to add the ability to move items around between the slots in the container. For now let’s just handle the situation where an item is dragged to an empty slot (we will expand on this in the next tutorial). To do that, let’s define a static function:

// A method to move item between two slots of two containers
public static void MoveItemsBetweenSlots(int fromSlot, int toSlot, BasicContainer fromInventory, BasicContainer toInventory)
{
    if (fromSlot >= fromInventory.slotAmount || toSlot >= toInventory.slotAmount || fromSlot < 0 || toSlot < 0) return; // make sure the slot number is ok

    BasicItem fromItem = fromInventory.GetItemOfSlotIndex(fromSlot); // get the references to the item being dragged
    BasicItem toItem = toInventory.GetItemOfSlotIndex(toSlot); // get the reference to the item in the target slot

    if (fromItem == null) return; // if there is no item in the start slot, do nothing

    if (toItem == null) // if the movement is between an item and an empty slot
    {
        fromInventory.ClearItemOfSlot(fromSlot); // Clears the inventory slot the item was dragged from

        if (toInventory.items.ContainsKey(toSlot)) // if the target container has an item in the target slot, overwrite the reference
        {
            toInventory.items[toSlot] = fromItem; 
        }
        else // if it doesnt have an item in the target slot, add a new dictionary entry
        {
            toInventory.items.Add(toSlot, fromItem);
        }

        toInventory.OnInventoryChange.Invoke(); // invoke the event in the target inventory, as it changed

        toInventory.HookUpItemEventsToContainer(fromItem); // clear the events and hook them up to the new inventory
    }
    else
    {
        // change places or join stacks
        // will implement in the next part of the tutorial
    }

}

Thus, we created the BasicContainer class, a client of BasicItem, which handles all the logic behind inventory manipulation. In the next chapter we will use this class to set up a UI window representing a player’s inventory. Here is the final code for BasicContainer:

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

public class BasicContainer {

    #region events

    public UnityEvent OnInventoryChange = new UnityEvent(); // an event that is invoked whenever the container changes (added, deleted items, etc.)

    #endregion

    #region private

    private Dictionary<int, BasicItem> items = new Dictionary<int, BasicItem>(); // collection of all items present in the container, the key being the slot index
    public int slotAmount = 20; // total number of slots in the container

    #endregion

    #region helperfunctions

    // Finds the first empty Slot and returns its index, or -1 when there is no free slot
    public int FindEmptySlot()
    {

        for (int i = 0; i < slotAmount; i++)
        {
            if (!items.ContainsKey(i))
            {
                return i;
            }
        }

        return -1;
    }

    //Finds the first item of a given name, or returns null if there is no such item
    public BasicItem FindFirstItemOfName(string itemName)
    {
        BasicItem item = null;

        foreach (KeyValuePair<int, BasicItem> entry in items)
        {
            if (entry.Value.Name == itemName)
            {
                item = entry.Value;
                return item;
            }

        }

        return item;
    }

    // Returns the item of a given slot index, or null if such item doesn't exist
    public BasicItem GetItemOfSlotIndex(int slotIndex)
    {
        BasicItem item = null;

        if (items.TryGetValue(slotIndex, out item))
        {
            return item;
        }

        return item;
    }

    // Clears the slot and its entry in the dictionary
    public void ClearItemOfSlot(int slotIndex)
    {
        BasicItem item = null;

        if (items.TryGetValue(slotIndex, out item))
        {
            items.Remove(slotIndex);
            item = null;
            OnInventoryChange.Invoke();
        }
    }

    #endregion

    #region inventory manipulation


    // Through this method, whenever an item properties change the container invokes the OnInvenotryChange event for its clients
    private void HookUpItemEventsToContainer(BasicItem item)
    {
        // Remove previous listeners if they exists
        item.OnNameChange.RemoveAllListeners();
        item.OnMaxAmountChange.RemoveAllListeners();
        item.OnGraphicChange.RemoveAllListeners();
        item.OnDurabilityChange.RemoveAllListeners();
        item.OnAmountChange.RemoveAllListeners();

        item.OnNameChange.AddListener(delegate { OnInventoryChange.Invoke(); });
        item.OnMaxAmountChange.AddListener(delegate { OnInventoryChange.Invoke(); });
        item.OnGraphicChange.AddListener(delegate { OnInventoryChange.Invoke(); });
        item.OnDurabilityChange.AddListener(delegate { OnInventoryChange.Invoke(); });
        item.OnAmountChange.AddListener(delegate { OnInventoryChange.Invoke(); });
    }

    // A method to move item between two slots of two containers
    public static void MoveItemsBetweenSlots(int fromSlot, int toSlot, BasicContainer fromInventory, BasicContainer toInventory)
    {
        if (fromSlot >= fromInventory.slotAmount || toSlot >= toInventory.slotAmount || fromSlot < 0 || toSlot < 0) return; // make sure the slot number is ok

        BasicItem fromItem = fromInventory.GetItemOfSlotIndex(fromSlot); // get the references to the item being dragged
        BasicItem toItem = toInventory.GetItemOfSlotIndex(toSlot); // get the reference to the item in the target slot

        if (fromItem == null) return; // if there is no item in the start slot, do nothing

        if (toItem == null) // if the movement is between an item and an empty slot
        {
            fromInventory.ClearItemOfSlot(fromSlot); // Clears the inventory slot the item was dragged from

            if (toInventory.items.ContainsKey(toSlot)) // if the target container has an item in the target slot, overwrite the reference
            {
                toInventory.items[toSlot] = fromItem; 
            }
            else // if it doesnt have an item in the target slot, add a new dictionary entry
            {
                toInventory.items.Add(toSlot, fromItem);
            }

            toInventory.OnInventoryChange.Invoke(); // invoke the event in the target inventory, as it changed

            toInventory.HookUpItemEventsToContainer(fromItem); // clear the events and hook them up to the new inventory
        }
        else
        {
            // change places or join stacks
            // will implement in the next part of the tutorial
        }

    }

    // Adds an item as a single stack, and returns true if it was successful
    public bool AddItemAsASingleStack(BasicItem item)
    {
        int iEmptySlot = FindEmptySlot();
        if (iEmptySlot == -1) return false; // if there are no free slots, do nothing
        else
        {
            HookUpItemEventsToContainer(item);
            items.Add(iEmptySlot, item);
            OnInventoryChange.Invoke();
            return true;
        }
    }

    #endregion
}

6. EntityInventory

Now let’s finally create a MonoBehaviour, a component that will be added to all entities in the game which will be able to have an inventory (players, monsters, etc.). For now it just defines a BasicContainer, nothing fancy.

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

public class EntityInventory : MonoBehaviour {


    #region public

    public BasicContainer mainContainer = new BasicContainer();

    #endregion

}

Add this component to an empty GameObect called TestEntityInventory, like so:

Now, for the sake of testing, let’s add an OnGui method to EntityInventory, to be able to add some items, and a Start method to add a listener to the OnInventoryChange event of the main container – to see if what we have written so far is working.

void OnGUI()
{
    if (GUI.Button(new Rect(10, 10, 150, 100), "Add item"))
    {
        BasicItem item = new BasicItem("Test Item", "BasicIcon");
        item.Amount = (ushort)Random.Range(1, 10);
        item.Durability = Random.Range(0.1f, 1.0f);
        mainContainer.AddItemAsASingleStack(item);
    }
}

private void Start()
{
     mainContainer.OnInventoryChange.AddListener(delegate { Debug.Log("Inventory changed!"); });
}

Now start the scene and see if the debug message shows up.

7. ContainerWindow

Now for the most interesting part of this tutorial. Let’s display our inventory! To do this, we should create a ContainerWindow MonoBehaviour, create a ItemSlot MonoBehaviour and necessary prefabs. Let’s start with the ContainerWindow and define some basic references for each window:

#region references

[Header("Inventory References")]
public EntityInventory entityInventory; // reference to the inventory displayed in the window

[Header("UI References")]
public GameObject slotPanel; // a panel that holds all the slots

[Header("Prefabs")]
public GameObject slotPrefab; // a prefab of a slot

#endregion

Now create a UI Panel, set its anchor to center and set the width and height to something reasonable. Rename it to ContainerWindow and drag the ContainerWindow component onto it.

Now create another panel inside the ContainerWindow panel, rename it to SlotPanel, drag the SlotPanel to the UI References entry in the ContainerWindow panel, and add a Grid Layout Group component like so:

Every container will populate the SlotPanel with Slot prefabs.

Add yet another Panel element inside SlotPanel. Change its color to black. Inside, create an Image game object, rename it to ItemImage, Now create a script called ItemSlot and drag it to the Slot panel game object. Fianlly, adjust the ItemImage’s width and height to be slightly smaller than the main Slot element, like so:

Now add a Text gameobject inside the Slot, like this:

Now edit the ItemSlot script. This script is resposible for all the logic of handling a sinlge item slot and keeping references for the slot prefab:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
using System;
using UnityEngine.Events;

// A generic type for an event that uses two ItemSlots as parameters
// If you don't know what these are, read this: https://docs.unity3d.com/ScriptReference/Events.UnityEvent_1.html
[System.Serializable]
public class DoubleItemSlotEvent : UnityEvent<ItemSlot, ItemSlot>
{
}

// Same as above but for a single parameter
[System.Serializable]
public class SingleItemSlotEvent : UnityEvent<ItemSlot>
{
}

// The main class that implements Pointer events, these are used for mouse actions, such as dragging, clicking, etc.
// Read more here: https://docs.unity3d.com/ScriptReference/EventSystems.IPointerEnterHandler.html
public class ItemSlot : MonoBehaviour,  IPointerClickHandler, IBeginDragHandler, IDragHandler, IEndDragHandler
{
    #region public

    [HideInInspector] public int slotNumber; // the index of the container slot this slot prefab holds, set by the ContainerWindow class

    #endregion

    #region events

    public SingleItemSlotEvent OnDoubleClickItem = new SingleItemSlotEvent(); // an event that is invoked when you double click on a slot
    public SingleItemSlotEvent OnDropItemOutside = new SingleItemSlotEvent(); // an event that is invoked when you drag the item outside a ContainerWindow
    public DoubleItemSlotEvent OnDropFromSlotToSlot = new DoubleItemSlotEvent(); // an event that is invoked when you drag the item between two slots

    #endregion

    #region references

    [Header("Inventory References")]
    public EntityInventory inventoryReference; // a reference to an EntityInventory MonoBehviour which holds the item, set by the ContainerWindow class
    public BasicItem inventoryItemReference; // a direct refernce to a BasicItem class that resides in this slot, null if slot is empty, this is set by the ContainerWindow class

    [Header("UI References")]
    public Image imageIcon; // the icon UI Image game object of the prefab
    public Text amountText; // the UI text that displays the amount of the item

    #endregion

    private static Vector2 offset; // these is used later do calculate the offset needed when dragging the item
    private Vector2 originalPos;

    //What should happen when somone clicks the item in the slot
    public void OnPointerClick(PointerEventData eventData)
    {
        //What happens on double click
        if (eventData.clickCount >= 2 && eventData.button == PointerEventData.InputButton.Left)
        {
            OnDoubleClickItem.Invoke(this);
            return;
        }
    }
    
    // BeginDrag interface
    public void OnBeginDrag(PointerEventData eventData)
    {
        if (inventoryItemReference == null) return; // if an empty slot, no dragging

        originalPos = imageIcon.transform.position; // sets the offset and origional pos for dragging visual
        offset = eventData.position - new Vector2(this.transform.position.x, this.transform.position.y);
    }

   
    public void OnDrag(PointerEventData eventData)
    {
        if (inventoryItemReference == null) return; // if empty slot, no dragging

        imageIcon.transform.SetParent(gameObject.transform.parent.parent); // set the parent object so it appears above everything else
        imageIcon.transform.position = eventData.position - offset; // set the imageIcon pos so it appears next to the cursor
    }

    // Finish dragging
    public void OnEndDrag(PointerEventData eventData)
    {
        if (inventoryItemReference == null) return;  // if empty slot, no dragging

        List<RaycastResult> myResults = RaycastMouse(); // get the list of all the UI elements under the cursor

        bool wasOutsideOfUI = true; // have we found a UI element thats a slot or a container window?

        for (int i = 0; i < myResults.Count; i++) // go through the list of all object under the cursor
        {
            // get references to possible Container Winodws or Item Slots where we wish to drag the item
            ContainerWindow containerWindow = myResults[i].gameObject.GetComponent<ContainerWindow>();
            ItemSlot itemSlot = myResults[i].gameObject.GetComponent<ItemSlot>();

            if (containerWindow != null || itemSlot != null) // if there's a container or a slot under the cursor, don't drop the item
            {
                wasOutsideOfUI = false;
            }

            if (itemSlot != null) // we found a slot under the cursor, invoke the drop from slot to slot event, to which the ContainerWindow will subscribe
            {
                OnDropFromSlotToSlot.Invoke(this, itemSlot);
            }
        }

        if (wasOutsideOfUI) // if no ContainerWindow or ItemSlot components are found, it means we dragged the item outside the inventory
        {
            OnDropItemOutside.Invoke(this); // invoke the drop outside event
        }


        imageIcon.transform.SetParent(gameObject.transform); // return to the proper parent and position
        imageIcon.transform.position = originalPos;
    }


    // A static method that gets all the UI elements below the cursor, to see if we dragged to a slot, outside a window, etc.
    public static List<RaycastResult> RaycastMouse()
    {

        PointerEventData pointerData = new PointerEventData(EventSystem.current)
        {
            pointerId = -1,
        };

        pointerData.position = Input.mousePosition;

        List<RaycastResult> results = new List<RaycastResult>();
        EventSystem.current.RaycastAll(pointerData, results);

        return results;
    }
}

Set all the references between the Slot game object and its ItemSlot script:

Now drag the Slot game object to a newly created Prefabs folder inside Scripts/Inventory, to create a prefab.

Remember to drag the Slot prefab to the Slot Prefab entry in ContainerWindow game object in inspector.

Now let’s edit the ContainerWindow script. Study this script and the comments in it as it’s the most important part of this part of the tutorial.

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

public class ContainerWindow : MonoBehaviour {

    #region references

    [Header("Inventory References")]
    public EntityInventory entityInventory; // reference to the inventory displayed in the window

    [Header("UI References")]
    public GameObject slotPanel; // a panel that holds all the slots

    [Header("Prefabs")]
    public GameObject slotPrefab; // a prefab of a slot

    #endregion


    #region private 

    private bool updateInventory = false; // a flag that is set when the inventory needs refreshing that is reset each frame, we do this not to have more than 1 refresh each frame
    private List<ItemSlot> slots = new List<ItemSlot>(); // a list of all the slots in the container

    #endregion

    // Use this for initialization
    void Start () {
        // populate the slots
        UpdateSlots();

        // the main event from the BasicContainer should trigger our redrawing of the inventory:
        entityInventory.mainContainer.OnInventoryChange.AddListener(delegate { UpdateInventory(); });

        // force refresh for the first time
        RefreshInventoryWindow();
    }

    // Method for handling double clicking the item
    private void OnDoubleClickItem(ItemSlot itemSlot)
    {
        if (itemSlot.inventoryReference != null && itemSlot.inventoryItemReference!=null)
        {
            Debug.Log("Double clicked: " + itemSlot.inventoryItemReference.Name);
        }

    }

    // Method for handling dragging events of the ItemSlot component
    private void OnMoveItemBetweenSlots(ItemSlot from, ItemSlot to)
    {
        int fromSlot = from.slotNumber;
        int toSlot = to.slotNumber;

        // Calling the basic container static method to handle the movement
        BasicContainer.MoveItemsBetweenSlots(fromSlot, toSlot, from.inventoryReference.mainContainer, to.inventoryReference.mainContainer);
    }

    // Method for handling dropping item outside the inventory
    private void OnDropItemOutside(ItemSlot itemSlot)
    {
        if (itemSlot.inventoryReference != null && itemSlot.inventoryItemReference != null)
        {
            Debug.Log("Drop item outside inventory: " + itemSlot.inventoryItemReference.Name);
        }
    }

    // This method applies a BasicItem entry from a container to an ItemSlot element of the ContainerWindow
    // Setting up all the game objects, texts, icons, etc, and its events.
    private void ApplyInventoryItemToSlot(ItemSlot itemSlot, BasicItem inventoryItem)
    {

        // Remove previous events
        itemSlot.OnDoubleClickItem.RemoveAllListeners();
        itemSlot.OnDropFromSlotToSlot.RemoveAllListeners();
        itemSlot.OnDropItemOutside.RemoveAllListeners();

        // Set up references to the inventory
        itemSlot.inventoryItemReference = inventoryItem;
        itemSlot.inventoryReference = entityInventory;

        // Set up amount text
        if (inventoryItem.Amount > 1)
        {
            itemSlot.amountText.gameObject.SetActive(true);
            itemSlot.amountText.text = inventoryItem.Amount.ToString();
        }
        else
        {
            itemSlot.amountText.gameObject.SetActive(false);
        }

        // Load the icon from resources
        itemSlot.imageIcon.gameObject.SetActive(true);
        Sprite myFruit = Resources.Load<Sprite>("Items/"+ inventoryItem.Graphic);
        if (myFruit!=null)
        {
            itemSlot.imageIcon.sprite = myFruit;
        } else
        {
            Debug.Log("Cannot find the sprite for: " + inventoryItem.Graphic);
        }

        // Add listeners to slot events
        itemSlot.OnDoubleClickItem.AddListener(OnDoubleClickItem);
        itemSlot.OnDropFromSlotToSlot.AddListener(OnMoveItemBetweenSlots);
        itemSlot.OnDropItemOutside.AddListener(OnDropItemOutside);

    }

    // This method empties the slot of all references, events, and hides its unneeded game objects like amount text, icon etc.
    private void EmptySlot(ItemSlot itemSlot)
    {
        // Remove all events
        itemSlot.OnDoubleClickItem.RemoveAllListeners();
        itemSlot.OnDropFromSlotToSlot.RemoveAllListeners();
        itemSlot.OnDropItemOutside.RemoveAllListeners();

        // Clear references
        itemSlot.inventoryItemReference = null;
        itemSlot.inventoryReference = entityInventory;

        // Hide unneeded game objects
        itemSlot.amountText.gameObject.SetActive(false);
        itemSlot.imageIcon.gameObject.SetActive(false);

    }

    // This method refreshes the inventory window slots and sets them up with necessary references
    private void RefreshInventoryWindow()
    {
        if (slots.Count <= 0) return;

        for (int i = 0; i < entityInventory.mainContainer.slotAmount; i++) // itirate through all the slots
        {
            ItemSlot itemSlot = slots[i]; // get the slot

            BasicItem inventoryItem = entityInventory.mainContainer.GetItemOfSlotIndex(i); // get the item

            if (inventoryItem != null) // if the item exists fill the inventory slot
            {
                ApplyInventoryItemToSlot(itemSlot, inventoryItem);
            }
            else // if it doesnt empty the slot
            {
                EmptySlot(itemSlot);
            }

        }

    }
    
    // Method to poulate the slots
    void UpdateSlots()
    {

        foreach (Transform children in slotPanel.gameObject.transform)
        {
            Destroy(children.gameObject); // destroy all the preset slots
        }
        slots.Clear(); // clear the list

        for (int i = 0; i < entityInventory.mainContainer.slotAmount; i++)
        {
            // Instantiate all the slots and set their slot number inside the ItemSlot class
            GameObject slotObject = Instantiate(slotPrefab, slotPanel.gameObject.transform);
            ItemSlot itemSlot = slotObject.GetComponent<ItemSlot>();
            itemSlot.slotNumber = i;

            slots.Add(itemSlot);
        }

        UpdateInventory();
    }

    // A method called to set the updateInventory flag
    void UpdateInventory()
    {
        updateInventory = true;
    }


    // Update is called once per frame
    void Update()
    {
        if (updateInventory) // if the flag is set, clear it and refresh the inventory, we do this to avoid more than 1 refresh per frame
        {
            RefreshInventoryWindow();
            updateInventory = false;
        }
    }

}

 

The final thing is to set the Entity Inventory reference of the ContainerWindow in the inspector to the TestEntityInventory’s component: just drag the TestEntityInventory game object to that field in the Inventory References section of the ContainerWindow script.

Also create a folder called Resources/Items in your project, and drag this image file there, call it BasicIcon, and set its type to Sprite in the inspector:

Now you should have a working inventory window that allows for dragging items between empty slots:

8. Conclusion and downloads

Here is the full code for the project: Inventory System Part 1

In this part, we chose the Observer Pattern to dictate the architecture for the inventory system and wrote some basic classes. In the next part, we will make the ContainerWindow a truly modal system, add containers inside containers and improve the graphical presentation.

If you’d like, please follow me on twitter, or leave comments for this part of the tutorial here.

Tags: ,

Categorised in: