Ever wanted to create a game like Harvest Moon in Unity? Check out Part 9 of our guide here, where we go through how to improve on our current Inventory system. You can also find Part 8 of our guide here, where we went through how to make crops that can be harvested multiple times.
A link to a package containing the project files up to Part 9 of this tutorial series can also be found at the end of this article, exclusive to Patreon supporters only.
Disclaimer: There was a bug that came out in this part. See the fix here.
1. Creating the ItemSlotData class
Currently, each item takes up an individual slot in our inventory as we are using ScriptableObject to directly store information, making the inventory fill up quickly. Today, we will be fixing it so that the same item can be stacked instead.

a. Setting up constructors
First, create a new class called ItemSlotData to store information about our inventory slots (take note that it is not a MonoBehavior class). Open it and create a constructor that takes in ItemData and quantity:
ItemSlotData.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; public class ItemSlotData:MonoBehaviour{ public ItemData itemData; public int quantity; //Class Constructor public ItemSlotData(ItemData itemData, int quantity) { this.itemData = itemData; this.quantity = quantity; } }
Whenever we pick up an item, we would always assume that we are holding 1 item. Similarly, we are going to set our default quantity to 1 whenever there in an item in the inventory slot instead of it being 0:
ItemSlotData.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ItemSlotData
{
public ItemData itemData;
public int quantity;
//Class Constructor
public ItemSlotData(ItemData itemData, int quantity)
{
this.itemData = itemData;
this.quantity = quantity;
}
//Automatically construct the class with the item data of quantity 1
public ItemSlotData(ItemData itemData)
{
this.itemData = itemData;
quantity = 1;
}
}
b. Quantity Management
Next, we are going to control their quantity of an item so they can be stacked. There are 3 cases that we need to manage:
- Increasing quantity
- Decreasing quantity
- When quantity is 0 or when the slot is empty
For case 1, we are creating 2 functions to handle it. The first (and main) function is AddQuantity(int amountToAdd), which adds any amount to the current quantity. The other function is AddQuantity(), a shortcut for adding one to the current stack each time. Add both functions to ItemSlotData:
ItemSlotData.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ItemSlotData
{
public ItemData itemData;
public int quantity;
//Class Constructor
public ItemSlotData(ItemData itemData, int quantity)
{
this.itemData = itemData;
this.quantity = quantity;
}
//Automatically construct the class with the item data of quantity 1
public ItemSlotData(ItemData itemData)
{
this.itemData = itemData;
quantity = 1;
}
//Stacking System
//Shortcut function to add 1 to the stack
public void AddQuantity()
{
AddQuantity(1);
}
//Add a specified amount to the stack
public void AddQuantity(int amountToAdd)
{
quantity += amountToAdd;
}
}
To handle decreasing quantity, do the opposite of the above and make a function that removes 1 from the current stack each time. This will be called whenever something is consumed or removed from our inventory.
ItemSlotData.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ItemSlotData
{
public ItemData itemData;
public int quantity;
//Class Constructor
public ItemSlotData(ItemData itemData, int quantity)
{
this.itemData = itemData;
this.quantity = quantity;
}
//Automatically construct the class with the item data of quantity 1
public ItemSlotData(ItemData itemData)
{
this.itemData = itemData;
quantity = 1;
}
//Stacking System
//Shortcut function to add 1 to the stack
public void AddQuantity()
{
AddQuantity(1);
}
//Add a specified amount to the stack
public void AddQuantity(int amountToAdd)
{
quantity += amountToAdd;
}
public void Remove()
{
quantity--;
}
}
Whenever something is removed from our inventory, there will be a chance that there is nothing in the stack, which brings us to the third case.
When we have a quantity of 0, we cannot leave the itemData in the slot. At the same time, when there are no items in the slot, it cannot have the default quantity of 1. Thus, we need a function ValidateQuantity() to check for both cases and Empty() to clear everything if there is something in it.
ItemSlotData.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ItemSlotData
{
public ItemData itemData;
public int quantity;
//Class Constructor
public ItemSlotData(ItemData itemData, int quantity)
{
this.itemData = itemData;
this.quantity = quantity;
ValidateQuantity();
}
//Automatically construct the class with the item data of quantity 1
public ItemSlotData(ItemData itemData)
{
this.itemData = itemData;
quantity = 1;
ValidateQuantity();
}
//Stacking System
//Shortcut function to add 1 to the stack
public void AddQuantity()
{
AddQuantity(1);
}
//Add a specified amount to the stack
public void AddQuantity(int amountToAdd)
{
quantity += amountToAdd;
}
public void Remove()
{
quantity--;
ValidateQuantity();
}
//Do checks to see if the values make sense
private void ValidateQuantity()
{
if (quantity <= 0 || itemData == null)
{
Empty();
}
}
//Empties out the item slot
public void Empty()
{
itemData = null;
quantity = 0;
}
}
2. Refactoring the Code
a. Changing over from ItemData to ItemSlotData
To rename all references in a script at the same time, highlight one of the variable and rename them and click apply. However, this does not apply to Type, such as ItemSlotData. Those need to be renamed manually
- tools -> toolSlots
- equippedTool ->equippedToolSlot
- items -> itemsSlot
- equippedItem -> equippedItemSlot
- (type) ItemSlot -> ItemSlotData
There will be a lot of errors that appear because we have not encapsulate enough of the code. You can comment out the code in InventoryToHand() and HandToInventory() to lessen some of the errors as we will be rewriting those functions later.
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
private ItemSlotData[] toolSlots = new ItemSlotData[8];
//Tool in the player's hand
private ItemSlotData equippedToolSlot = null;
[Header("Items")]
//Item Slots
private ItemSlotData[] itemSlots = new ItemSlotData[8];
//Item in the player's hand
private ItemSlotData equippedItemSlot = null;
//The transform for the player to hold items in the scene
public Transform handPoint;
//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(InventorySlot.InventoryType inventoryType)
{
...
}
//Render the player's equipped item in the scene
public void RenderHand()
{
...
}
// Start is called before the first frame updat
void Start()
{
}
// Update is called once per frame
void Update()
{
}
}
b. Getters and setters
The first thing we want to do is to be able to access our ItemData in the item slot. To do that, use the InventoryType to determine whether it is a tool or on item and return the appropriate variable. The same applies for ItemDataSlot. However, the ItenDataSlot we are retrieving is only the equipped slots. Add the following code 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
private ItemSlotData[] toolSlots = new ItemSlotData[8];
//Tool in the player's hand
private ItemSlotData equippedToolSlot = null;
[Header("Items")]
//Item Slots
private ItemSlotData[] itemSlots = new ItemSlotData[8];
//Item in the player's hand
private ItemSlotData equippedItemSlot = null;
//The transform for the player to hold items in the scene
public Transform handPoint;
//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(InventorySlot.InventoryType inventoryType)
{
...
}
//Render the player's equipped item in the scene
public void RenderHand()
{
//Reset objects on the hand
if(handPoint.childCount > 0)
{
Destroy(handPoint.GetChild(0).gameObject);
}
//Check if the player has anything equipped
if(equippedItemSlot != null)
{
//Instantiate the game model on the player's hand and put it on the scene
Instantiate(GetEquippedSlotItem(InventorySlot.InventoryType.Item).gameModel, handPoint);
}
}
//Inventory Slot Data
#region Gets and Checks
//Get the slot item (ItemData)
public ItemData GetEquippedSlotItem(InventorySlot.InventoryType inventoryType)
{
if(inventoryType == InventorySlot.InventoryType.Item)
{
return equippedItemSlot.itemData;
}
return equippedToolSlot.itemData;
}
//Get function for the slots (ItemSlotData)
public ItemSlotData GetEquippedSlot(InventorySlot.InventoryType inventoryType)
{
if (inventoryType == InventorySlot.InventoryType.Item)
{
return equippedItemSlot;
}
return equippedToolSlot;
}
#endregion
// Start is called before the first frame upda
void Start()
{
}
// Update is called once per frame
void Update()
{
}
}
c. Accounting for quantity in InventorySlot
Now, you might notice that most of the problem is on the Display() function in InventorySlot. This function handles the UI in the inventory screen. As we now have item stacks, the following changes must be made: Take in an int and name it “quantity” to display the quantity of the stack; change ItemData to ItemSlotData as the item to display and the quantity are going to be from the item slots.
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;
int quantity;
public Image itemDisplayImage;
public Text quantityText;
public enum InventoryType
{
Item, Tool
}
//Determines which inventory section this slot is apart of.
public InventoryType inventoryType;
int slotIndex;
public void Display(ItemSlotData itemSlot)
{
//Set the variables accordingly
itemToDisplay = itemSlot.itemData;
quantity = itemSlot.quantity;
//By default, the quantity text should not show
quantityText.text = "";
//Check if there is an item to display
if(itemToDisplay != null)
{
//Switch the thumbnail over
itemDisplayImage.sprite = itemToDisplay.thumbnail;
this.itemToDisplay = itemToDisplay;
//Display the stack quantity if there is more than 1 in the stack
if(quantity > 1)
{
quantityText.text = quantity.ToString();
}
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);
}
}
d. Fixing errors in UIManager
Back in UIManager, we need to use Get functions to get all of the equipped slots variables since they are private in InventoryManager. Do these changes to fix the errors:
UIManager.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class UIManager : MonoBehaviour, ITimeTracker
{
public static UIManager Instance { get; private set; }
[Header("Status Bar")]
//Tool equip slot on the status bar
public Image toolEquipSlot;
//Tool Quantity text on the status bar
public Text toolQuantityText;
//Time UI
public Text timeText;
public Text dateText;
[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();
//Add UIManager to the list of objects TimeManager will notify when the time updates
TimeManager.Instance.RegisterTracker(this);
}
//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(ItemSlotData[] inventoryToolSlots, ItemSlotData[] inventoryItemSlots)
{
//Get the inventory tool slots from Inventory Manager
ItemData[] inventoryToolSlots = InventoryManager.Instance.toolSlots;
//Get the inventory item slots from Inventory Manager
ItemData[] inventoryItemSlots = InventoryManager.Instance.itemSlots;
//Render the Tool section
RenderInventoryPanel(inventoryToolSlots, toolSlots);
//Render the Item section
RenderInventoryPanel(inventoryItemSlots, itemSlots);
//Render the equipped slots
toolHandSlot.Display(InventoryManager.Instance.GetEquippedSlot(InventorySlot.InventoryType.Tool));
itemHandSlot.Display(InventoryManager.Instance.GetEquippedSlot(InventorySlot.InventoryType.Item));
//Get Tool Equip from InventoryManager
ItemData equippedTool = InventoryManager.Instance.GetEquippedSlotItem(InventorySlot.InventoryType.Tool);
//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(ItemSlotData[] 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;
}
//Callback to handle the UI for time
public void ClockUpdate(GameTimestamp timestamp)
{
//Handle the time
//Get the hours and minutes
int hours = timestamp.hour;
int minutes = timestamp.minute;
//AM or PM
string prefix = "AM ";
//Convert hours to 12 hour clock
if (hours > 12)
{
//Time becomes PM
prefix = "PM ";
hours = hours - 12;
Debug.Log(hours);
}
//Format it for the time text display
timeText.text = prefix + hours + ":" + minutes.ToString("00");
//Handle the Date
int day = timestamp.day;
string season = timestamp.season.ToString();
string dayOfTheWeek = timestamp.GetDayOfTheWeek().ToString();
//Format it for the date text display
dateText.text = season + " " + day + " (" + dayOfTheWeek +")";
}
}
We also need to create a Get function in InventoryManager to get the inventory slots because RenderInventory() is not only called from InventoryManager; it is also called in UIManager, which does not have a reference for those slots.
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
private ItemSlotData[] toolSlots = new ItemSlotData[8];
//Tool in the player's hand
private ItemSlotData equippedToolSlot = null;
[Header("Items")]
//Item Slots
private ItemSlotData[] itemSlots = new ItemSlotData[8];
//Item in the player's hand
private ItemSlotData equippedItemSlot = null;
//The transform for the player to hold items in the scene
public Transform handPoint;
//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(InventorySlot.InventoryType inventoryType)
{
...
}
//Render the player's equipped item in the scene
public void RenderHand()
{
//Reset objects on the hand
if(handPoint.childCount > 0)
{
Destroy(handPoint.GetChild(0).gameObject);
}
//Check if the player has anything equipped
if(equippedItemSlot != null)
{
//Instantiate the game model on the player's hand and put it on the scene
Instantiate(GetEquippedSlotItem(InventorySlot.InventoryType.Item).gameModel, handPoint);
}
}
//Inventory Slot Data
#region Gets and Checks
//Get the slot item (ItemData)
public ItemData GetEquippedSlotItem(InventorySlot.InventoryType inventoryType)
{
if(inventoryType == InventorySlot.InventoryType.Item)
{
return equippedItemSlot.itemData;
}
return equippedToolSlot.itemData;
}
//Get function for the slots (ItemSlotData)
public ItemSlotData GetEquippedSlot(InventorySlot.InventoryType inventoryType)
{
if (inventoryType == InventorySlot.InventoryType.Item)
{
return equippedItemSlot;
}
return equippedToolSlot;
}
//Get function for the inventory slots
public ItemSlotData[] GetInventorySlots(InventorySlot.InventoryType inventoryType)
{
if (inventoryType == InventorySlot.InventoryType.Item)
{
return itemSlots;
}
return toolSlots;
}
#endregion
// Start is called before the first frame up
void Start()
{
}
// Update is called once per frame
void Update()
{
}
}
Next, remove the parameters from RenderInventory() in and declare each of the variables inside the function instead. To do that, add the following code to UIManager:
UIManager.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class UIManager : MonoBehaviour, ITimeTracker
{
public static UIManager Instance { get; private set; }
[Header("Status Bar")]
//Tool equip slot on the status bar
public Image toolEquipSlot;
//Tool Quantity text on the status bar
public Text toolQuantityText;
//Time UI
public Text timeText;
public Text dateText;
[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();
//Add UIManager to the list of objects TimeManager will notify when the time updates
TimeManager.Instance.RegisterTracker(this);
}
//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(ItemSlotData[] inventoryToolSlots, ItemSlotData[] inventoryItemSlots)
{
//Get the respective slots to process
ItemSlotData[] inventoryToolSlots = InventoryManager.Instance.GetInventorySlots(InventorySlot.InventoryType.Tool);
ItemSlotData[] inventoryItemSlots = InventoryManager.Instance.GetInventorySlots(InventorySlot.InventoryType.Item);
//Render the Tool section
RenderInventoryPanel(inventoryToolSlots, toolSlots);
//Render the Item section
RenderInventoryPanel(inventoryItemSlots, itemSlots);
//Render the equipped slots
toolHandSlot.Display(InventoryManager.Instance.GetEquippedSlot(InventorySlot.InventoryType.Tool));
itemHandSlot.Display(InventoryManager.Instance.GetEquippedSlot(InventorySlot.InventoryType.Item));
//Get Tool Equip from InventoryManager
ItemData equippedTool = InventoryManager.Instance.GetEquippedSlotItem(InventorySlot.InventoryType.Tool);
//Check if there is an item to display
if (equippedTool != null)
{
//Switch the thumbnail over
toolEquipSlot.sprite = equippedTool.thumbnail;
toolEquipSlot.gameObject.SetActive(true);
}
toolEquipSlot.gameObject.SetActive(false);
}
//Iterate through a slot in a section and display them in the UI
void RenderInventoryPanel(ItemSlotData[] 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;
}
//Callback to handle the UI for time
public void ClockUpdate(GameTimestamp timestamp)
{
//Handle the time
//Get the hours and minutes
int hours = timestamp.hour;
int minutes = timestamp.minute;
//AM or PM
string prefix = "AM ";
//Convert hours to 12 hour clock
if (hours > 12)
{
//Time becomes PM
prefix = "PM ";
hours = hours - 12;
Debug.Log(hours);
}
//Format it for the time text display
timeText.text = prefix + hours + ":" + minutes.ToString("00");
//Handle the Date
int day = timestamp.day;
string season = timestamp.season.ToString();
string dayOfTheWeek = timestamp.GetDayOfTheWeek().ToString();
//Format it for the date text display
dateText.text = season + " " + day + " (" + dayOfTheWeek +")";
}
}
e. Fixing errors in PlayerInteraction
Moving onto PlayerInteraction, all the null checks have errors in this script. To fix it, create a boolean function inside InventoryManager to check for null instead. The code is the same as GetInventorySlots(), only that we are checking if the equippedItemSlot is not null instead of just getting the itemSlots, the same applies for equippedToolSlot.
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
private ItemSlotData[] toolSlots = new ItemSlotData[8];
//Tool in the player's hand
private ItemSlotData equippedToolSlot = null;
[Header("Items")]
//Item Slots
private ItemSlotData[] itemSlots = new ItemSlotData[8];
//Item in the player's hand
private ItemSlotData equippedItemSlot = null;
//The transform for the player to hold items in the scene
public Transform handPoint;
//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(InventorySlot.InventoryType inventoryType)
{
...
}
//Render the player's equipped item in the scene
public void RenderHand()
{
...
}
//Inventory Slot Data
#region Gets and Checks
//Get the slot item (ItemData)
public ItemData GetEquippedSlotItem(InventorySlot.InventoryType inventoryType)
{
if(inventoryType == InventorySlot.InventoryType.Item)
{
return equippedItemSlot.itemData;
}
return equippedToolSlot.itemData;
}
//Get function for the slots (ItemSlotData)
public ItemSlotData GetEquippedSlot(InventorySlot.InventoryType inventoryType)
{
if (inventoryType == InventorySlot.InventoryType.Item)
{
return equippedItemSlot;
}
return equippedToolSlot;
}
//Get function for the inventory slots
public ItemSlotData[] GetInventorySlots(InventorySlot.InventoryType inventoryType)
{
if (inventoryType == InventorySlot.InventoryType.Item)
{
return itemSlots;
}
return toolSlots;
}
//Check if a hand slot has an item
public bool SlotEquipped(InventorySlot.InventoryType inventoryType)
{
if (inventoryType == InventorySlot.InventoryType.Item)
{
return equippedItemSlot != null;
}
return equippedToolSlot != null;
}
#endregion
// Start is called before the first frame upda
void Start()
{
}
// Update is called once per frame
void Update()
{
}
}
Back in PlayerInteraction, we use the function we just created to check if the player has an equipped item. Do this in both functions Interact() and ItemInteract().
PlayerInteraction.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerInteraction : MonoBehaviour
{
PlayerController playerController;
//The land the player is currently selecting
Land selectedLand = null;
//The interactable object the player is currently selecting
InteractableObject selectedInteractable = null;
// Start is called before the first frame update
void Start()
{
//Get access to our PlayerController component
playerController = transform.parent.GetComponent<PlayerController>();
}
// Update is called once per frame
void Update()
{
RaycastHit hit;
if(Physics.Raycast(transform.position, Vector3.down,out hit, 1))
{
OnInteractableHit(hit);
}
}
//Handles what happens when the interaction raycast hits something interactable
void OnInteractableHit(RaycastHit hit)
{
...
}
//Handles the selection process of the land
void SelectLand(Land land)
{
...
}
//Triggered when the player presses the tool button
public void Interact()
{
//The player shouldn't be able to use his tool when he has his hands full with an item
if(InventoryManager.Instance.SlotEquipped(InventorySlot.InventoryType.Item))
{
return;
}
//Check if the player is selecting any land
if(selectedLand != null)
{
selectedLand.Interact();
return;
}
Debug.Log("Not on any land!");
}
//Triggered when the player presses the item interact button
public void ItemInteract()
{
//If the player is holding something, keep it in his inventory
if(InventoryManager.Instance.SlotEquipped(InventorySlot.InventoryType.Item))
{
InventoryManager.Instance.HandToInventory(InventorySlot.InventoryType.Item);
return;
}
//If the player isn't holding anything, pick up an item
//Check if there is an interactable selected
if (selectedInteractable != null)
{
//Pick it up
selectedInteractable.Pickup();
}
}
}
f. Fixing errors in Land
There is only a small thing to tweak here:
Land.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Land : MonoBehaviour, ITimeTracker
{
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;
//Cache the time the land was watered
GameTimestamp timeWatered;
[Header("Crops")]
//The crop prefab to instantiate
public GameObject cropPrefab;
//The crop currently planted on the land
CropBehaviour cropPlanted = null;
// 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);
//Add this to TimeManager's Listener list
TimeManager.Instance.RegisterTracker(this);
}
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;
//Cache the time it was watered
timeWatered = TimeManager.Instance.GetGameTimestamp();
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.GetEquippedSlotItem(InventorySlot.InventoryType.Tool);
//If there's nothing equipped, return
if (!InventoryManager.Instance.SlotEquipped(InventorySlot.InventoryType.Tool))
{
return;
}
//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;
case EquipmentData.ToolType.Shovel:
//Remove the crop from the land
if(cropPlanted != null)
{
Destroy(cropPlanted.gameObject);
}
break;
}
//We don't need to check for seeds if we have already confirmed the tool to be an equipment
return;
}
//Try casting the itemdata in the toolslot as SeedData
SeedData seedTool = toolSlot as SeedData;
///Conditions for the player to be able to plant a seed
///1: He is holding a tool of type SeedData
///2: The Land State must be either watered or farmland
///3. There isn't already a crop that has been planted
if(seedTool != null && landStatus != LandStatus.Soil && cropPlanted == null)
{
//Instantiate the crop object parented to the land
GameObject cropObject = Instantiate(cropPrefab, transform);
//Move the crop object to the top of the land gameobject
cropObject.transform.position = new Vector3(transform.position.x, 0, transform.position.z);
//Access the CropBehaviour of the crop we're going to plant
cropPlanted = cropObject.GetComponent<CropBehaviour>();
//Plant it with the seed's information
cropPlanted.Plant(seedTool);
//Consume the item
InventoryManager.Instance.ConsumeItem(InventoryManager.Instance.GetEquippedSlot(InventorySlot.InventoryType.Tool));
}
}
public void ClockUpdate(GameTimestamp timestamp)
{
//Checked if 24 hours has passed since last watered
if(landStatus == LandStatus.Watered)
{
//Hours since the land was watered
int hoursElapsed = GameTimestamp.CompareTimestamps(timeWatered, timestamp);
Debug.Log(hoursElapsed + " hours since this was watered");
//Grow the planted crop, if any
if(cropPlanted != null)
{
cropPlanted.Grow();
}
if(hoursElapsed > 24)
{
//Dry up (Switch back to farmland)
SwitchLandStatus(LandStatus.Farmland);
}
}
//Handle the wilting of the plant when the land is not watered
if(landStatus != LandStatus.Watered && cropPlanted != null)
{
//If the crop has already germinated, start the withering
if (cropPlanted.cropState != CropBehaviour.CropState.Seed)
{
cropPlanted.Wither();
}
}
}
}
g. Organise code
Lastly, we are going to use some getter functions to organise some of our code.
Firstly, we are creating 2 functions to handle the equipping of the player’s hand slots since they will be picking things up.
IsTool()is for discerning theInventoryTypetheItemDatais fromEquipHandSlot()is for equipping the hand slots
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
private ItemSlotData[] toolSlots = new ItemSlotData[8];
//Tool in the player's hand
private ItemSlotData equippedToolSlot = null;
[Header("Items")]
//Item Slots
private ItemSlotData[] itemSlots = new ItemSlotData[8];
//Item in the player's hand
private ItemSlotData equippedItemSlot = null;
//The transform for the player to hold items in the scene
public Transform handPoint;
//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(InventorySlot.InventoryType inventoryType)
{
...
}
//Render the player's equipped item in the scene
public void RenderHand()
{
...
}
//Inventory Slot Data
#region Gets and Checks
//Get the slot item (ItemData)
public ItemData GetEquippedSlotItem(InventorySlot.InventoryType inventoryType)
{
...
}
//Get function for the slots (ItemSlotData)
public ItemSlotData GetEquippedSlot(InventorySlot.InventoryType inventoryType)
{
...
}
//Get function for the inventory slots
public ItemSlotData[] GetInventorySlots(InventorySlot.InventoryType inventoryType)
{
...
}
//Check if a hand slot has an item
public bool SlotEquipped(InventorySlot.InventoryType inventoryType)
{
...
}
//Check if the item is a tool
public bool IsTool(ItemData item)
{
//Is it equipment?
//Try to cast it as equipment first
EquipmentData equipment = item as EquipmentData;
if(equipment != null)
{
return true;
}
//Is it a seed?
//Try to cast it as a seed
SeedData seed = item as SeedData;
//If the seed is not null it is a seed
return seed != null;
}
#endregion
//Equip the hand slot with an ItemData (Will overwrite the slot)
public void EquipHandSlot(ItemData item)
{
if (IsTool(item))
{
equippedToolSlot = new ItemSlotData(item);
} else
{
equippedItemSlot = new ItemSlotData(item);
}
}
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
}
}
With those changes, we cannot directly assign equippedItemSlot to the items anymore. Instead, we need to switch it to the functions that we have just created. Apply the following changes to InteractableObjects and RegrowableHarvestBehavior:
InteractableObject.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class InteractableObject : MonoBehaviour
{
//The item information the GameObject is supposed to represent
public ItemData item;
public virtual void Pickup()
{
//Set the player's inventory to the item
InventoryManager.Instance.EquipHandSlot(item);
//Update the changes in the scene
InventoryManager.Instance.RenderHand();
//Destroy this instance so as to not have multiple copies
Destroy(gameObject);
}
}
RegrowableHarvestBehaviour.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class RegrowableHarvestBehaviour : InteractableObject
{
CropBehaviour parentCrop;
//Sets the parent crop
public void SetParent(CropBehaviour parentCrop)
{
this.parentCrop = parentCrop;
}
public override void Pickup()
{
//Set the player's inventory to the item
InventoryManager.Instance.EquipHandSlot(item);
//Update the changes in the scene
InventoryManager.Instance.RenderHand();
//Set the parent crop back to seedling to regrow it
parentCrop.Regrow();
}
}
With that, we have completed refactoring the code and you would see that most of our errors are fixed.
3. Working with ItemSlotData
a. Editing slot values in the inspector
Moving on, we are going to make the inventory slots editable in the inspector. This is to allow us to assign the quantity and ItemData that we want in a specific inventory slot.
For us to be able to edit the slot values in the inspector, there are 3 main steps:
- Make
ItemSlotData System.Serializable - Inventory Slot Validation
- Assigning the items in the player’s inventory
Step 1 is to serialize the all the fields in InventoryManager and ItemSlotData so that we have control over them:
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()
{
...
}
[Header("Tools")]
//Tool Slots
[SerializeField]
private ItemSlotData[] toolSlots = new ItemSlotData[8];
//Tool in the player's hand
[SerializeField]
private ItemSlotData equippedToolSlot = null;
[Header("Items")]
//Item Slots
[SerializeField]
private ItemSlotData[] itemSlots = new ItemSlotData[8];
//Item in the player's hand
[SerializeField]
private ItemSlotData equippedItemSlot = null;
{...}
}
ItemSlotData.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[System.Serializable]
public class ItemSlotData
{
...
}
Next is Inventory Slot Validation. What it does is to automatically assign the quantity value as 1 when we place a value in the ItemData; instead of us needing to manually change the quantity from 0 to 1, which is what we are currently doing.
This function, which we will name OnValidate(), will be called whenever a value changes in the Inspector or a script is loaded. This function can also be used to validate the inventory slots.
To validate one slot, add the following code to InventoryManager. Thereafter, use this function to validate our equipped slots for the tool and item.
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()
{
...
}
[Header("Tools")]
//Tool Slots
[SerializeField]
private ItemSlotData[] toolSlots = new ItemSlotData[8];
//Tool in the player's hand
[SerializeField]
private ItemSlotData equippedToolSlot = null;
[Header("Items")]
//Item Slots
[SerializeField]
private ItemSlotData[] itemSlots = new ItemSlotData[8];
//Item in the player's hand
[SerializeField]
private ItemSlotData equippedItemSlot = null;
//The transform for the player to hold items in the scene
public Transform handPoint;
//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(InventorySlot.InventoryType inventoryType)
{
...
}
//Render the player's equipped item in the scene
public void RenderHand()
{
...
}
//Inventory Slot Data
#region Gets and Checks
//Get the slot item (ItemData)
public ItemData GetEquippedSlotItem(InventorySlot.InventoryType inventoryType)
{
...
}
//Get function for the slots (ItemSlotData)
public ItemSlotData GetEquippedSlot(InventorySlot.InventoryType inventoryType)
{
...
}
//Get function for the inventory slots
public ItemSlotData[] GetInventorySlots(InventorySlot.InventoryType inventoryType)
{
...
}
//Check if a hand slot has an item
public bool SlotEquipped(InventorySlot.InventoryType inventoryType)
{
...
}
//Check if the item is a tool
public bool IsTool(ItemData item)
{
...
}
#endregion
//Equip the hand slot with an ItemData (Will overwrite the slot)
public void EquipHandSlot(ItemData item)
{
...
}
#region Inventory Slot Validation
private void OnValidate()
{
//Validate the hand slots
ValidateInventorySlot(equippedToolSlot);
ValidateInventorySlot(equippedItemSlot);
}
//When giving the itemData value in the inspector, automatically set the quantity to 1
void ValidateInventorySlot(ItemSlotData slot)
{
if(slot.itemData != null && slot.quantity == 0)
{
slot.quantity = 1;
}
}
#endregion
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
}
}
Now, onto validating the rest of the inventory slots. They are made up of 2 arrays: toolSlots and itemSlots. To validate them, create a separate function called ValidateInventorySlots(). It will take in the arrays and loop through them to validate each inventory slot. Just like before, pass the itemSlots and toolSlots arrays in the OnValidate() function.
InventoryManager.cs
#region Inventory Slot Validation
private void OnValidate()
{
//Validate the hand slots
ValidateInventorySlot(equippedToolSlot);
ValidateInventorySlot(equippedItemSlot);
//Validate the slots in the inventory
ValidateInventorySlots(itemSlots);
ValidateInventorySlots(toolSlots);
}
//When giving the itemData value in the inspector, automatically set the quantity to 1
void ValidateInventorySlot(ItemSlotData slot)
{
if(slot.itemData != null && slot.quantity == 0)
{
slot.quantity = 1;
}
}
//Validate arrays
void ValidateInventorySlots(ItemSlotData[] array)
{
foreach (ItemSlotData slot in array)
{
ValidateInventorySlot(slot);
}
}
#endregion
To test, give an item to the player and it should automatically change the quantity to 1 in the Inspector. Assign all the tools back to the player. For the item slot, give a cabbage.

4. Rendering the UI
a. Adding Quantity Text
Onto the UI, we want to display the stacks in the Inventory. Starting with the hand slot, create a new text panel and anchor it to the bottom left of the inventory slot. Shrink it until the text fits nicely and rename it to “QuantityText”.

Copy the text panel with its RectTransform and paste it into the other hand slot and the one of the inventory slot prefab as a child.
Thereafter, in the inventory slot prefab, go to override and select the “QuantityText” GameObject and click “Apply to Prefab Inventory Slot”.

You should see the text showing up at all of the inventory slot prefabs.

We also need to display the number of items in a stack. There are 2 situations for this
- Do not show the text if there is nothing to display or if there is only 1 item (default state)
- Show the quantity text when there are more than 1 item in the stack
Add the following code to InventorySlot to achieve 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;
int quantity;
public Image itemDisplayImage;
public Text quantityText;
public enum InventoryType
{
Item, Tool
}
//Determines which inventory section this slot is apart of.
public InventoryType inventoryType;
int slotIndex;
public void Display(ItemSlotData itemSlot)
{
//Set the variables accordingly
itemToDisplay = itemSlot.itemData;
quantity = itemSlot.quantity;
//By default, the quantity text should not show
quantityText.text = "";
//Check if there is an item to display
if(itemToDisplay != null)
{
//Switch the thumbnail over
itemDisplayImage.sprite = itemToDisplay.thumbnail;
//Display the stack quantity if there is more than 1 in the stack
if(quantity > 1)
{
quantityText.text = quantity.ToString();
}
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);
}
}
In the Inventory Slot Prefab, set the Quantity Text (GameObject) to the Quantity Text (field). Do the same for each of the hand slot.

Then, give the player 2 cabbage seeds and 4 tomato seeds in the tool slots. Also, put some cabbage seeds into the equipped tool slot. This is what our inventory will look like now.

5. Moving the Inventory Items
a. Equipping from inventory to hand
Currently, the InventoryToHand() function only exchanges the items we hold without checking if it can stack. To fix this, we are first going to make a boolean function to compare the inventory’s ItemData with the slot’s. Under ItemSlotData, add the following code:
ItemSlotData.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[System.Serializable]
public class ItemSlotData
{
public ItemData itemData;
public int quantity;
//Class Constructor
public ItemSlotData(ItemData itemData, int quantity)
{
this.itemData = itemData;
this.quantity = quantity;
ValidateQuantity();
}
//Automatically construct the class with the item data of quantity 1
public ItemSlotData(ItemData itemData)
{
this.itemData = itemData;
quantity = 1;
ValidateQuantity();
}
//Stacking System
//Shortcut function to add 1 to the stack
public void AddQuantity()
{
AddQuantity(1);
}
//Add a specified amount to the stack
public void AddQuantity(int amountToAdd)
{
quantity += amountToAdd;
}
public void Remove()
{
quantity--;
ValidateQuantity();
}
//Compares the item to see if it can be stacked
public bool Stackable(ItemSlotData slotToCompare)
{
return slotToCompare.itemData == itemData;
}
//Do checks to see if the values make sense
private void ValidateQuantity()
{
if (quantity <= 0 || itemData == null)
{
Empty();
}
}
//Empties out the item slot
public void Empty()
{
itemData = null;
quantity = 0;
}
}
The criteria for stacking in the above code is if the hand slot’s ItemData matches with the ItemData of the item we picked. If you want to add more attributes (rating, size etc.) to an item, you just need to add more checks into this scalable function to see if they are identical for stacking.
Now, in InventoryManager, we need to know what hand slot to shift the inventory slot to. As it can either be the item or tool hand slot, we can use the InventoryType to determine the slot that we are currently handling. By default, we have set it as the tool slot. Thus, in InventoryManager:
InventoryManager.cs
//Handles movement of item from Inventory to Hand public void InventoryToHand(int slotIndex, InventorySlot.InventoryType inventoryType) { //The slot to equip (Tool by default) ItemSlotData handToEquip = equippedToolSlot; if(inventoryType == InventorySlot.InventoryType.Item) { handToEquip = equippedItemSlot; } }
Similarly, we need to keep track of the inventory slot that we are moving to the hand. The logic is the same as the above: if the InventoryType is Item, take the slotIndex from the item slot, else take it from itemSlot. Thereafter, check if the items are stackable before rendering the changes using RenderInventory(). Add the following to the same function to accomplish that:
InventoryManager.cs
public void InventoryToHand(int slotIndex, InventorySlot.InventoryType inventoryType)
{
//The slot to equip (Tool by default)
ItemSlotData handToEquip = equippedToolSlot;
ItemSlotData slotToMove = toolSlots[slotIndex];
if(inventoryType == InventorySlot.InventoryType.Item)
{
//Change the slot to item
handToEquip = equippedItemSlot;
slotToMove = itemSlots[slotIndex];
}
//Check if stackable
if (handToEquip.Stackable(inventoryToAlter[slotIndex]))
{
} else
{
//Not stackable
}
//Update the changes to the UI
UIManager.Instance.RenderInventory();
}
Due to the ItemSlotData created when we refactor our code, we are no longer able to use our old code for when it is not stackable. The main reason is our old code passes a reference, not a variable. Now, if we need to cache the ItemSlotData, we need to create a constructor that clones the data instead. Create this constructor in ItemSlotData:
ItemSlotData.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[System.Serializable]
public class ItemSlotData
{
public ItemData itemData;
public int quantity;
//Class Constructor
public ItemSlotData(ItemData itemData, int quantity)
{
this.itemData = itemData;
this.quantity = quantity;
ValidateQuantity();
}
//Automatically construct the class with the item data of quantity 1
public ItemSlotData(ItemData itemData)
{
this.itemData = itemData;
quantity = 1;
ValidateQuantity();
}
//Clones the ItemSlotData
public ItemSlotData (ItemSlotData slotToClone)
{
itemData = slotToClone.itemData;
quantity = slotToClone.quantity;
}
//Stacking System
//Shortcut function to add 1 to the stack
public void AddQuantity()
{
AddQuantity(1);
}
//Add a specified amount to the stack
public void AddQuantity(int amountToAdd)
{
quantity += amountToAdd;
}
public void Remove()
{
quantity--;
ValidateQuantity();
}
//Compares the item to see if it can be stacked
public bool Stackable(ItemSlotData slotToCompare)
{
return slotToCompare.itemData == itemData;
}
//Do checks to see if the values make sense
private void ValidateQuantity()
{
if (quantity <= 0 || itemData == null)
{
Empty();
}
}
//Empties out the item slot
public void Empty()
{
itemData = null;
quantity = 0;
}
}
What the above constructor does is copy the other slot data and create a new instance to allow us to cache the inventory ItemSlotData in InventoryManager.
InventoryManager.cs
public void InventoryToHand(int slotIndex, InventorySlot.InventoryType inventoryType)
{
//The slot to equip (Tool by default)
ItemSlotData handToEquip = equippedToolSlot;
ItemSlotData slotToMove = toolSlots[slotIndex];
if(inventoryType == InventorySlot.InventoryType.Item)
{
//Change the slot to item
handToEquip = equippedItemSlot;
slotToMove = itemSlots[slotIndex];
}
//Check if stackable
if (handToEquip.Stackable(inventoryToAlter[slotIndex]))
{
} else
{
//Not stackable
//Cache the Inventory ItemSlotData
ItemSlotData slotToEquip = new ItemSlotData(slotToMove);
}
//Update the changes to the UI
UIManager.Instance.RenderInventory();
}
However, we cannot use the slotToMove variable to change the inventory slot’s ItemSlotData because it only stores a reference to the ItemSlotData class instance itself, rather than the value of the array element.
To fix that, we are going to do the following:
- Get the entire array of ItemSlotData instead of the slot itself.
- Rename that variable from slotToMove to inventoryToAlter [make the same changes to itemSlots]
For non-stackable items:
- Set the
slotIndexin theinventoryToAlterand use that to cache and swap the hand’s and inventory’s slot.
Do the following changes to InventoryToHand():
InventoryManager.cs
public void InventoryToHand(int slotIndex, InventorySlot.InventoryType inventoryType)
{
//The slot to equip (Tool by default)
ItemSlotData handToEquip = equippedToolSlot;
//The array to change
ItemSlotData[] inventoryToAlter = toolSlots;
if(inventoryType == InventorySlot.InventoryType.Item)
{
//Change the slot to item
handToEquip = equippedItemSlot;
inventoryToAlter = itemSlots;
}
//Check if stackable
if (handToEquip.Stackable(inventoryToAlter[slotIndex]))
{
} else
{
//Not stackable
//Cache the Inventory ItemSlotData
ItemSlotData slotToEquip = new ItemSlotData(inventoryToAlter[slotIndex]);
//Change the inventory slot to the hands
inventoryToAlter[slotIndex] = new ItemSlotData(handToEquip);
//Change the Hand's Slot to the Inventory Slot's
handToEquip = slotToEquip;
//Update the changes in the scene
RenderHand();
}
//Update the changes to the UI
UIManager.Instance.RenderInventory();
}
On the other hand, to stack an item, the steps are slightly different:
- Find the slot data of the slot we are altering.
- Take the quantity of the item in the inventory slot and add it to the quantity of the item in the hand slot
- Clear the inventory slot
InventoryManager.cs
public void InventoryToHand(int slotIndex, InventorySlot.InventoryType inventoryType)
{
//The slot to equip (Tool by default)
ItemSlotData handToEquip = equippedToolSlot;
//The array to change
ItemSlotData[] inventoryToAlter = toolSlots;
if(inventoryType == InventorySlot.InventoryType.Item)
{
//Change the slot to item
handToEquip = equippedItemSlot;
inventoryToAlter = itemSlots;
}
//Check if stackable
if (handToEquip.Stackable(inventoryToAlter[slotIndex]))
{
ItemSlotData slotToAlter = inventoryToAlter[slotIndex];
//Add to the hand slot
handToEquip.AddQuantity(slotToAlter.quantity);
//Empty the inventory slot
slotToAlter.Empty();
} else
{
//Not stackable
//Cache the Inventory ItemSlotData
ItemSlotData slotToEquip = new ItemSlotData(inventoryToAlter[slotIndex]);
//Change the inventory slot to the hands
inventoryToAlter[slotIndex] = new ItemSlotData(handToEquip);
//Change the Hand's Slot to the Inventory Slot's
handToEquip = slotToEquip;
//Update the changes in the scene
RenderHand();
}
//Update the changes to the UI
UIManager.Instance.RenderInventory();
}
Lastly, when the InventoryType is an item, call RenderHand(). Also, delete all our old code in this function as it is no longer needed.
InventoryManager.cs
public void InventoryToHand(int slotIndex, InventorySlot.InventoryType inventoryType)
{
//The slot to equip (Tool by default)
ItemSlotData handToEquip = equippedToolSlot;
//The array to change
ItemSlotData[] inventoryToAlter = toolSlots;
if(inventoryType == InventorySlot.InventoryType.Item)
{
//Change the slot to item
handToEquip = equippedItemSlot;
inventoryToAlter = itemSlots;
}
//Check if stackable
if (handToEquip.Stackable(inventoryToAlter[slotIndex]))
{
ItemSlotData slotToAlter = inventoryToAlter[slotIndex];
//Add to the hand slot
handToEquip.AddQuantity(slotToAlter.quantity);
//Empty the inventory slot
slotToAlter.Empty();
} else
{
//Not stackable
//Cache the Inventory ItemSlotData
ItemSlotData slotToEquip = new ItemSlotData(inventoryToAlter[slotIndex]);
//Change the inventory slot to the hands
inventoryToAlter[slotIndex] = new ItemSlotData(handToEquip);
//Change the Hand's Slot to the Inventory Slot's
handToEquip = slotToEquip;
//Update the changes in the scene
RenderHand();
}
//Update the changes in the scene
if (inventoryType == InventorySlot.InventoryType.Item)
{
RenderHand();
}
//Update the changes to the UI
UIManager.Instance.RenderInventory();
}
Now, when you test it, if cabbage seeds will stack when added together.

b. Unequipping from hand to inventory
The same thing applies for HandToInventory(). Copy the following code from InventoryToHand() to HandToInventory():
InventoryManager.cs
//Handles movement of item from Hand to Inventory public void HandToInventory(InventorySlot.InventoryType inventoryType) { //The slot to move from (Tool by default) ItemSlotData handSlot = equippedToolSlot; //The array to change ItemSlotData[] inventoryToAlter = toolSlots; if (inventoryType == InventorySlot.InventoryType.Item) { handSlot = equippedItemSlot; inventoryToAlter = itemSlots; } }
With these variables defined, we now tackle moving the item from the hand back to inventory. This is more complicated as we have to find if any of the items is stackable with the one in our hand. To this end, create a function that iterates through each of the items in the inventory to see if it can be stacked.
Add the new function StackItemToInventory() below HandToInventory():
InventoryManager.cs
public void HandToInventory(InventorySlot.InventoryType inventoryType)
{
...
}
//Iterate through each of the items in the inventory to see if it can be stacked
//Will perform the operation if found, returns false if unsuccessful
public bool StackItemToInventory(ItemSlotData itemSlot, ItemSlotData[] inventoryArray)
{
for (int i = 0; i < inventoryArray.Length; i++)
{
if (inventoryArray[i].Stackable(itemSlot))
{
//Add to the inventory slot's stack
inventoryArray[i].AddQuantity(itemSlot.quantity);
//Empty the item slot
itemSlot.Empty();
return true;
}
}
//Can't find any slot that can be stacked
return false;
}
To explain the above code: this function takes in 2 arguments, the item we are checking and the inventory array that we are iterating through. Using a for loop, we check for every item in the array. If a slot can be stacked, it will return true before emptying the slot so we can perform the stacking operation; else, it will return false.
Back in HandToInventory(), we will try stacking the hand slot. If it cannot stack , it will move to an empty inventory slot and at the end of the function, update the changes to scene and UI.
InventoryManager.cs
//Handles movement of item from Hand to Inventory public void HandToInventory(InventorySlot.InventoryType inventoryType) { //The slot to move from (Tool by default) ItemSlotData handSlot = equippedToolSlot; //The array to change ItemSlotData[] inventoryToAlter = toolSlots; if (inventoryType == InventorySlot.InventoryType.Item) { handSlot = equippedItemSlot; inventoryToAlter = itemSlots; } //Try stacking the hand slot. //Check if the operation failed if (!StackItemToInventory(handSlot, inventoryToAlter)) { //Find an empty slot to put the item in } //Update the changes in the scene if (inventoryType == InventorySlot.InventoryType.Item) { RenderHand(); } //Update the changes to the UI UIManager.Instance.RenderInventory(); }
To find the empty slot to put our non-stackable item, copy our old code (highlighted below) and change the variable names to the one we are currently using. Also, instead of setting it to null, call the Empty() function. This will clone the hand slot to an empty slot in the inventory.
InventoryManager.cs
public void HandToInventory(InventorySlot.InventoryType inventoryType)
{
//The slot to move from (Tool by default)
ItemSlotData handSlot = equippedToolSlot;
//The array to change
ItemSlotData[] inventoryToAlter = toolSlots;
if (inventoryType == InventorySlot.InventoryType.Item)
{
handSlot = equippedItemSlot;
inventoryToAlter = itemSlots;
}
//Try stacking the hand slot.
//Check if the operation failed
if (!StackItemToInventory(handSlot, inventoryToAlter))
{
//Find an empty slot to put the item in
//Iterate through each inventory slot and find an empty slot
for (int i = 0; i < inventoryToAlter.Length; i++)
{
if (inventoryToAlter[i] == null)
{
//Send the equipped item over to its new slot
inventoryToAlter[i] = new ItemSlotData(handSlot);
//Remove the item from the hand
handSlot.Empty();
break;
}
}
}
//Update the changes in the scene
if (inventoryType == InventorySlot.InventoryType.Item)
{
RenderHand();
}
//Update the changes to the UI
UIManager.Instance.RenderInventory();
}
As before, delete the rest of the old code in the HandToInventory() function.
Currently, this will still generate an error because RenderHand() is still checking if the equippedItemSlot is null. To fix this, replace the line with the SlotEquipped() function.
InventoryManager.cs
public void RenderHand()
{
//Reset objects on the hand
if(handPoint.childCount > 0)
{
Destroy(handPoint.GetChild(0).gameObject);
}
//Check if the player has anything equipped
if(equippedItemSlot != null SlotEquipped(InventorySlot.InventoryType.Item))
{
//Instantiate the game model on the player's hand and put it on the scene
Instantiate(GetEquippedSlotItem(InventorySlot.InventoryType.Item).gameModel, handPoint);
}
}
In our SlotEquipped() function, you might realise that we are making the same mistake because we directly check if the equippedItemSlot is null although it will never be null. To handle this, we need to create a simple function in ItemSlotData that checks if it is considered empty. All it does is to check if the itemData is null. Thus, in ItemSlotData:
ItemSlotData.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[System.Serializable]
public class ItemSlotData
{
public ItemData itemData;
public int quantity;
//Class Constructor
public ItemSlotData(ItemData itemData, int quantity)
{
this.itemData = itemData;
this.quantity = quantity;
ValidateQuantity();
}
//Automatically construct the class with the item data of quantity 1
public ItemSlotData(ItemData itemData)
{
this.itemData = itemData;
quantity = 1;
ValidateQuantity();
}
//Clones the ItemSlotData
public ItemSlotData (ItemSlotData slotToClone)
{
itemData = slotToClone.itemData;
quantity = slotToClone.quantity;
}
//Stacking System
//Shortcut function to add 1 to the stack
public void AddQuantity()
{
AddQuantity(1);
}
//Add a specified amount to the stack
public void AddQuantity(int amountToAdd)
{
quantity += amountToAdd;
}
public void Remove()
{
quantity--;
ValidateQuantity();
}
//Compares the item to see if it can be stacked
public bool Stackable(ItemSlotData slotToCompare)
{
return slotToCompare.itemData == itemData;
}
//Do checks to see if the values make sense
private void ValidateQuantity()
{
if (quantity <= 0 || itemData == null)
{
Empty();
}
}
//Empties out the item slot
public void Empty()
{
itemData = null;
quantity = 0;
}
//Check if the slot is considered 'empty'
public bool IsEmpty()
{
return itemData == null;
}
}
Back in InventoryManager, switch out the null with the isEmpty() function for both inventory types. There is an exclamation mark because it returns true when it is not empty.
InventoryManager.cs
//Check if a hand slot has an item public bool SlotEquipped(InventorySlot.InventoryType inventoryType) { if (inventoryType == InventorySlot.InventoryType.Item) { returnequippedItemSlot != null!equippedItemSlot.IsEmpty(); } returnequippedToolSlot != null!equippedToolSlot.IsEmpty(); }
Now, in InventoryToHand(), we can no longer directly swap the slots because we are passing a reference onto a variable. Instead, use the EquipHandSlot() function we made earlier.
InventoryManager.cs
public void InventoryToHand(int slotIndex, InventorySlot.InventoryType inventoryType)
{
//The slot to equip (Tool by default)
ItemSlotData handToEquip = equippedToolSlot;
//The array to change
ItemSlotData[] inventoryToAlter = toolSlots;
if(inventoryType == InventorySlot.InventoryType.Item)
{
//Change the slot to item
handToEquip = equippedItemSlot;
inventoryToAlter = itemSlots;
}
//Check if stackable
if (handToEquip.Stackable(inventoryToAlter[slotIndex]))
{
ItemSlotData slotToAlter = inventoryToAlter[slotIndex];
//Add to the hand slot
handToEquip.AddQuantity(slotToAlter.quantity);
//Empty the inventory slot
slotToAlter.Empty();
} else
{
//Not stackable
//Cache the Inventory ItemSlotData
ItemSlotData slotToEquip = new ItemSlotData(inventoryToAlter[slotIndex]);
//Change the inventory slot to the hands
inventoryToAlter[slotIndex] = new ItemSlotData(handToEquip);
//Change the Hand's Slot to the Inventory Slot's
handToEquip = slotToEquip;
EquipHandSlot(slotToEquip);
}
//Update the changes in the scene
if (inventoryType == InventorySlot.InventoryType.Item)
{
RenderHand();
}
//Update the changes to the UI
UIManager.Instance.RenderInventory();
}
There is an error as the function only accepts ItemData variables but we want to pass over an ItemSlotData. To fix this, overload the EquipHandSlot() function with one that accepts ItemSlotData and clone it.
InventoryManager.cs
//Equip the hand slot with an ItemData (Will overwrite the slot) public void EquipHandSlot(ItemData item) { if (IsTool(item)) { equippedToolSlot = new ItemSlotData(item); } else { equippedItemSlot = new ItemSlotData(item); } } //Equip the hand slot with an ItemSlotData (Will overwrite the slot) public void EquipHandSlot(ItemSlotData itemSlot) { //Get the item data from the slot ItemData item = itemSlot.itemData; if (IsTool(item)) { equippedToolSlot = new ItemSlotData(itemSlot); } else { equippedItemSlot = new ItemSlotData(itemSlot); } }
Also, change the null to itsEmpty() function.
InventoryManager.cs
//Handles movement of item from Hand to Inventory public void HandToInventory(InventorySlot.InventoryType inventoryType) { //The slot to move from (Tool by default) ItemSlotData handSlot = equippedToolSlot; //The array to change ItemSlotData[] inventoryToAlter = toolSlots; if (inventoryType == InventorySlot.InventoryType.Item) { handSlot = equippedItemSlot; inventoryToAlter = itemSlots; } //Try stacking the hand slot. //Check if the operation failed if (!StackItemToInventory(handSlot, inventoryToAlter)) { //Find an empty slot to put the item in //Iterate through each inventory slot and find an empty slot for (int i = 0; i < inventoryToAlter.Length; i++) { if (inventoryToAlter[i].IsEmpty()== null) { //Send the equipped item over to its new slot inventoryToAlter[i] = new ItemSlotData(handSlot); //Remove the item from the hand handSlot.Empty(); break; } } } //Update the changes in the scene if (inventoryType == InventorySlot.InventoryType.Item) { RenderHand(); } //Update the changes to the UI UIManager.Instance.RenderInventory(); }
Now, when you test, everything will be working.

6. Consumables
a. Depleting the quantity of consumables when used
We need to update the Interact() function with the above code to detect the item we are holding. In Land:
Land.cs
public void Interact()
{
//Check the player's tool slot
ItemData toolSlot = InventoryManager.Instance.GetEquippedSlotItem(InventorySlot.InventoryType.Tool);
//If there's nothing equipped, return
if (!InventoryManager.Instance.SlotEquipped(InventorySlot.InventoryType.Tool))
{
return;
}
//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;
case EquipmentData.ToolType.Shovel:
//Remove the crop from the land
if(cropPlanted != null)
{
Destroy(cropPlanted.gameObject);
}
break;
}
//We don't need to check for seeds if we have already confirmed the tool to be an equipment
return;
}
//Try casting the itemdata in the toolslot as SeedData
SeedData seedTool = toolSlot as SeedData;
///Conditions for the player to be able to plant a seed
///1: He is holding a tool of type SeedData
///2: The Land State must be either watered or farmland
///3. There isn't already a crop that has been planted
if(seedTool != null && landStatus != LandStatus.Soil && cropPlanted == null)
{
//Instantiate the crop object parented to the land
GameObject cropObject = Instantiate(cropPrefab, transform);
//Move the crop object to the top of the land gameobject
cropObject.transform.position = new Vector3(transform.position.x, 0, transform.position.z);
//Access the CropBehaviour of the crop we're going to plant
cropPlanted = cropObject.GetComponent<CropBehaviour>();
//Plant it with the seed's information
cropPlanted.Plant(seedTool);
//Consume the item
InventoryManager.Instance.ConsumeItem(InventoryManager.Instance.GetEquippedSlot(InventorySlot.InventoryType.Tool));
}
}
Currently, none of our seeds are consumed although we have planted them. To handle this, create a new function called ConsumeItem() that checks and removes the item before refreshing the inventory. 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()
{
...
}
[Header("Tools")]
//Tool Slots
[SerializeField]
private ItemSlotData[] toolSlots = new ItemSlotData[8];
//Tool in the player's hand
[SerializeField]
private ItemSlotData equippedToolSlot = null;
[Header("Items")]
//Item Slots
[SerializeField]
private ItemSlotData[] itemSlots = new ItemSlotData[8];
//Item in the player's hand
[SerializeField]
private ItemSlotData equippedItemSlot = null;
//The transform for the player to hold items in the scene
public Transform handPoint;
//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(InventorySlot.InventoryType inventoryType)
{
...
}
//Iterate through each of the items in the inventory to see if it can be stacked
//Will perform the operation if found, returns false if unsuccessful
public bool StackItemToInventory(ItemSlotData itemSlot, ItemSlotData[] inventoryArray)
{
...
}
//Render the player's equipped item in the scene
public void RenderHand()
{
...
}
//Inventory Slot Data
#region Gets and Checks
...
#endregion
//Equip the hand slot with an ItemData (Will overwrite the slot)
public void EquipHandSlot(ItemData item)
{
...
}
//Equip the hand slot with an ItemSlotData (Will overwrite the slot)
public void EquipHandSlot(ItemSlotData itemSlot)
{
...
}
public void ConsumeItem(ItemSlotData itemSlot)
{
if (itemSlot.IsEmpty())
{
Debug.LogError("There is nothing to consume!");
return;
}
//Use up one of the item slots
itemSlot.Remove();
//Refresh inventory
RenderHand();
UIManager.Instance.RenderInventory();
}
#region Inventory Slot Validation
...
#endregion
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
}
}
b. Updating the changes in the UI
Copy and paste the QuantityText GameObject from HandSlot GameObject under Inventory to the HandSlot GameObject under Status Bar.

Thereafter, under UIManager, create a new Text called toolQuantityText.
This text will update under 2 situations:
- When quantity is greater than 1, it will display the quantity
- When quantity is lesser or equal to 1, the text will be empty
To do that, add the following code to UIManager:
UIManager.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class UIManager : MonoBehaviour, ITimeTracker
{
public static UIManager Instance { get; private set; }
[Header("Status Bar")]
//Tool equip slot on the status bar
public Image toolEquipSlot;
//Tool Quantity text on the status bar
public Text toolQuantityText;
//Time UI
public Text timeText;
public Text dateText;
[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()
{
...
}
private void Start()
{
...
}
//Iterate through the slot UI elements and assign it its reference slot index
public void AssignSlotIndexes()
{
...
}
//Render the inventory screen to reflect the Player's Inventory.
public void RenderInventory()
{
//Get the respective slots to process
ItemSlotData[] inventoryToolSlots = InventoryManager.Instance.GetInventorySlots(InventorySlot.InventoryType.Tool);
ItemSlotData[] inventoryItemSlots = InventoryManager.Instance.GetInventorySlots(InventorySlot.InventoryType.Item);
//Render the Tool section
RenderInventoryPanel(inventoryToolSlots, toolSlots);
//Render the Item section
RenderInventoryPanel(inventoryItemSlots, itemSlots);
//Render the equipped slots
toolHandSlot.Display(InventoryManager.Instance.GetEquippedSlot(InventorySlot.InventoryType.Tool));
itemHandSlot.Display(InventoryManager.Instance.GetEquippedSlot(InventorySlot.InventoryType.Item));
//Get Tool Equip from InventoryManager
ItemData equippedTool = InventoryManager.Instance.GetEquippedSlotItem(InventorySlot.InventoryType.Tool);
//Text should be empty by default
toolQuantityText.text = "";
//Check if there is an item to display
if (equippedTool != null)
{
//Switch the thumbnail over
toolEquipSlot.sprite = equippedTool.thumbnail;
toolEquipSlot.gameObject.SetActive(true);
//Get quantity
int quantity = InventoryManager.Instance.GetEquippedSlot(InventorySlot.InventoryType.Tool).quantity;
if (quantity > 1)
{
toolQuantityText.text = quantity.ToString();
}
return;
}
toolEquipSlot.gameObject.SetActive(false);
}
//Iterate through a slot in a section and display them in the UI
void RenderInventoryPanel(ItemSlotData[] slots, InventorySlot[] uiSlots)
{
...
}
public void ToggleInventoryPanel()
{
...
}
//Display Item info on the Item infobox
public void DisplayItemInfo(ItemData data)
{
...
}
//Callback to handle the UI for time
public void ClockUpdate(GameTimestamp timestamp)
{
...
}
}
The ToString() function to convert variables of other types (such as integers in this case) to a string.
When testing the game now, you should be able to see we are able to plant and consume all of our items.

7. Conclusion
We have successfully improved our inventory system and made our code more encapsulated, so it can be managed more easily in the future.
For now, if you are a Patreon supporter, you can download the project files for what we’ve done so far. To use the files, you will have to unzip the file (7-Zip can help you do that), and open the folder with Assets and ProjectSettings as a project using Unity.
Here is the final code for all the scripts we have worked with today:
ItemSlotData.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[System.Serializable]
public class ItemSlotData
{
public ItemData itemData;
public int quantity;
//Class Constructor
public ItemSlotData(ItemData itemData, int quantity)
{
this.itemData = itemData;
this.quantity = quantity;
ValidateQuantity();
}
//Automatically construct the class with the item data of quantity 1
public ItemSlotData(ItemData itemData)
{
this.itemData = itemData;
quantity = 1;
ValidateQuantity();
}
//Clones the ItemSlotData
public ItemSlotData (ItemSlotData slotToClone)
{
itemData = slotToClone.itemData;
quantity = slotToClone.quantity;
}
//Stacking System
//Shortcut function to add 1 to the stack
public void AddQuantity()
{
AddQuantity(1);
}
//Add a specified amount to the stack
public void AddQuantity(int amountToAdd)
{
quantity += amountToAdd;
}
public void Remove()
{
quantity--;
ValidateQuantity();
}
//Compares the item to see if it can be stacked
public bool Stackable(ItemSlotData slotToCompare)
{
return slotToCompare.itemData == itemData;
}
//Do checks to see if the values make sense
private void ValidateQuantity()
{
if (quantity <= 0 || itemData == null)
{
Empty();
}
}
//Empties out the item slot
public void Empty()
{
itemData = null;
quantity = 0;
}
//Check if the slot is considered 'empty'
public bool IsEmpty()
{
return itemData == 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
[SerializeField]
private ItemSlotData[] toolSlots = new ItemSlotData[8];
//Tool in the player's hand
[SerializeField]
private ItemSlotData equippedToolSlot = null;
[Header("Items")]
//Item Slots
[SerializeField]
private ItemSlotData[] itemSlots = new ItemSlotData[8];
//Item in the player's hand
[SerializeField]
private ItemSlotData equippedItemSlot = null;
//The transform for the player to hold items in the scene
public Transform handPoint;
//Equipping
//Handles movement of item from Inventory to Hand
public void InventoryToHand(int slotIndex, InventorySlot.InventoryType inventoryType)
{
//The slot to equip (Tool by default)
ItemSlotData handToEquip = equippedToolSlot;
//The array to change
ItemSlotData[] inventoryToAlter = toolSlots;
if(inventoryType == InventorySlot.InventoryType.Item)
{
//Change the slot to item
handToEquip = equippedItemSlot;
inventoryToAlter = itemSlots;
}
//Check if stackable
if (handToEquip.Stackable(inventoryToAlter[slotIndex]))
{
ItemSlotData slotToAlter = inventoryToAlter[slotIndex];
//Add to the hand slot
handToEquip.AddQuantity(slotToAlter.quantity);
//Empty the inventory slot
slotToAlter.Empty();
} else
{
//Not stackable
//Cache the Inventory ItemSlotData
ItemSlotData slotToEquip = new ItemSlotData(inventoryToAlter[slotIndex]);
//Change the inventory slot to the hands
inventoryToAlter[slotIndex] = new ItemSlotData(handToEquip);
EquipHandSlot(slotToEquip);
}
//Update the changes in the scene
if (inventoryType == InventorySlot.InventoryType.Item)
{
RenderHand();
}
//Update the changes to the UI
UIManager.Instance.RenderInventory();
}
//Handles movement of item from Hand to Inventory
public void HandToInventory(InventorySlot.InventoryType inventoryType)
{
//The slot to move from (Tool by default)
ItemSlotData handSlot = equippedToolSlot;
//The array to change
ItemSlotData[] inventoryToAlter = toolSlots;
if (inventoryType == InventorySlot.InventoryType.Item)
{
handSlot = equippedItemSlot;
inventoryToAlter = itemSlots;
}
//Try stacking the hand slot.
//Check if the operation failed
if (!StackItemToInventory(handSlot, inventoryToAlter))
{
//Find an empty slot to put the item in
//Iterate through each inventory slot and find an empty slot
for (int i = 0; i < inventoryToAlter.Length; i++)
{
if (inventoryToAlter[i].IsEmpty())
{
//Send the equipped item over to its new slot
inventoryToAlter[i] = new ItemSlotData(handSlot);
//Remove the item from the hand
handSlot.Empty();
break;
}
}
}
//Update the changes in the scene
if (inventoryType == InventorySlot.InventoryType.Item)
{
RenderHand();
}
//Update the changes to the UI
UIManager.Instance.RenderInventory();
}
//Iterate through each of the items in the inventory to see if it can be stacked
//Will perform the operation if found, returns false if unsuccessful
public bool StackItemToInventory(ItemSlotData itemSlot, ItemSlotData[] inventoryArray)
{
for (int i = 0; i < inventoryArray.Length; i++)
{
if (inventoryArray[i].Stackable(itemSlot))
{
//Add to the inventory slot's stack
inventoryArray[i].AddQuantity(itemSlot.quantity);
//Empty the item slot
itemSlot.Empty();
return true;
}
}
//Can't find any slot that can be stacked
return false;
}
//Render the player's equipped item in the scene
public void RenderHand()
{
//Reset objects on the hand
if(handPoint.childCount > 0)
{
Destroy(handPoint.GetChild(0).gameObject);
}
//Check if the player has anything equipped
if(SlotEquipped(InventorySlot.InventoryType.Item))
{
//Instantiate the game model on the player's hand and put it on the scene
Instantiate(GetEquippedSlotItem(InventorySlot.InventoryType.Item).gameModel, handPoint);
}
}
//Inventory Slot Data
#region Gets and Checks
//Get the slot item (ItemData)
public ItemData GetEquippedSlotItem(InventorySlot.InventoryType inventoryType)
{
if(inventoryType == InventorySlot.InventoryType.Item)
{
return equippedItemSlot.itemData;
}
return equippedToolSlot.itemData;
}
//Get function for the slots (ItemSlotData)
public ItemSlotData GetEquippedSlot(InventorySlot.InventoryType inventoryType)
{
if (inventoryType == InventorySlot.InventoryType.Item)
{
return equippedItemSlot;
}
return equippedToolSlot;
}
//Get function for the inventory slots
public ItemSlotData[] GetInventorySlots(InventorySlot.InventoryType inventoryType)
{
if (inventoryType == InventorySlot.InventoryType.Item)
{
return itemSlots;
}
return toolSlots;
}
//Check if a hand slot has an item
public bool SlotEquipped(InventorySlot.InventoryType inventoryType)
{
if (inventoryType == InventorySlot.InventoryType.Item)
{
return !equippedItemSlot.IsEmpty();
}
return !equippedToolSlot.IsEmpty();
}
//Check if the item is a tool
public bool IsTool(ItemData item)
{
//Is it equipment?
//Try to cast it as equipment first
EquipmentData equipment = item as EquipmentData;
if(equipment != null)
{
return true;
}
//Is it a seed?
//Try to cast it as a seed
SeedData seed = item as SeedData;
//If the seed is not null it is a seed
return seed != null;
}
#endregion
//Equip the hand slot with an ItemData (Will overwrite the slot)
public void EquipHandSlot(ItemData item)
{
if (IsTool(item))
{
equippedToolSlot = new ItemSlotData(item);
} else
{
equippedItemSlot = new ItemSlotData(item);
}
}
//Equip the hand slot with an ItemSlotData (Will overwrite the slot)
public void EquipHandSlot(ItemSlotData itemSlot)
{
//Get the item data from the slot
ItemData item = itemSlot.itemData;
if (IsTool(item))
{
equippedToolSlot = new ItemSlotData(itemSlot);
}
else
{
equippedItemSlot = new ItemSlotData(itemSlot);
}
}
public void ConsumeItem(ItemSlotData itemSlot)
{
if (itemSlot.IsEmpty())
{
Debug.LogError("There is nothing to consume!");
return;
}
//Use up one of the item slots
itemSlot.Remove();
//Refresh inventory
RenderHand();
UIManager.Instance.RenderInventory();
}
#region Inventory Slot Validation
private void OnValidate()
{
//Validate the hand slots
ValidateInventorySlot(equippedToolSlot);
ValidateInventorySlot(equippedItemSlot);
//Validate the slots in the inventoryy
ValidateInventorySlots(itemSlots);
ValidateInventorySlots(toolSlots);
}
//When giving the itemData value in the inspector, automatically set the quantity to 1
void ValidateInventorySlot(ItemSlotData slot)
{
if(slot.itemData != null && slot.quantity == 0)
{
slot.quantity = 1;
}
}
//Validate arrays
void ValidateInventorySlots(ItemSlotData[] array)
{
foreach (ItemSlotData slot in array)
{
ValidateInventorySlot(slot);
}
}
#endregion
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
}
}
UIManager.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class UIManager : MonoBehaviour, ITimeTracker
{
public static UIManager Instance { get; private set; }
[Header("Status Bar")]
//Tool equip slot on the status bar
public Image toolEquipSlot;
//Tool Quantity text on the status bar
public Text toolQuantityText;
//Time UI
public Text timeText;
public Text dateText;
[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();
//Add UIManager to the list of objects TimeManager will notify when the time updates
TimeManager.Instance.RegisterTracker(this);
}
//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 respective slots to process
ItemSlotData[] inventoryToolSlots = InventoryManager.Instance.GetInventorySlots(InventorySlot.InventoryType.Tool);
ItemSlotData[] inventoryItemSlots = InventoryManager.Instance.GetInventorySlots(InventorySlot.InventoryType.Item);
//Render the Tool section
RenderInventoryPanel(inventoryToolSlots, toolSlots);
//Render the Item section
RenderInventoryPanel(inventoryItemSlots, itemSlots);
//Render the equipped slots
toolHandSlot.Display(InventoryManager.Instance.GetEquippedSlot(InventorySlot.InventoryType.Tool));
itemHandSlot.Display(InventoryManager.Instance.GetEquippedSlot(InventorySlot.InventoryType.Item));
//Get Tool Equip from InventoryManager
ItemData equippedTool = InventoryManager.Instance.GetEquippedSlotItem(InventorySlot.InventoryType.Tool);
//Text should be empty by default
toolQuantityText.text = "";
//Check if there is an item to display
if (equippedTool != null)
{
//Switch the thumbnail over
toolEquipSlot.sprite = equippedTool.thumbnail;
toolEquipSlot.gameObject.SetActive(true);
//Get quantity
int quantity = InventoryManager.Instance.GetEquippedSlot(InventorySlot.InventoryType.Tool).quantity;
if (quantity > 1)
{
toolQuantityText.text = quantity.ToString();
}
return;
}
toolEquipSlot.gameObject.SetActive(false);
}
//Iterate through a slot in a section and display them in the UI
void RenderInventoryPanel(ItemSlotData[] 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;
}
//Callback to handle the UI for time
public void ClockUpdate(GameTimestamp timestamp)
{
//Handle the time
//Get the hours and minutes
int hours = timestamp.hour;
int minutes = timestamp.minute;
//AM or PM
string prefix = "AM ";
//Convert hours to 12 hour clock
if (hours > 12)
{
//Time becomes PM
prefix = "PM ";
hours = hours - 12;
Debug.Log(hours);
}
//Format it for the time text display
timeText.text = prefix + hours + ":" + minutes.ToString("00");
//Handle the Date
int day = timestamp.day;
string season = timestamp.season.ToString();
string dayOfTheWeek = timestamp.GetDayOfTheWeek().ToString();
//Format it for the date text display
dateText.text = season + " " + day + " (" + dayOfTheWeek +")";
}
}
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;
int quantity;
public Image itemDisplayImage;
public Text quantityText;
public enum InventoryType
{
Item, Tool
}
//Determines which inventory section this slot is apart of.
public InventoryType inventoryType;
int slotIndex;
public void Display(ItemSlotData itemSlot)
{
//Set the variables accordingly
itemToDisplay = itemSlot.itemData;
quantity = itemSlot.quantity;
//By default, the quantity text should not show
quantityText.text = "";
//Check if there is an item to display
if(itemToDisplay != null)
{
//Switch the thumbnail over
itemDisplayImage.sprite = itemToDisplay.thumbnail;
//Display the stack quantity if there is more than 1 in the stack
if(quantity > 1)
{
quantityText.text = quantity.ToString();
}
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);
}
}
PlayerInteraction.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerInteraction : MonoBehaviour
{
PlayerController playerController;
//The land the player is currently selecting
Land selectedLand = null;
//The interactable object the player is currently selecting
InteractableObject selectedInteractable = null;
// Start is called before the first frame update
void Start()
{
//Get access to our PlayerController component
playerController = transform.parent.GetComponent<PlayerController>();
}
// Update is called once per frame
void Update()
{
RaycastHit hit;
if(Physics.Raycast(transform.position, Vector3.down,out hit, 1))
{
OnInteractableHit(hit);
}
}
//Handles what happens when the interaction raycast hits something interactable
void OnInteractableHit(RaycastHit hit)
{
Collider other = hit.collider;
//Check if the player is going to interact with land
if(other.tag == "Land")
{
//Get the land component
Land land = other.GetComponent<Land>();
SelectLand(land);
return;
}
//Check if the player is going to interact with an Item
if(other.tag == "Item")
{
//Set the interactable to the currently selected interactable
selectedInteractable = other.GetComponent<InteractableObject>();
return;
}
//Deselect the interactable if the player is not standing on anything at the moment
if(selectedInteractable != null)
{
selectedInteractable = null;
}
//Deselect the land if the player is not standing on any land at the moment
if(selectedLand != null)
{
selectedLand.Select(false);
selectedLand = null;
}
}
//Handles the selection process of the land
void SelectLand(Land land)
{
//Set the previously selected land to false (If any)
if (selectedLand != null)
{
selectedLand.Select(false);
}
//Set the new selected land to the land we're selecting now.
selectedLand = land;
land.Select(true);
}
//Triggered when the player presses the tool button
public void Interact()
{
//The player shouldn't be able to use his tool when he has his hands full with an item
if(InventoryManager.Instance.SlotEquipped(InventorySlot.InventoryType.Item))
{
return;
}
//Check if the player is selecting any land
if(selectedLand != null)
{
selectedLand.Interact();
return;
}
Debug.Log("Not on any land!");
}
//Triggered when the player presses the item interact button
public void ItemInteract()
{
//If the player is holding something, keep it in his inventory
if(InventoryManager.Instance.SlotEquipped(InventorySlot.InventoryType.Item))
{
InventoryManager.Instance.HandToInventory(InventorySlot.InventoryType.Item);
return;
}
//If the player isn't holding anything, pick up an item
//Check if there is an interactable selected
if (selectedInteractable != null)
{
//Pick it up
selectedInteractable.Pickup();
}
}
}
Land.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Land : MonoBehaviour, ITimeTracker
{
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;
//Cache the time the land was watered
GameTimestamp timeWatered;
[Header("Crops")]
//The crop prefab to instantiate
public GameObject cropPrefab;
//The crop currently planted on the land
CropBehaviour cropPlanted = null;
// 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);
//Add this to TimeManager's Listener list
TimeManager.Instance.RegisterTracker(this);
}
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;
//Cache the time it was watered
timeWatered = TimeManager.Instance.GetGameTimestamp();
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.GetEquippedSlotItem(InventorySlot.InventoryType.Tool);
//If there's nothing equipped, return
if (!InventoryManager.Instance.SlotEquipped(InventorySlot.InventoryType.Tool))
{
return;
}
//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;
case EquipmentData.ToolType.Shovel:
//Remove the crop from the land
if(cropPlanted != null)
{
Destroy(cropPlanted.gameObject);
}
break;
}
//We don't need to check for seeds if we have already confirmed the tool to be an equipment
return;
}
//Try casting the itemdata in the toolslot as SeedData
SeedData seedTool = toolSlot as SeedData;
///Conditions for the player to be able to plant a seed
///1: He is holding a tool of type SeedData
///2: The Land State must be either watered or farmland
///3. There isn't already a crop that has been planted
if(seedTool != null && landStatus != LandStatus.Soil && cropPlanted == null)
{
//Instantiate the crop object parented to the land
GameObject cropObject = Instantiate(cropPrefab, transform);
//Move the crop object to the top of the land gameobject
cropObject.transform.position = new Vector3(transform.position.x, 0, transform.position.z);
//Access the CropBehaviour of the crop we're going to plant
cropPlanted = cropObject.GetComponent<CropBehaviour>();
//Plant it with the seed's information
cropPlanted.Plant(seedTool);
//Consume the item
InventoryManager.Instance.ConsumeItem(InventoryManager.Instance.GetEquippedSlot(InventorySlot.InventoryType.Tool));
}
}
public void ClockUpdate(GameTimestamp timestamp)
{
//Checked if 24 hours has passed since last watered
if(landStatus == LandStatus.Watered)
{
//Hours since the land was watered
int hoursElapsed = GameTimestamp.CompareTimestamps(timeWatered, timestamp);
Debug.Log(hoursElapsed + " hours since this was watered");
//Grow the planted crop, if any
if(cropPlanted != null)
{
cropPlanted.Grow();
}
if(hoursElapsed > 24)
{
//Dry up (Switch back to farmland)
SwitchLandStatus(LandStatus.Farmland);
}
}
//Handle the wilting of the plant when the land is not watered
if(landStatus != LandStatus.Watered && cropPlanted != null)
{
//If the crop has already germinated, start the withering
if (cropPlanted.cropState != CropBehaviour.CropState.Seed)
{
cropPlanted.Wither();
}
}
}
}
InteractableObject.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class InteractableObject : MonoBehaviour
{
//The item information the GameObject is supposed to represent
public ItemData item;
public virtual void Pickup()
{
//Set the player's inventory to the item
InventoryManager.Instance.EquipHandSlot(item);
//Update the changes in the scene
InventoryManager.Instance.RenderHand();
//Destroy this instance so as to not have multiple copies
Destroy(gameObject);
}
}
RegrowableHarvestBehaviour.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class RegrowableHarvestBehaviour : InteractableObject
{
CropBehaviour parentCrop;
//Sets the parent crop
public void SetParent(CropBehaviour parentCrop)
{
this.parentCrop = parentCrop;
}
public override void Pickup()
{
//Set the player's inventory to the item
InventoryManager.Instance.EquipHandSlot(item);
//Update the changes in the scene
InventoryManager.Instance.RenderHand();
//Set the parent crop back to seedling to regrow it
parentCrop.Regrow();
}
}

i’m having an issue whith the UI bur on Unity Console nothing appears.
When i select an Item it does not show in Item Hand Slot, but when i click in the Item Hand Slot, the Item comes back to the Inventory to the right section (Items).
But when i select a Tool it also shows at the Item Hand Slot and when i click in the Tool Hand Slot the Tool comes back to the Inventory to Tools section.
I made some prints:
https://im.ge/i/5NIHdq
https://im.ge/i/5NIvfP
https://im.ge/i/5NLlPL
https://im.ge/i/5NL18J
https://im.ge/i/5NLUFX
And yes, on Unity Inspector i selected the right InventoryType, i even put differents names:
(HandInventorySlot script)
https://im.ge/i/5NLZFG
https://im.ge/i/5NLzUy
(InventorySlot script)
https://im.ge/i/5NL4zK
https://im.ge/i/5NLGO9
what should i do?