Creating a Farming RPG in Unity - Part 4: Item Management

Creating a Farming RPG (like Harvest Moon) in Unity — Part 4: Item Management

This article is a part of the series:
Creating a Farming RPG (like Harvest Moon) in Unity

Ever wanted to create a game like Harvest Moon in Unity? Check out Part 4 of our guide here, where we go through how to create an item management system. You can also find Part 3 of our guide here, where we went through how to set up farmland elements that our player character will interact with.

This is a loose transcription of our accompanying video guide on Youtube. We suggest you watch the video guide instead and use this article as a supplement to it.

When working with scripts in Unity, we usually inherit from MonoBehaviour. However, this will not be suitable for creating our items because:

  1. It will generate needless copies of values whenever the player gets more than one of the same item
  2. It is going to be difficult to scale the more items we have as it will slow down performance.
  3. It is dependent on being attached to a GameObject in a Scene which makes the data non-persistent in itself.

Since the data we want to record is going to be persistent and unchanging, it would be better for us to create data containers instead.

1. Using ScriptableObjects to store data of our items

This is where ScriptableObjects come in. Unlike MonoBehaviour classes, they allow us to save information on our items as custom Assets within our project. The information can then be read to be used at runtime.

They will be especially useful because:

  1. They are bound to the project, making their data persistent
  2. They will save us a lot of data, as there is only 1 copy for our other scripts to reference.
  3. It is easy to scale up and allows us to make as many of them as we want.

a. Setting up the base ScriptableObject

Create a new script and call it ItemData. Instead of inheriting from MonoBehaviour, make it inherit from ScriptableObject, like this:

ItemData.cs

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

[CreateAssetMenu(menuName ="Item")]
public class ItemData : MonoBehaviourScriptableObject
{
    // Start is called before the first frame update
    void Start()
    {
        
    }


    // Update is called once per frame
    public void Update()
    {
        
    }

}
Create New Item option

Similar to the Friends of Mineral Town (2003) entry of the Harvest Moon series, we’ll segregate the Tools (Items players use for farming) from other items. The player will see it like this:

How your inventory screen should look at the end of this part.

Create a folder to store the ScriptableObjects called Data. In it, create the 2 subfolders to categorise them. The structure should look like this:

  • Data
    • Items
    • Tools
ScriptableObjects Folder

Each Item should have the following properties:

NameTypeUse
descriptionstringExplain the use of the item in the Inventory UI
thumbnailSpriteIcon to be displayed in the Inventory UI
gameModelGameObjectThe GameObject in the scene for the player to interact with

Add these into the script:

ItemData.cs

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

[CreateAssetMenu(menuName ="Item")]
public class ItemData : ScriptableObject
{
    public string description;

    //Icon to be displayed in UI
    public Sprite thumbnail;

    //GameObject to be shown in the scene
    public GameObject gameModel;
}

Article continues after the advertisement:

Save Soil

The Save Soil Movement

Poor farming practices and policies across the world have led to the degradation of agricultural soils in the world. This has led to the loss of nutritional value in our food over the years, and it will affect our food production capabilities in the coming decades.

Learn more about Save Soil →


Now Create > Item in the Assets/Data/Items folder and name it ‘Cabbage’ to test our new item. You should see our newly-declared properties in the Inspector.

ItemData ScriptableObject in the inspector
Remember to give it some kind of description!

Within the Tools subcategory, we have 2 subclasses that need to store very different sets of information:

  • Equipment: Various tools used to prepare the land for farming (clearing obstacles and changing the state of the Land prefab).
  • Seeds: Items the player can plant to grow into a crop.

b. EquipmentData

Making it Scaleable: We could technically just use the ItemData class for our equipment ScriptableObjects, and code the interaction scripts to just check for the name of our items. But we decided to record each of the Equipment types with enumerations (i.e. enums) instead so it’s easier to create tool upgrades and variants should we want to do it in future.

For this part, we will set up the following tools:

  1. Axe: To chop down wood obstacles
  2. Hoe: To till the land for farming
  3. Pickaxe: To clear rock obstacles
  4. Watering Can: To water the land.

Note: In the Harvest Moon and Story of Seasons games, it’s usually a hammer that is used to clear rock obstacles. We changed it to a pickaxe because it’s easier to find sprites of it online.

Since we’re pretty much recording the same set of data from ItemData on top of its own set of properties, we can just inherit its class from ItemData. Create a new script called EquipmentData with the following:

EquipmentData.cs

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

[CreateAssetMenu(menuName = "Items/Equipment")]
public class EquipmentData :MonoBehaviourItemData
{
    public enum ToolType
    {
        Hoe, WateringCan, Axe, Pickaxe
    }
    public ToolType toolType;

 

    // Start is called before the first frame update
    void Start()
    {
        
    }


    // Update is called once per frame
    public void Update()
    {
        
    }


}

Notice that we set the ScriptableObject‘s menu name to be a submenu of ‘Items’. We have to make ItemData‘s asset menu creation to have a submenu as well:

ItemData.cs

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

[CreateAssetMenu(menuName ="Items/Item")]
public class ItemData : ScriptableObject
{
    public string description;

    //Icon to be displayed in UI
    public Sprite thumbnail;

    //GameObject to be shown in the scene
    public GameObject gameModel; 
}
create asset menu equipment
The new asset creation menu.

Under Assets/Data/Tools, create an asset for each Equipment you have in your game:

equipment scriptableobjects
The Equipment ScriptableObjects
EquipmentData Scriptable Objects
Equipment assets, being children of ItemData, inherits fields from it on top of its own.

c. Getting item sprites

We’ll need some 2D assets for the thumbnail of our items. Download the following assets:

  1. Farming Tool Icons by Calciumtrice, usable under Creative Commons Attribution 3.0 license.
  2. 2D Vegetables by ScratchIO, Public Domain (CC0 license)

Import the sprite sheets to Assets/Imported Asset/ UI. For each of the images, set:

  1. Texture Type to Sprite (2D and UI), and;
  2. Sprite Mode to Multiple,

…and click Apply.

sprite import settings
Import settings can be found on the Inspector tab when you select the image.

As the images are sprite sheets, you see that all of the sprites are on one image. We need Unity to extract them so that we are able to display them individually. Click on Sprite Editor. You might get the following:

no sprite editor window
This is because the 2D Sprite package doesn’t come with the typical 3D project configuration.

To fix this, go to Window > Package Manager and find 2D Sprite. Click Install.

2d sprite package manager
If you don’t see it on the list, just switch around the packages filters (e.g. All Packages to Built-in Packages and back) until it loads.

After the package is installed, open up the Sprite Editor and slice up the sprites accordingly:

sprite editor window
Don’t forget to hit Apply before you close the editor window.

Article continues after the advertisement:

Save Soil

The Save Soil Movement

Poor farming practices and policies across the world have led to the degradation of agricultural soils in the world. This has led to the loss of nutritional value in our food over the years, and it will affect our food production capabilities in the coming decades.

Learn more about Save Soil →


If done right, this is what it should look like in the editor:

sliced sprites in the editor
Don’t forget to do the same for the Farming Tools sprite sheet.

Go to each of our Equipment assets, and assign an appropriate sprite to its thumbnail.

assigning the thumbnail to ScriptableObjects
This should make it easy to identify all the different farming tools.

Note: The Farming Tools sprite sheet does not come with a sprite for the Watering Can, so just set its sprite to any placeholder for now.

d. SeedData

We want seeds to have the following properties:

NameTypeUse
daysToGrowintAmount of time in days before it can be harvested
cropToYieldItemDataThe item to go into the player’s inventory once it matures and is harvested.

To this end, create a new script called SeedData with the following:

SeedData.cs

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

[CreateAssetMenu(menuName ="Items/Seed")]
public class SeedData : ItemData
{
    //Time it takes before the seed matures into a crop
    public int daysToGrow;

    //The crop the seed will yield
    public ItemData cropToYield; 
}

In Assets/Data/Tools, create a folder called Seeds, and Create > Items > Seed, naming it ‘Cabbage Seeds’. Fill in the fields:

seeds scriptableobject in the inspector
Write whatever you think is best for the description.

2. Setting up the InventoryManager

In the Scene, create an empty GameObject and name it Manager. This will hold our Manager scripts, which will manage various aspects of our game’s state.

As only 1 instance of a Manager script is needed at any point in time, we are going to use the Singleton pattern. This restricts the class to have only 1 instance, which makes it easier to reference in our other scripts.

You can refer to this article for an explanation of Singletons.

Instead of having to use FindObjectOfType() to find a Manager class every time we want to work with one, we simply can refer to its globally-accessible instance, stored as a static variable, like Manager.Instance.

Create a new script called InventoryManager:

InventoryManager.cs

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

public class InventoryManager : MonoBehaviour
{
    public static InventoryManager Instance { get; private set; }

    private void Awake()
    {
        //If there is more than one instance, destroy the extra
        if(Instance != null && Instance != this)
        {
            Destroy(this);
        }
        else
        {
            //Set the static instance to this instance
            Instance = this; 
        }
    }

    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

Assign it to the Manager GameObject.

The Player’s inventory will comprise of the following:

  • Tools: 8 slots
  • Equipped Tool: 1 Slot
  • Items: 8 Slots
  • Equipped Item: 1 Slot

The InventoryManager will keep track of this information with variables:

InventoryManager.cs

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

public class InventoryManager : MonoBehaviour
{
    public static InventoryManager Instance { get; private set; }

    private void Awake()
    {
        //If there is more than one instance, destroy the extra
        if(Instance != null && Instance != this)
        {
            Destroy(this);
        }
        else
        {
            //Set the static instance to this instance
            Instance = this; 
        }
    }

    [Header("Tools")]
    //Tool Slots
    public ItemData[] tools = new ItemData[8];
    //Tool in the player's hand
    public ItemData equippedTool = null; 

    [Header("Items")]
    //Item Slots
    public ItemData[] items = new ItemData[8];
    //Item in the player's hand
    public ItemData equippedItem = null;

    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

Article continues after the advertisement:

Save Soil

The Save Soil Movement

Poor farming practices and policies across the world have led to the degradation of agricultural soils in the world. This has led to the loss of nutritional value in our food over the years, and it will affect our food production capabilities in the coming decades.

Learn more about Save Soil →


3. Setting up the user interface

Note: You do not have to follow this section to the letter. More importantly, ensure that the hierarchy of the Canvas’ contents is the same. You can refer to the video for a more in-depth demonstration.

Create a new UI canvas as a child of the Manager GameObject.

canvas on manager
Right-click Manager, UI > Canvas

On its Canvas Scaler component, set the UI Scale Mode to Scale With Screen Size.

Set the Reference Resolution to 1600×900.

canvas scale

a. Setting up the Inventory Panel GameObject

Create an empty GameObject parented to the Canvas and name it ‘Inventory Panel‘. You should see a RectTransform component on it instead of the usual Transform. Stretch it to take up the entire screen.

stretch to fit screen
Hold Alt and click.

Create a button parented to the Canvas to open this Inventory Panel ( Right-click Manager, UI > Button ). Place it at the bottom-right of the screen and change the source image to the backpack icon from our Farm Tools sprite sheet.

inventory open button
Resize the button to your desired size with the Rect tool.

Article continues after the advertisement:


b. Creating the Tools Section of the Inventory Panel

Right-click Inventory Panel, UI > Image, and resize it like this:

It should take up about half the screen.

Name this Image GameObject ‘ToolsPanel’. You may give it a colour and a Header with a text component (Right-click ToolsPanel > UI > Text).

Changing Tools Section background colour and adding a header
The hex code for the colour of the ToolsPanel Image component is #F4DDB7.

Create the Hand slot with the Image component and set the source image to UISprite. Position it like this:

handslot image component
The Image colour is #AF8E60.

Create an empty GameObject called InventorySlots and resize it within the ToolsPanel GameObject like this:

inventory slots gameobject

On the GameObject, add a Grid Layout Group component and give it the following parameters:

  • Cell Size: 100×100
  • Spacing: 50×50
More importantly, ensure that it is able to evenly spread out 8 Image GameObjects.

Create a new folder in Assets/Prefabs called ‘UI‘.

As a child of Inventory Slots, create a new Image GameObject, with the same colour as Hand Slot. Call this ‘Inventory Slot‘. Parented to this, create another Image GameObject called ‘Item Display‘. Make it slightly smaller than its parent:

Inventory Slots Item Display
You can set the Source Image for Item Display to anything to preview of what a filled item slot should look like.

Save the Inventory Slot GameObject as a Prefab under Assets/Prefabs/UI. Add 7 more of the same prefab to the Inventory Slots object.

inventory slots
The Grid Layout Group component aligns these prefabs automatically for you.

c. Creating the Items Panel

  1. Duplicate the ToolsPanel GameObject.
  2. Rename it to ItemsPanel
  3. Change the header text to ‘Tools’
  4. Move the panel to the right and anchor it there.
tools and items panel
Tools and Items

Article continues after the advertisement:


We want the Inventory Panel to be in front of the Inventory Button. Swap their positions in the scene hierarchy:

inventory button hierarchy order
Click and drag.

d. Creating the Item Info box.

Add another Image GameObject of the InventoryPanel, and name it Item Info. Add a text component each for the item name and description and position it on the bottom of the panel.

item info box
Give it some placeholder text.

Note: We created the Item Description Text GameObject by just duplicating the Item Name Text and changing up the size. However, we forgot to rename Item Name (1) to Item Description until much later in the video. Do this now so you will not be confused later on.

e. Toggling the Inventory Panel

By default, the Inventory panel should not be showing. Set the Inventory Panel GameObject to inactive. We will make it open when the player clicks the Inventory Button. The easiest way to do so is by configuring it on the Button component.

On the Button component of the Inventory Button GameObject, add a new On Click event, and drag the Inventory Panel GameObject on it.

Set it to GameObject.SetActive, and tick the boolean box.

inventory button onclick
When the player clicks on this button, it will set the Inventory Panel to active

So far, the player is able to open the Inventory Panel in-game by clicking on the Backpack. However, we have not set up a way for the player to close it. Hence, we’ll add a return button on the inventory panel.

  1. Create a button in the Inventory Panel and call it ‘Exit Button‘.
  2. Position it on the bottom right.
  3. Set the background colour to match the colour scheme.
  4. Give it the same OnClick event as the Inventory Button.
  5. Uncheck the boolean.
The background colour is #009679.

You should be able to open and close the inventory in-game now.

The inventory panel should look like this in-game.

4. Adding functionality to the Inventory UI

As of now it only displays the basic layout of the inventory, but it does not reflect what is in our player’s inventory at all.

a. Displaying items in the inventory slots

Create a new script, UIManager using the Singleton pattern and add it to the Manager GameObject in the scene:

UIManager.cs

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

public class UIManager : MonoBehaviour
{
    public static UIManager Instance { get; private set; }


    private void Awake()
    {
        //If there is more than one instance, destroy the extra
        if (Instance != null && Instance != this)
        {
            Destroy(this);
        }
        else
        {
            //Set the static instance to this instance
            Instance = this;
        }
    }

    private void Start()
    {
        
    }


    // Update is called once per frame
    public void Update()
    {
        
    }



    
    
}

Article continues after the advertisement:


We need each Inventory Slot to keep track of its own contents and display the information accordingly. We’ll set up a script with a function to change its Item Display Image sprite according to the thumbnail value in the ItemData we feed it.

Create a new script, InventorySlot:

InventorySlot.cs

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

public class InventorySlot : MonoBehaviour
{
    ItemData itemToDisplay;

    public Image itemDisplayImage; 

    public void Display(ItemData itemToDisplay)
    {
        //Check if there is an item to display
        if(itemToDisplay != null)
        {
            //Switch the thumbnail over
            itemDisplayImage.sprite = itemToDisplay.thumbnail;
            this.itemToDisplay = itemToDisplay;
        }

        
    }


    // Start is called before the first frame update
    void Start()
    {
        
    }


    // Update is called once per frame
    public void Update()
    {
        
    }



}

Add the new script to the prefab GameObject.

InventorySlot component
Remember to do this while inside the Inventory Slot prefab, so it applies to all of its instances!

We also need to account for how empty slots are to be displayed. Even if we were to give our Item Display Image’s sprite a null value, it will display a white box at most.

Inventory Slots Item Display
This image is from when we first set up the Inventory Slot prefab.

When there is no item in the slot, we want only an empty box to be shown. Hence, we will have to disable the Item Display GameObject when nothing is showing:

InventorySlot.cs

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

public class InventorySlot : MonoBehaviour
{
    ItemData itemToDisplay;

    public Image itemDisplayImage; 

    public void Display(ItemData itemToDisplay)
    {
        //Check if there is an item to display
        if(itemToDisplay != null)
        {
            //Switch the thumbnail over
            itemDisplayImage.sprite = itemToDisplay.thumbnail;
            this.itemToDisplay = itemToDisplay;

            itemDisplayImage.gameObject.SetActive(true);

            return; 
        }

        itemDisplayImage.gameObject.SetActive(false);

        
    }


}

Article continues after the advertisement:


Now that we have set up the InventorySlot class, we will assign all of them to UIManager to handle. We will need it to do the following:

  1. Store information of all the InventorySlots of the Tools section
  2. Store information of all the InventorySlots of the Items section
  3. Have a function that transfers the information from InventoryManager into the corresponding InventorySlots

To this end, add this to UIManager:

UIManager.cs

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

public class UIManager : MonoBehaviour
{
    public static UIManager Instance { get; private set; }

    [Header("Inventory System")]
    //The tool slot UIs
    public InventorySlot[] toolSlots;

    //The item slot UIs
    public InventorySlot[] itemSlots;


    private void Awake()
    {
        //If there is more than one instance, destroy the extra
        if (Instance != null && Instance != this)
        {
            Destroy(this);
        }
        else
        {
            //Set the static instance to this instance
            Instance = this;
        }
    }

    private void Start()
    {
        RenderInventory();
    }

    //Render the inventory screen to reflect the Player's Inventory. 
    public void RenderInventory()
    {
        //Get the inventory tool slots from Inventory Manager
        ItemData[] inventoryToolSlots = InventoryManager.Instance.tools;

        //Get the inventory item slots from Inventory Manager
        ItemData[] inventoryItemSlots = InventoryManager.Instance.items;

        //Render the Tool section
        RenderInventoryPanel(inventoryToolSlots, toolSlots);

        //Render the Item section
        RenderInventoryPanel(inventoryItemSlots, itemSlots);
    }

    //Iterate through a slot in a section and display them in the UI
    void RenderInventoryPanel(ItemData[] slots, InventorySlot[] uiSlots)
    {
        for (int i = 0; i < uiSlots.Length; i++)
        {
            //Display them accordingly
            uiSlots[i].Display(slots[i]);
        }
    }

    
}

Now to assign each slot for each section. Make sure you assign the slots to the correct sections!

assigning tool slots to UIManager
Assigning Inventory Slots in the Tool section to UIManager’s Tool Slots.
assigning item slots to UIManager
Assigning Inventory Slots in the Item section to UIManager’s Item Slots.

Article continues after the advertisement:


Assign the Item Display Image to the Item Display Image field in InventorySlot.

Assigning item display to inventory slot

b. Toggling the inventory UI programmatically

If everything is working properly, it should show empty Inventory slots, as our inventory is empty.

empty in game inventory panel
You should not be able to see the placeholders anymore.

However, you were to equip or assign items to the Inventory in InventoryManager during runtime, you would not be able to see the changes reflected. This is because the function RenderInventory is only called in UIManager once in the Start function.

We have to call this function more often. Yet, calling this on Update will not be optimal as the player will not be checking or changing the inventory state that often. As the Inventory is more likely to be shown and updated every time the player opens or close it, it would make sense to call RenderInventory at those points.

Hence, it would make more sense to toggle the Inventory Panel programmatically rather than what we did in 3e. This would make things a lot easier in the future if we wanted the panel to be opened by keyboard hotkeys too. To this end, let’s set up the function to toggle the panel in UIManager:

UIManager.cs

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

public class UIManager : MonoBehaviour
{
    public static UIManager Instance { get; private set; }

    [Header("Inventory System")]
    //The inventory panel
    public GameObject inventoryPanel; 

    //The tool slot UIs
    public InventorySlot[] toolSlots;

    //The item slot UIs
    public InventorySlot[] itemSlots;

    private void Awake()
    {
        //If there is more than one instance, destroy the extra
        if (Instance != null && Instance != this)
        {
            Destroy(this);
        }
        else
        {
            //Set the static instance to this instance
            Instance = this;
        }
    }

    private void Start()
    {
        RenderInventory();
    }

    //Render the inventory screen to reflect the Player's Inventory. 
    public void RenderInventory()
    {
        //Get the inventory tool slots from Inventory Manager
        ItemData[] inventoryToolSlots = InventoryManager.Instance.tools;

        //Get the inventory item slots from Inventory Manager
        ItemData[] inventoryItemSlots = InventoryManager.Instance.items;

        //Render the Tool section
        RenderInventoryPanel(inventoryToolSlots, toolSlots);

        //Render the Item section
        RenderInventoryPanel(inventoryItemSlots, itemSlots);
    }

    //Iterate through a slot in a section and display them in the UI
    void RenderInventoryPanel(ItemData[] slots, InventorySlot[] uiSlots)
    {
        for (int i = 0; i < uiSlots.Length; i++)
        {
            //Display them accordingly
            uiSlots[i].Display(slots[i]);
        }
    }

    public void ToggleInventoryPanel()
    {
        //If the panel is hidden, show it and vice versa
        inventoryPanel.SetActive(!inventoryPanel.activeSelf);

        RenderInventory();
    }

    
}

Assign the Inventory Panel GameObject to the field we declared in UIManager.

assigning inventory panel to manager

On each of the buttons, Exit Button and Inventory Button, set the On Click event to call UIManager.ToggleInventoryPanel.

toggle button click event
Remember to do this for both Inventory Button and Exit Button.

Now you can alter the player’s inventory during runtime and the inventory screen will update accordingly every time you clicked the Backpack or ‘Return’ buttons.

Note: Whatever you give the player in the InventoryManager during runtime will be rolled back the moment you stop. To give your player some starting equipment, make sure that you do it in the editor outside of the runtime environment.

runtime item changes
The watering can sticks out like a sore thumb because we don’t have a suitable sprite for it for now.

Article continues after the advertisement:


b. Displaying the item name and description in the Item Info box

When the player hovers over an inventory slot, the item name and description should appear on the Item Info Box.

Let’s make a function on UIManager that takes in an ItemData parameter and processes it to show the relevant information on the UI elements:

UIManager.cs

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

public class UIManager : MonoBehaviour
{
    public static UIManager Instance { get; private set; }

    [Header("Inventory System")]
    //The inventory panel
    public GameObject inventoryPanel; 

    //The tool slot UIs
    public InventorySlot[] toolSlots;

    //The item slot UIs
    public InventorySlot[] itemSlots;

    //Item info box
    public Text itemNameText;
    public Text itemDescriptionText; 

    private void Awake()
    {
        //If there is more than one instance, destroy the extra
        if (Instance != null && Instance != this)
        {
            Destroy(this);
        }
        else
        {
            //Set the static instance to this instance
            Instance = this;
        }
    }

    private void Start()
    {
        RenderInventory();
    }

    //Render the inventory screen to reflect the Player's Inventory. 
    public void RenderInventory()
    {
        //Get the inventory tool slots from Inventory Manager
        ItemData[] inventoryToolSlots = InventoryManager.Instance.tools;

        //Get the inventory item slots from Inventory Manager
        ItemData[] inventoryItemSlots = InventoryManager.Instance.items;

        //Render the Tool section
        RenderInventoryPanel(inventoryToolSlots, toolSlots);

        //Render the Item section
        RenderInventoryPanel(inventoryItemSlots, itemSlots);
    }

    //Iterate through a slot in a section and display them in the UI
    void RenderInventoryPanel(ItemData[] slots, InventorySlot[] uiSlots)
    {
        for (int i = 0; i < uiSlots.Length; i++)
        {
            //Display them accordingly
            uiSlots[i].Display(slots[i]);
        }
    }

    public void ToggleInventoryPanel()
    {
        //If the panel is hidden, show it and vice versa
        inventoryPanel.SetActive(!inventoryPanel.activeSelf);

        RenderInventory();
    }

    //Display Item info on the Item infobox
    public void DisplayItemInfo(ItemData data)
    {
        itemNameText.text = data.name;
        itemDescriptionText.text = data.description; 
    }
    
}

Assign the Item Info Box references we just declared in the Inspector.

item name and description
You should have renamed Item Name (1) to Item Description from Step 3d.

We need to call this function when the player’s mouse enters or exits the Inventory Slot. We can accomplish this with Unity’s IPointerEnterHandler and IPointerExitHandler interfaces, which provides us with a callback function for the job.

Interfaces serve as ‘blueprints’ in which they add additional specifications to what a class must include while having the flexibility of letting the class decide how to implement it.

In the case of IPointerEnterHandler, it requires the class to implement a function that is fired whenever the player hovers over the element. We implement that function and define for ourselves what we want it to do when it is fired.

You can read up more on Interfaces here.

We want it to do the following:

  • When the player’s mouse enters the inventory slot: Display the Item name and description
  • When the player’s mouse exits the inventory slot: Reset the Item Info Box to display nothing

Let’s implement this interface in InventorySlot:

InventorySlot.cs

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

public class InventorySlot : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler
{
    ItemData itemToDisplay;

    public Image itemDisplayImage; 

    public void Display(ItemData itemToDisplay)
    {
        //Check if there is an item to display
        if(itemToDisplay != null)
        {
            //Switch the thumbnail over
            itemDisplayImage.sprite = itemToDisplay.thumbnail;
            this.itemToDisplay = itemToDisplay;

            itemDisplayImage.gameObject.SetActive(true);

            return; 
        }

        itemDisplayImage.gameObject.SetActive(false);

        
    }


    //Display the item info on the item info box when the player mouses over
    public void OnPointerEnter(PointerEventData eventData)
    {
        UIManager.Instance.DisplayItemInfo(itemToDisplay);
    }

    //Reset the item info box when the player leaves
    public void OnPointerExit(PointerEventData eventData)
    {
        UIManager.Instance.DisplayItemInfo(null);
    }
}

If we were to test this now, we would get the following error:

item info null reference
Comes up when the player’s mouse exits a slot or enters an empty slot

This is because just passing a null value into the DisplayItemInfo function will not just output an empty text. We have to actually account for this scenario in the code. Add the following to UIManager:

UIManager.cs

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

public class UIManager : MonoBehaviour
{
    public static UIManager Instance { get; private set; }

    [Header("Inventory System")]
    //The inventory panel
    public GameObject inventoryPanel; 

    //The tool slot UIs
    public InventorySlot[] toolSlots;

    //The item slot UIs
    public InventorySlot[] itemSlots;

    //Item info box
    public Text itemNameText;
    public Text itemDescriptionText; 

    private void Awake()
    {
        //If there is more than one instance, destroy the extra
        if (Instance != null && Instance != this)
        {
            Destroy(this);
        }
        else
        {
            //Set the static instance to this instance
            Instance = this;
        }
    }

    private void Start()
    {
        RenderInventory();
    }

    //Render the inventory screen to reflect the Player's Inventory. 
    public void RenderInventory()
    {
        //Get the inventory tool slots from Inventory Manager
        ItemData[] inventoryToolSlots = InventoryManager.Instance.tools;

        //Get the inventory item slots from Inventory Manager
        ItemData[] inventoryItemSlots = InventoryManager.Instance.items;

        //Render the Tool section
        RenderInventoryPanel(inventoryToolSlots, toolSlots);

        //Render the Item section
        RenderInventoryPanel(inventoryItemSlots, itemSlots);
    }

    //Iterate through a slot in a section and display them in the UI
    void RenderInventoryPanel(ItemData[] slots, InventorySlot[] uiSlots)
    {
        for (int i = 0; i < uiSlots.Length; i++)
        {
            //Display them accordingly
            uiSlots[i].Display(slots[i]);
        }
    }

    public void ToggleInventoryPanel()
    {
        //If the panel is hidden, show it and vice versa
        inventoryPanel.SetActive(!inventoryPanel.activeSelf);

        RenderInventory();
    }

    //Display Item info on the Item infobox
    public void DisplayItemInfo(ItemData data)
    {
        //If data is null, reset
        if(data == null)
        {
            itemNameText.text = "";
            itemDescriptionText.text = "";

            return;
        }

        itemNameText.text = data.name;
        itemDescriptionText.text = data.description; 
    }
    
}

Conclusion

item info box in action
The Item Info Box displays the item information on hover and clears on mouse exit.

With that, we have laid the groundwork for our Item Management system. Here is the final code for all the scripts we have worked with today:

ScriptableObject scripts:

ItemData.cs

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

[CreateAssetMenu(menuName ="Items/Item")]
public class ItemData : ScriptableObject
{
    public string description;

    //Icon to be displayed in UI
    public Sprite thumbnail;

    //GameObject to be shown in the scene
    public GameObject gameModel; 
}

EquipmentData.cs

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

[CreateAssetMenu(menuName = "Items/Equipment")]
public class EquipmentData : ItemData
{
    public enum ToolType
    {
        Hoe, WateringCan, Axe, Pickaxe
    }
    public ToolType toolType; 
 
}

SeedData.cs

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

[CreateAssetMenu(menuName ="Items/Seed")]
public class SeedData : ItemData
{
    //Time it takes before the seed matures into a crop
    public int daysToGrow;

    //The crop the seed will yield
    public ItemData cropToYield; 
}

Inventory Management and UI scripts:

UIManager.cs

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

public class UIManager : MonoBehaviour
{
    public static UIManager Instance { get; private set; }

    [Header("Inventory System")]
    //The inventory panel
    public GameObject inventoryPanel; 

    //The tool slot UIs
    public InventorySlot[] toolSlots;

    //The item slot UIs
    public InventorySlot[] itemSlots;

    //Item info box
    public Text itemNameText;
    public Text itemDescriptionText; 

    private void Awake()
    {
        //If there is more than one instance, destroy the extra
        if (Instance != null && Instance != this)
        {
            Destroy(this);
        }
        else
        {
            //Set the static instance to this instance
            Instance = this;
        }
    }

    private void Start()
    {
        RenderInventory();
    }

    //Render the inventory screen to reflect the Player's Inventory. 
    public void RenderInventory()
    {
        //Get the inventory tool slots from Inventory Manager
        ItemData[] inventoryToolSlots = InventoryManager.Instance.tools;

        //Get the inventory item slots from Inventory Manager
        ItemData[] inventoryItemSlots = InventoryManager.Instance.items;

        //Render the Tool section
        RenderInventoryPanel(inventoryToolSlots, toolSlots);

        //Render the Item section
        RenderInventoryPanel(inventoryItemSlots, itemSlots);
    }

    //Iterate through a slot in a section and display them in the UI
    void RenderInventoryPanel(ItemData[] slots, InventorySlot[] uiSlots)
    {
        for (int i = 0; i < uiSlots.Length; i++)
        {
            //Display them accordingly
            uiSlots[i].Display(slots[i]);
        }
    }

    public void ToggleInventoryPanel()
    {
        //If the panel is hidden, show it and vice versa
        inventoryPanel.SetActive(!inventoryPanel.activeSelf);

        RenderInventory();
    }

    //Display Item info on the Item infobox
    public void DisplayItemInfo(ItemData data)
    {
        //If data is null, reset
        if(data == null)
        {
            itemNameText.text = "";
            itemDescriptionText.text = "";

            return;
        }

        itemNameText.text = data.name;
        itemDescriptionText.text = data.description; 
    }
    
}

InventorySlot.cs

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

public class InventorySlot : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler
{
    ItemData itemToDisplay;

    public Image itemDisplayImage; 

    public void Display(ItemData itemToDisplay)
    {
        //Check if there is an item to display
        if(itemToDisplay != null)
        {
            //Switch the thumbnail over
            itemDisplayImage.sprite = itemToDisplay.thumbnail;
            this.itemToDisplay = itemToDisplay;

            itemDisplayImage.gameObject.SetActive(true);

            return; 
        }

        itemDisplayImage.gameObject.SetActive(false);

        
    }


    //Display the item info on the item info box when the player mouses over
    public void OnPointerEnter(PointerEventData eventData)
    {
        UIManager.Instance.DisplayItemInfo(itemToDisplay);
    }

    //Reset the item info box when the player leaves
    public void OnPointerExit(PointerEventData eventData)
    {
        UIManager.Instance.DisplayItemInfo(null);
    }
}

InventoryManager.cs

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

public class InventoryManager : MonoBehaviour
{
    public static InventoryManager Instance { get; private set; }

    private void Awake()
    {
        //If there is more than one instance, destroy the extra
        if(Instance != null && Instance != this)
        {
            Destroy(this);
        }
        else
        {
            //Set the static instance to this instance
            Instance = this; 
        }
    }

    [Header("Tools")]
    //Tool Slots
    public ItemData[] tools = new ItemData[8];
    //Tool in the player's hand
    public ItemData equippedTool = null; 

    [Header("Items")]
    //Item Slots
    public ItemData[] items = new ItemData[8];
    //Item in the player's hand
    public ItemData equippedItem = null;

    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

There are 10 comments:

  1. This has been a great tutorial so far. Everything is working nicely except the pointer handlers are not firing and I cant figure out why… I replaced onpointerenter with a debug log to see if its doing anything and it isnt… any idea why it wouldnt work? The inventoryslot script is on my itemslot which is parent to the itemdisplay. itemdisplay is set in the inventory slot script on the itemslot. Both are checked as raycast targets. I dont know what i am missing… any ideas? Im using unity 2021.3.

    1. I just noticed my project has an intercepted event for onpointerenter… I guess something else in my project is blocking it so nevermind… I will have to figure out what is blocking it. Really loving this tutorial! Hopefully i can figure it out so i can continue.

      1. Ok I figured it out! For anyone with the same problem, you have to make sure your canvas has the EventSystem under it. My project already had an existing canvas and the EventSystem was under that canvas. I moved the EventSystem under my inventory canvas built with this tutorial and now the OnPointer functions work perfectly.

        1. Hi Nick, thank you for contributing your answers to the comments section. Sorry, it looks like we missed your comment! Feel free to message us on Patreon too (in future) if you need an answer quick.

  2. hi, I can’t drag Item name game and item description on UI manager inspector item name text and item description text, no error shown on console thou

  3. I know this is an a year old article, but sheesh, it really helped. I’ve been pounding my head trying to get my inventory looking nice and functional. Can’t believe I haven’t ran into the IPointer interfaces smh. I’ve bookmarked this article, as it is seriously one of the better ones out there (in terms of simplicity and readability).

Leave a Reply

Your email address will not be published. Required fields are marked *

Note: You can use Markdown to format your comments.

For security, use of Google's reCAPTCHA service is required which is subject to the Google Privacy Policy and Terms of Use.

I agree to these terms.

This site uses Akismet to reduce spam. Learn how your comment data is processed.