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() { } }
Article continues after the advertisement:
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 +")"; } }
Article continues after the advertisement:
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(); } } } }
Article continues after the advertisement:
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 theInventoryType
theItemData
is 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.
Article continues after the advertisement:
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
slotIndex
in theinventoryToAlter
and 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.
Article continues after the advertisement:
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 != nullSlotEquipped(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() { } }
Article continues after the advertisement:
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(); } }
Article continues after the advertisement:
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?