Ever wanted to create a game like Harvest Moon in Unity? Check out Part 5 of our guide here, where we go through how to equip and unequip items. You can also find Part 4 of our guide here, where we went through how to create an item management system.
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.
In Part 4 of the series, we managed to get the Inventory Panel to reflect our player’s inventory. However, if you look closely, the aspect ratio of our item sprites are not preserved.
To fix this, simply open the Inventory Slot prefab and check Preserve Aspect on the Item Display Image.
It should look like this now:
1. Setting up the HUD status bar
In the HUD, there should be a bar that summarises critical information to the player. We want it to show the following:
- An icon of the equipped tool (Player’s Hand Slot)
- The weather
- The date
- The time
Why display the equipped tool but not the item? Typically in a Harvest Moon game, when the player equips a tool, you do not see the player hold the tool until he actually uses it. Hence, we need to convey this information non-diegetically, that is to display it in the status bar. On the other hand, one usually knows what item the player has equipped by looking at the model he’s holding in the game. Information on the item is thus conveyed diegetically, that is in the context of the game world.
At least for this tutorial, we will be referencing the design choices of Friends of Mineral Town (2003). The status bar should look like this:
a. Setting up the UI elements
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.
On the Canvas, create a new Image and name it Status Bar.
Change its image colour to match the Inventory Panel (#F4DDB7
) and position it on the bottom left of the screen.
Resize it to be big enough to fit the 4 elements.
Make a copy of the Hand Slot Image from the ToolsPanel / ItemsPanel and make it a child of the Status Bar, and position it to the left of the panel.
Create an empty GameObject within the Status Bar, called Time Info, to organise the rest of the elements in the bar. Resize it to take up the rest of the space in the panel.
As children of Time Info, create the other 3 elements:
Name | Type | Instructions |
---|---|---|
Weather | Image | Copy from Hand Slot and change the colour to something else to set it apart |
Date | Text | Give it placeholder text ‘SPR 1 (Sun)’ of font size 24 |
Time | Text | Give it placeholder text ‘AM 6:00’ of font size 48 |
Align them like this:
Do some sizing and positioning adjustments until you are satisfied with how it looks.
This is a repeat of what we did with the Inventory Slots prefab. Parented to Hand Slot, create another Image GameObject called ‘Item Display‘. Make it slightly smaller than its parent, and check Preserve Aspect.
Article continues after the advertisement:
b. Adding functionality to Tool Hand Slot
For the Hand Slot to display what is in our Tool equip slot, we need UIManager
to switch the sprites of the Hand Slot Image to the equipped tool that is set in InventoryManager
. Similar to what we did in InventorySlot
, 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("Status Bar")] //Tool equip slot on the status bar public Image toolEquipSlot; [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); //Get Tool Equip from InventoryManager ItemData equippedTool = InventoryManager.Instance.equippedTool; //Check if there is an item to display if (equippedTool != null) { //Switch the thumbnail over toolEquipSlot.sprite = equippedTool.thumbnail; toolEquipSlot.gameObject.SetActive(true); return; } toolEquipSlot.gameObject.SetActive(false); } //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; } }
In the Inspector, assign Hand Slot Image to the newly declared Tool Equip Slot in UIManager
:
When you playtest the game, the changes you make to the Equipped Tool value in InventoryManager
during runtime should update in the Status Bar whenever you open or close the inventory panel.
2. Equipping and unequipping in the Inventory
There are many ways one can design the equipment system. However, at least for this series, we will keep the equipping system simple.
When the Hand Slot is clicked on, the Item moves to the first empty Inventory Slot.
When an Inventory Slot is clicked on:
- The Item in Inventory Slot moves to the Hand Slot.
- If the Hand Slot has an item, it will move to the Inventory Slot
a. Distinguishing between Tools and Items in slots
Our inventory is split into two sections: Tools and Items.
Hence, before we get to dealing with equipping and unequipping, we need to first create a way for the code to tell the two sections apart. We will do this with a new enum, InventoryType
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 enum InventoryType { Item, Tool } //Determines which inventory section this slot is apart of. public InventoryType inventoryType; 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); } }
Select all the Inventory Slots under the ToolsPanel and set their InventoryType
to Tool.
Note: Set the Inventory Panel to active while you’re making edits, but remember to set them back to inactive once you are done!
Select all the Inventory Slots under the ItemsPanel and set their InventoryType
to Item.
Article continues after the advertisement:
b. Setting up the equip and unequip methods
Create a function in InventoryManager
for each of the 2 cases mentioned earlier:
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; //Equipping //Handles movement of item from Inventory to Hand public void InventoryToHand() { } //Handles movement of item from Hand to Inventory public void HandToInventory() { } // Start is called before the first frame update void Start() { } // Update is called once per frame void Update() { } }
The function, InventoryToHand
, should be called when the player clicks on an Inventory Slot. We will use the IPointerClickHandler interface in InventorySlot
to call this:
InventorySlot.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI; public class InventorySlot : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler, IPointerClickHandler { ItemData itemToDisplay; public Image itemDisplayImage; public enum InventoryType { Item, Tool } //Determines which inventory section this slot is apart of. public InventoryType inventoryType; 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); } public void OnPointerClick(PointerEventData eventData) { //Move item from inventory to hand InventoryManager.Instance.InventoryToHand(); } //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); } }
The Hand Slot essentially shares the same behaviour with the Inventory Slot except when it is clicked. Thus, it is best for its script to inherit from the InventorySlot
script and override the OnPointerClick
callback.
Before we create a script for the Hand Slot then, we need to make OnPointerClick
overridable. Add the virtual keyword to the function 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, IPointerClickHandler { ItemData itemToDisplay; public Image itemDisplayImage; public enum InventoryType { Item, Tool } //Determines which inventory section this slot is apart of. public InventoryType inventoryType; 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); } public virtual void OnPointerClick(PointerEventData eventData) { //Move item from inventory to hand InventoryManager.Instance.InventoryToHand(); } //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); } }
Learn more about the virtual
keyword here.
Create a new script and call it HandInventorySlot
with the following:
HandInventorySlot.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.EventSystems; public class HandInventorySlot :MonoBehaviourInventorySlot { public override void OnPointerClick(PointerEventData eventData) { //Move item from hand to inventory InventoryManager.Instance.HandToInventory(); }// Start is called before the first frame update void Start() { } // Update is called once per frame public void Update() { }}
c. Distinguishing Inventory Slots from one another
This setup brings us into a problem: when the OnPointerClick
callback function is fired on InventorySlot
, how does it know which inventory slot it is supposed to be representing? We can easily fix this by assigning a variable that corresponds to its array index in UIManager
. Add the following to InventorySlot
:
InventorySlot.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI; public class InventorySlot : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler, IPointerClickHandler { ItemData itemToDisplay; public Image itemDisplayImage; public enum InventoryType { Item, Tool } //Determines which inventory section this slot is apart of. public InventoryType inventoryType; int slotIndex; 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); } public virtual void OnPointerClick(PointerEventData eventData) { //Move item from inventory to hand InventoryManager.Instance.InventoryToHand(); } //Set the Slot index public void AssignIndex(int slotIndex) { this.slotIndex = slotIndex; } //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); } }
Get UIManager
to iterate through its Item and Tool inventory slots and assign the indexes:
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("Status Bar")] //Tool equip slot on the status bar public Image toolEquipSlot; [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(); AssignSlotIndexes(); } //Iterate through the slot UI elements and assign it its reference slot index public void AssignSlotIndexes() { for (int i =0; i<toolSlots.Length; i++) { toolSlots[i].AssignIndex(i); itemSlots[i].AssignIndex(i); } } //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); //Get Tool Equip from InventoryManager ItemData equippedTool = InventoryManager.Instance.equippedTool; //Check if there is an item to display if (equippedTool != null) { //Switch the thumbnail over toolEquipSlot.sprite = equippedTool.thumbnail; toolEquipSlot.gameObject.SetActive(true); return; } toolEquipSlot.gameObject.SetActive(false); } //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; } }
Note: Only 1 for loop is used to assign the indexes because, in this tutorial, the number of Tool Slots and Item Slots are identical. If you are planning to make them different, you will need to set up separate for loops for each section in AssignSlotIndexes
.
You should be able to see this in the Inspector in Debug mode.
Article continues after the advertisement:
d. Displaying the Hand Equip Slots
For each of the Hand Slots under the ToolsPanel and the ItemsPanel, create another Image GameObject as its child called ‘Item Display‘. Make it slightly smaller than its parent:
On the Hand Slot GameObject, add the HandInventorySlot
script. Assign the Item Display Image we just created to the Item Display Image property in the Inspector.
To render it in UIManager
, we just have to pass a reference to the Hand Slots and get it to display in RenderInventory
:
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("Status Bar")] //Tool equip slot on the status bar public Image toolEquipSlot; [Header("Inventory System")] //The inventory panel public GameObject inventoryPanel; //The tool equip slot UI on the Inventory panel public HandInventorySlot toolHandSlot; //The tool slot UIs public InventorySlot[] toolSlots; //The item equip slot UI on the Inventory panel public HandInventorySlot itemHandSlot; //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(); AssignSlotIndexes(); } //Iterate through the slot UI elements and assign it its reference slot index public void AssignSlotIndexes() { for (int i =0; i<toolSlots.Length; i++) { toolSlots[i].AssignIndex(i); itemSlots[i].AssignIndex(i); } } //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); //Render the equipped slots toolHandSlot.Display(InventoryManager.Instance.equippedTool); itemHandSlot.Display(InventoryManager.Instance.equippedItem); //Get Tool Equip from InventoryManager ItemData equippedTool = InventoryManager.Instance.equippedTool; //Check if there is an item to display if (equippedTool != null) { //Switch the thumbnail over toolEquipSlot.sprite = equippedTool.thumbnail; toolEquipSlot.gameObject.SetActive(true); return; } toolEquipSlot.gameObject.SetActive(false); } //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; } }
Assign the Hand Slots to the newly declared variables.
e. Equipping (Inventory to Hand)
InventoryToHand
needs to know which section and inventory slot to change. Hence it must take in additional parameters. Add the following to 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; } } [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; //Equipping //Handles movement of item from Inventory to Hand public void InventoryToHand(int slotIndex, InventorySlot.InventoryType inventoryType) { } //Handles movement of item from Hand to Inventory public void HandToInventory() { } // Start is called before the first frame update void Start() { } // Update is called once per frame void Update() { } }
This will cause errors on all references to this function, so we need to pass the required parameters on InventorySlot
:
InventorySlot.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI; public class InventorySlot : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler, IPointerClickHandler { ItemData itemToDisplay; public Image itemDisplayImage; public enum InventoryType { Item, Tool } //Determines which inventory section this slot is apart of. public InventoryType inventoryType; int slotIndex; 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); } public virtual void OnPointerClick(PointerEventData eventData) { //Move item from inventory to hand InventoryManager.Instance.InventoryToHand(slotIndex, inventoryType); } //Set the Slot index public void AssignIndex(int slotIndex) { this.slotIndex = slotIndex; } //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); } }
To move the items from the Inventory to the Hand, we need to do the following:
- Check which section the slot is from (Items or Tools)
- Cache the
ItemData
in the Inventory Slot before we change it - Change the Inventory Slot’s
ItemData
to that of the Hand Slot’s - Change the Hand Slot’s
ItemData
to the cached variable from 1.
Add the following to the InventoryToHand
function in 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; } } [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; //Equipping //Handles movement of item from Inventory to Hand public void InventoryToHand(int slotIndex, InventorySlot.InventoryType inventoryType) { if(inventoryType == InventorySlot.InventoryType.Item) { //Cache the Inventory slot ItemData from InventoryManager ItemData itemToEquip = items[slotIndex]; //Change the Inventory Slot to the Hand's items[slotIndex] = equippedItem; //Change the Hand's Slot to the Inventory Slot's equippedItem = itemToEquip; } else { //Cache the Inventory slot ItemData from InventoryManager ItemData toolToEquip = tools[slotIndex]; //Change the Inventory Slot to the Hand's tools[slotIndex] = equippedTool; //Change the Hand's Slot to the Inventory Slot's equippedTool = toolToEquip; } //Update the changes to the UI UIManager.Instance.RenderInventory(); } //Handles movement of item from Hand to Inventory public void HandToInventory() { } // Start is called before the first frame update void Start() { } // Update is called once per frame void Update() { } }
It should be working now when you playtest the game.
Article continues after the advertisement:
f. Unequipping (Hand to Inventory)
Unlike the Inventory Slots, there is no need to keep track of any slot index as there is only 1 Hand Slot for each Inventory section. Hence, we only need to add the inventory type parameter to the HandToInventory
function of 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; } } [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; //Equipping //Handles movement of item from Inventory to Hand public void InventoryToHand(int slotIndex, InventorySlot.InventoryType inventoryType) { if(inventoryType == InventorySlot.InventoryType.Item) { //Cache the Inventory slot ItemData from InventoryManager ItemData itemToEquip = items[slotIndex]; //Change the Inventory Slot to the Hand's items[slotIndex] = equippedItem; //Change the Hand's Slot to the Inventory Slot's equippedItem = itemToEquip; } else { //Cache the Inventory slot ItemData from InventoryManager ItemData toolToEquip = tools[slotIndex]; //Change the Inventory Slot to the Hand's tools[slotIndex] = equippedTool; //Change the Hand's Slot to the Inventory Slot's equippedTool = toolToEquip; } //Update the changes to the UI UIManager.Instance.RenderInventory(); } //Handles movement of item from Hand to Inventory public void HandToInventory(InventorySlot.InventoryType inventoryType) { } // Start is called before the first frame update void Start() { } // Update is called once per frame void Update() { } }
In HandInventorySlot
, supply the HandToInventory
function with its required parameter:
HandInventorySlot.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
public class HandInventorySlot : InventorySlot
{
public override void OnPointerClick(PointerEventData eventData)
{
//Move item from hand to inventory
InventoryManager.Instance.HandToInventory(inventoryType);
}
}
This inventoryType
variable is inherited from the class’ parent, InventorySlot
.
We can find the first available Inventory Slot to move the Hand Slot’s item over easily with a for loop. The logic of the loop is pretty simple:
- Check if the slot is empty
- Set the empty slot’s
ItemData
to the Hand Slot’s - Empty out the Hand Slot
- Break out of the loop
To this end, add the following to the HandToInventory
function of 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; } } [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; //Equipping //Handles movement of item from Inventory to Hand public void InventoryToHand(int slotIndex, InventorySlot.InventoryType inventoryType) { if(inventoryType == InventorySlot.InventoryType.Item) { //Cache the Inventory slot ItemData from InventoryManager ItemData itemToEquip = items[slotIndex]; //Change the Inventory Slot to the Hand's items[slotIndex] = equippedItem; //Change the Hand's Slot to the Inventory Slot's equippedItem = itemToEquip; } else { //Cache the Inventory slot ItemData from InventoryManager ItemData toolToEquip = tools[slotIndex]; //Change the Inventory Slot to the Hand's tools[slotIndex] = equippedTool; //Change the Hand's Slot to the Inventory Slot's equippedTool = toolToEquip; } //Update the changes to the UI UIManager.Instance.RenderInventory(); } //Handles movement of item from Hand to Inventory public void HandToInventory(InventorySlot.InventoryType inventoryType) { if(inventoryType == InventorySlot.InventoryType.Item) { //Iterate through each inventory slot and find an empty slot for(int i =0; i < items.Length; i++) { if(items[i] == null) { //Send the equipped item over to its new slot items[i] = equippedItem; //Remove the item from the hand equippedItem = null; break; } } } else { //Iterate through each inventory slot and find an empty slot for (int i = 0; i < tools.Length; i++) { if (tools[i] == null) { //Send the equipped item over to its new slot tools[i] = equippedTool; //Remove the item from the hand equippedTool = null; break; } } } //Update changes in the inventory UIManager.Instance.RenderInventory(); } // Start is called before the first frame update void Start() { } // Update is called once per frame void Update() { } }
To test if it works in both sections, add something to the Items array in InventoryManager:
Run the game and test to see if it works.
Article continues after the advertisement:
3. Farmland interaction with tools
In Part 3, we temporarily made the Land object change its state to Farmland on Interact
. Now that we have a working Inventory system set up, let’s get it to change states based on the equipped tool.
However, we have 2 subclasses within the Tools subcategory: the Equipment and Seeds. Out of the 2, only the Equipment subclass can directly influence the Land’s state. Hence, we will need to check if the player’s equipped tool is of the Equipment subclass first. This can be done by casting the Tool Hand Slot’s ItemData
into EquipmentData
and checking if it returns a null value.
//Check the player's tool slot ItemData toolSlot = InventoryManager.Instance.equippedTool; //Try casting the itemdata in the toolslot as EquipmentData EquipmentData equipmentTool = toolSlot as EquipmentData; //Check if it is of type EquipmentData if(equipmentTool != null) { //This will be executed if the cast is successful. }
Learn more about casting objects safely here.
Once we can verify that it is indeed of subclass Equipment
, we can easily set the Land’s state from a switch statement. Add the following to the Land
script:
Land.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Land : MonoBehaviour { public enum LandStatus { Soil, Farmland, Watered } public LandStatus landStatus; public Material soilMat, farmlandMat, wateredMat; new Renderer renderer; //The selection gameobject to enable when the player is selecting the land public GameObject select; // Start is called before the first frame update void Start() { //Get the renderer component renderer = GetComponent<Renderer>(); //Set the land to soil by default SwitchLandStatus(LandStatus.Soil); //Deselect the land by default Select(false); } public void SwitchLandStatus(LandStatus statusToSwitch) { //Set land status accordingly landStatus = statusToSwitch; Material materialToSwitch = soilMat; //Decide what material to switch to switch (statusToSwitch) { case LandStatus.Soil: //Switch to the soil material materialToSwitch = soilMat; break; case LandStatus.Farmland: //Switch to farmland material materialToSwitch = farmlandMat; break; case LandStatus.Watered: //Switch to watered material materialToSwitch = wateredMat; break; } //Get the renderer to apply the changes renderer.material = materialToSwitch; } public void Select(bool toggle) { select.SetActive(toggle); } //When the player presses the interact button while selecting this land public void Interact() {//Interaction SwitchLandStatus(LandStatus.Farmland);//Check the player's tool slot ItemData toolSlot = InventoryManager.Instance.equippedTool; //Try casting the itemdata in the toolslot as EquipmentData EquipmentData equipmentTool = toolSlot as EquipmentData; //Check if it is of type EquipmentData if(equipmentTool != null) { //Get the tool type EquipmentData.ToolType toolType = equipmentTool.toolType; switch (toolType) { case EquipmentData.ToolType.Hoe: SwitchLandStatus(LandStatus.Farmland); break; case EquipmentData.ToolType.WateringCan: SwitchLandStatus(LandStatus.Watered); break; } } } }
Go to each of the EquipmentData
ScriptableObjects and ensure that their respective Tool Types are correctly assigned.
When the player interacts with the Land with a Hoe equipped, it changes its state to Farmland.
When the player interacts with the Land with a Watering Can equipped, it changes its state to Watered.
Article continues after the advertisement:
Conclusion
With that, we are able to equip and unequip items in the Inventory screen. Here is the final code for all the scripts we have worked with today:
HandInventorySlot.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
public class HandInventorySlot : InventorySlot
{
public override void OnPointerClick(PointerEventData eventData)
{
//Move item from hand to inventory
InventoryManager.Instance.HandToInventory(inventoryType);
}
}
InventorySlot.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI; public class InventorySlot : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler, IPointerClickHandler { ItemData itemToDisplay; public Image itemDisplayImage; public enum InventoryType { Item, Tool } //Determines which inventory section this slot is apart of. public InventoryType inventoryType; int slotIndex; 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); } public virtual void OnPointerClick(PointerEventData eventData) { //Move item from inventory to hand InventoryManager.Instance.InventoryToHand(slotIndex, inventoryType); } //Set the Slot index public void AssignIndex(int slotIndex) { this.slotIndex = slotIndex; } //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); } }
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("Status Bar")] //Tool equip slot on the status bar public Image toolEquipSlot; [Header("Inventory System")] //The inventory panel public GameObject inventoryPanel; //The tool equip slot UI on the Inventory panel public HandInventorySlot toolHandSlot; //The tool slot UIs public InventorySlot[] toolSlots; //The item equip slot UI on the Inventory panel public HandInventorySlot itemHandSlot; //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(); AssignSlotIndexes(); } //Iterate through the slot UI elements and assign it its reference slot index public void AssignSlotIndexes() { for (int i =0; i<toolSlots.Length; i++) { toolSlots[i].AssignIndex(i); itemSlots[i].AssignIndex(i); } } //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); //Render the equipped slots toolHandSlot.Display(InventoryManager.Instance.equippedTool); itemHandSlot.Display(InventoryManager.Instance.equippedItem); //Get Tool Equip from InventoryManager ItemData equippedTool = InventoryManager.Instance.equippedTool; //Check if there is an item to display if (equippedTool != null) { //Switch the thumbnail over toolEquipSlot.sprite = equippedTool.thumbnail; toolEquipSlot.gameObject.SetActive(true); return; } toolEquipSlot.gameObject.SetActive(false); } //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; } }
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; //Equipping //Handles movement of item from Inventory to Hand public void InventoryToHand(int slotIndex, InventorySlot.InventoryType inventoryType) { if(inventoryType == InventorySlot.InventoryType.Item) { //Cache the Inventory slot ItemData from InventoryManager ItemData itemToEquip = items[slotIndex]; //Change the Inventory Slot to the Hand's items[slotIndex] = equippedItem; //Change the Hand's Slot to the Inventory Slot's equippedItem = itemToEquip; } else { //Cache the Inventory slot ItemData from InventoryManager ItemData toolToEquip = tools[slotIndex]; //Change the Inventory Slot to the Hand's tools[slotIndex] = equippedTool; //Change the Hand's Slot to the Inventory Slot's equippedTool = toolToEquip; } //Update the changes to the UI UIManager.Instance.RenderInventory(); } //Handles movement of item from Hand to Inventory public void HandToInventory(InventorySlot.InventoryType inventoryType) { if(inventoryType == InventorySlot.InventoryType.Item) { //Iterate through each inventory slot and find an empty slot for(int i =0; i < items.Length; i++) { if(items[i] == null) { //Send the equipped item over to its new slot items[i] = equippedItem; //Remove the item from the hand equippedItem = null; break; } } } else { //Iterate through each inventory slot and find an empty slot for (int i = 0; i < tools.Length; i++) { if (tools[i] == null) { //Send the equipped item over to its new slot tools[i] = equippedTool; //Remove the item from the hand equippedTool = null; break; } } } //Update changes in the inventory UIManager.Instance.RenderInventory(); } // Start is called before the first frame update void Start() { } // Update is called once per frame void Update() { } }
Land.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Land : MonoBehaviour { public enum LandStatus { Soil, Farmland, Watered } public LandStatus landStatus; public Material soilMat, farmlandMat, wateredMat; new Renderer renderer; //The selection gameobject to enable when the player is selecting the land public GameObject select; // Start is called before the first frame update void Start() { //Get the renderer component renderer = GetComponent<Renderer>(); //Set the land to soil by default SwitchLandStatus(LandStatus.Soil); //Deselect the land by default Select(false); } public void SwitchLandStatus(LandStatus statusToSwitch) { //Set land status accordingly landStatus = statusToSwitch; Material materialToSwitch = soilMat; //Decide what material to switch to switch (statusToSwitch) { case LandStatus.Soil: //Switch to the soil material materialToSwitch = soilMat; break; case LandStatus.Farmland: //Switch to farmland material materialToSwitch = farmlandMat; break; case LandStatus.Watered: //Switch to watered material materialToSwitch = wateredMat; break; } //Get the renderer to apply the changes renderer.material = materialToSwitch; } public void Select(bool toggle) { select.SetActive(toggle); } //When the player presses the interact button while selecting this land public void Interact() { //Check the player's tool slot ItemData toolSlot = InventoryManager.Instance.equippedTool; //Try casting the itemdata in the toolslot as EquipmentData EquipmentData equipmentTool = toolSlot as EquipmentData; //Check if it is of type EquipmentData if(equipmentTool != null) { //Get the tool type EquipmentData.ToolType toolType = equipmentTool.toolType; switch (toolType) { case EquipmentData.ToolType.Hoe: SwitchLandStatus(LandStatus.Farmland); break; case EquipmentData.ToolType.WateringCan: SwitchLandStatus(LandStatus.Watered); break; } } } }
Article continues after the advertisement:
I feel like I’ve followed things correctly. I even overwrote my code by copy and pasting yours.
I’m getting a bug where, now matter which inventory slot I click on either side, it switches item 0 to hand and then back.
Please help if you can
Hi Kraege, if the code is exactly the same, it could be a problem with how the slots are assigned to UIManager in the Inspector. Try checking the toolSlots and itemSlots values of UIManager and see if they correspond to the right slots.
Also, try running the game and enabling ‘Debug mode’ in the inspector and check the slotIndex value of each of the InventorySlots. Only the first slots should have a value of 0. If not, then the problem lies somewhere in the AssignSlotIndexes() function in UIManager.