Making Inventory and Item Crafting System in Unity
In this tutorial, I will be showing how to make a Minecraft-style inventory and item crafting system in Unity.
Item crafting in video games is a process of combining specific (usually simpler) items into more complex items, with new and enhanced properties. For example, combining wood and stone into a pickaxe, or combining metal sheet and wood into a sword.
The crafting system below is mobile-friendly and fully automated, meaning it will work with any UI layout and with the ability to create custom crafting recipes.
Step 1: Set Up Crafting UI
We begin by setting up the crafting UI:
- Create a new Canvas (Unity Top Taskbar: GameObject -> UI -> Canvas)
- Create a new Image by Right-Clicking on Canvas Object -> UI -> Image
- Rename the Image Object to "CraftingPanel" and change its Source Image to default "UISprite"
- Change "CraftingPanel" RectTransform values to (Pos X: 0 Pos Y: 0 Width: 410 Height: 365)
- Create two Objects inside "CraftingPanel" (Right-click on CraftingPanel -> Create Empty, 2 times)
- Rename the first Object to "CraftingSlots" and change its RectTransform values to ("Top Left align" Pivot X: 0 Pivot Y: 1 Pos X: 50 Pos Y: -35 Width: 140 Height: 140). This Object will contain crafting slots.
- Rename the second Object to "PlayerSlots" and change its RectTransform values to ("Top Stretch Horizontally" Pivot X: 0.5 Pivot Y: 1 Left: 0 Pos Y: -222 Right: 0 Height: 100). This Object will contain player slots.
Section Title:
- Create new Text by Right-Clicking on "PlayerSlots" Object -> UI -> Text and rename it to "SectionTitle"
- Change "SectionTitle" RectTransform values to ("Top Left align" Pivot X: 0 Pivot Y: 0 Pos X: 5 Pos Y: 0 Width: 160 Height: 30)
- Change the "SectionTitle" text to "Inventory" and set its Font Size to 18, Alignment to Left Middle, and Color to (0.2, 0.2, 0.2, 1)
- Duplicate the "SectionTitle" Object, change its Text to "Crafting" and move it under the "CraftingSlots" Object, then set the same RectTransform values as the previous "SectionTitle".
Crafting Slot:
The crafting slot will consist of a background image, an item image, and a count text:
- Create a new Image by Right-Clicking on Canvas Object -> UI -> Image
- Rename the new Image to "slot_template", set its RectTransform values to (Post X: 0 Pos Y: 0 Width: 40 Height: 40), and change its color to (0.32, 0.32, 0.32, 0.8)
- Duplicate "slot_template" and rename it to "Item", move it inside "slot_template" Object, change its RectTransform dimensions to (Width: 30 Height: 30) and Color to (1, 1, 1, 1)
- Create new Text by Right-Clicking on "slot_template" Object -> UI -> Text and rename it to "Count"
- Change "Count" RectTransform values to ("Bottom Right align" Pivot X: 1 Pivot Y: 0 Pos X: 0 Pos Y: 0 Width: 30 Height: 30)
- Set "Count" Text to a random number (ex. 12), Font Style to Bold, Font Size to 14, Alignment to Right Bottom, and Color to (1, 1, 1, 1)
- Add Shadow component to "Count" Text and set Effect Color to (0, 0, 0, 0.5)
The end result should look like this:
Result Slot (That will be used for crafting results):
- Duplicate the "slot_template" Object and rename it to "result_slot_template"
- Change the Width and Height of "result_slot_template" to 50
Crafting Button and Additional Graphics:
- Create a new Button by Right-Clicking on "CraftingSlots" Object -> UI -> Button and rename it to "CraftButton"
- Set "CraftButton" RectTransform values to ("Middle Left align" Pivot X: 1 Pivot Y: 0.5 Pos X: 0 Pos Y: 0 Width: 40 Height: 40)
- Chage Text of "CraftButton" to "Craft"
- Create a new Image by Right-Clicking on "CraftingSlots" Object -> UI -> Image and rename it to "Arrow"
- Set "Arrow" RectTransform values to ("Middle Right align" Pivot X: 0 Pivot Y: 0.5 Pos X: 10 Pos Y: 0 Width: 30 Height: 30)
For the Source Image, you can use the image below (Right-click -> Save as.. to download it). After importing set its Texture type to "Sprite (2D and UI)" and Filter Mode to "Point (no filter)"
- Right Click on "CraftingSlots" -> Create Empty and rename it to "ResultSlot", this object will contain the result slot
- Set "ResultSlot" RectTransform values to ("Middle Right align" Pivot X: 0 Pivot Y: 0.5 Pos X: 50 Pos Y: 0 Width: 50 Height: 50)
The UI setup is ready.
Step 2: Program Crafting System
This Crafting System will consist of 2 scripts, SC_ItemCrafting.cs, and SC_SlotTemplate.cs
- Create a new script, name it "SC_ItemCrafting" then paste the code below inside it:
SC_ItemCrafting.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class SC_ItemCrafting : MonoBehaviour
{
public RectTransform playerSlotsContainer;
public RectTransform craftingSlotsContainer;
public RectTransform resultSlotContainer;
public Button craftButton;
public SC_SlotTemplate slotTemplate;
public SC_SlotTemplate resultSlotTemplate;
[System.Serializable]
public class SlotContainer
{
public Sprite itemSprite; //Sprite of the assigned item (Must be the same sprite as in items array), or leave null for no item
public int itemCount; //How many items in this slot, everything equal or under 1 will be interpreted as 1 item
[HideInInspector]
public int tableID;
[HideInInspector]
public SC_SlotTemplate slot;
}
[System.Serializable]
public class Item
{
public Sprite itemSprite;
public bool stackable = false; //Can this item be combined (stacked) together?
public string craftRecipe; //Item Keys that are required to craft this item, separated by comma (Tip: Use Craft Button in Play mode and see console for printed recipe)
}
public SlotContainer[] playerSlots;
SlotContainer[] craftSlots = new SlotContainer[9];
SlotContainer resultSlot = new SlotContainer();
//List of all available items
public Item[] items;
SlotContainer selectedItemSlot = null;
int craftTableID = -1; //ID of table where items will be placed one at a time (ex. Craft table)
int resultTableID = -1; //ID of table from where we can take items, but cannot place to
ColorBlock defaultButtonColors;
// Start is called before the first frame update
void Start()
{
//Setup slot element template
slotTemplate.container.rectTransform.pivot = new Vector2(0, 1);
slotTemplate.container.rectTransform.anchorMax = slotTemplate.container.rectTransform.anchorMin = new Vector2(0, 1);
slotTemplate.craftingController = this;
slotTemplate.gameObject.SetActive(false);
//Setup result slot element template
resultSlotTemplate.container.rectTransform.pivot = new Vector2(0, 1);
resultSlotTemplate.container.rectTransform.anchorMax = resultSlotTemplate.container.rectTransform.anchorMin = new Vector2(0, 1);
resultSlotTemplate.craftingController = this;
resultSlotTemplate.gameObject.SetActive(false);
//Attach click event to craft button
craftButton.onClick.AddListener(PerformCrafting);
//Save craft button default colors
defaultButtonColors = craftButton.colors;
//InitializeItem Crafting Slots
InitializeSlotTable(craftingSlotsContainer, slotTemplate, craftSlots, 5, 0);
UpdateItems(craftSlots);
craftTableID = 0;
//InitializeItem Player Slots
InitializeSlotTable(playerSlotsContainer, slotTemplate, playerSlots, 5, 1);
UpdateItems(playerSlots);
//InitializeItemResult Slot
InitializeSlotTable(resultSlotContainer, resultSlotTemplate, new SlotContainer[] { resultSlot }, 0, 2);
UpdateItems(new SlotContainer[] { resultSlot });
resultTableID = 2;
//Reset Slot element template (To be used later for hovering element)
slotTemplate.container.rectTransform.pivot = new Vector2(0.5f, 0.5f);
slotTemplate.container.raycastTarget = slotTemplate.item.raycastTarget = slotTemplate.count.raycastTarget = false;
}
void InitializeSlotTable(RectTransform container, SC_SlotTemplate slotTemplateTmp, SlotContainer[] slots, int margin, int tableIDTmp)
{
int resetIndex = 0;
int rowTmp = 0;
for (int i = 0; i < slots.Length; i++)
{
if (slots[i] == null)
{
slots[i] = new SlotContainer();
}
GameObject newSlot = Instantiate(slotTemplateTmp.gameObject, container.transform);
slots[i].slot = newSlot.GetComponent<SC_SlotTemplate>();
slots[i].slot.gameObject.SetActive(true);
slots[i].tableID = tableIDTmp;
float xTmp = (int)((margin + slots[i].slot.container.rectTransform.sizeDelta.x) * (i - resetIndex));
if (xTmp + slots[i].slot.container.rectTransform.sizeDelta.x + margin > container.rect.width)
{
resetIndex = i;
rowTmp++;
xTmp = 0;
}
slots[i].slot.container.rectTransform.anchoredPosition = new Vector2(margin + xTmp, -margin - ((margin + slots[i].slot.container.rectTransform.sizeDelta.y) * rowTmp));
}
}
//Update Table UI
void UpdateItems(SlotContainer[] slots)
{
for (int i = 0; i < slots.Length; i++)
{
Item slotItem = FindItem(slots[i].itemSprite);
if (slotItem != null)
{
if (!slotItem.stackable)
{
slots[i].itemCount = 1;
}
//Apply total item count
if (slots[i].itemCount > 1)
{
slots[i].slot.count.enabled = true;
slots[i].slot.count.text = slots[i].itemCount.ToString();
}
else
{
slots[i].slot.count.enabled = false;
}
//Apply item icon
slots[i].slot.item.enabled = true;
slots[i].slot.item.sprite = slotItem.itemSprite;
}
else
{
slots[i].slot.count.enabled = false;
slots[i].slot.item.enabled = false;
}
}
}
//Find Item from the items list using sprite as reference
Item FindItem(Sprite sprite)
{
if (!sprite)
return null;
for (int i = 0; i < items.Length; i++)
{
if (items[i].itemSprite == sprite)
{
return items[i];
}
}
return null;
}
//Find Item from the items list using recipe as reference
Item FindItem(string recipe)
{
if (recipe == "")
return null;
for (int i = 0; i < items.Length; i++)
{
if (items[i].craftRecipe == recipe)
{
return items[i];
}
}
return null;
}
//Called from SC_SlotTemplate.cs
public void ClickEventRecheck()
{
if (selectedItemSlot == null)
{
//Get clicked slot
selectedItemSlot = GetClickedSlot();
if (selectedItemSlot != null)
{
if (selectedItemSlot.itemSprite != null)
{
selectedItemSlot.slot.count.color = selectedItemSlot.slot.item.color = new Color(1, 1, 1, 0.5f);
}
else
{
selectedItemSlot = null;
}
}
}
else
{
SlotContainer newClickedSlot = GetClickedSlot();
if (newClickedSlot != null)
{
bool swapPositions = false;
bool releaseClick = true;
if (newClickedSlot != selectedItemSlot)
{
//We clicked on the same table but different slots
if (newClickedSlot.tableID == selectedItemSlot.tableID)
{
//Check if new clicked item is the same, then stack, if not, swap (Unless it's a crafting table, then do nothing)
if (newClickedSlot.itemSprite == selectedItemSlot.itemSprite)
{
Item slotItem = FindItem(selectedItemSlot.itemSprite);
if (slotItem.stackable)
{
//Item is the same and is stackable, remove item from previous position and add its count to a new position
selectedItemSlot.itemSprite = null;
newClickedSlot.itemCount += selectedItemSlot.itemCount;
selectedItemSlot.itemCount = 0;
}
else
{
swapPositions = true;
}
}
else
{
swapPositions = true;
}
}
else
{
//Moving to different table
if (resultTableID != newClickedSlot.tableID)
{
if (craftTableID != newClickedSlot.tableID)
{
if (newClickedSlot.itemSprite == selectedItemSlot.itemSprite)
{
Item slotItem = FindItem(selectedItemSlot.itemSprite);
if (slotItem.stackable)
{
//Item is the same and is stackable, remove item from previous position and add its count to a new position
selectedItemSlot.itemSprite = null;
newClickedSlot.itemCount += selectedItemSlot.itemCount;
selectedItemSlot.itemCount = 0;
}
else
{
swapPositions = true;
}
}
else
{
swapPositions = true;
}
}
else
{
if (newClickedSlot.itemSprite == null || newClickedSlot.itemSprite == selectedItemSlot.itemSprite)
{
//Add 1 item from selectedItemSlot
newClickedSlot.itemSprite = selectedItemSlot.itemSprite;
newClickedSlot.itemCount++;
selectedItemSlot.itemCount--;
if (selectedItemSlot.itemCount <= 0)
{
//We placed the last item
selectedItemSlot.itemSprite = null;
}
else
{
releaseClick = false;
}
}
else
{
swapPositions = true;
}
}
}
}
}
if (swapPositions)
{
//Swap items
Sprite previousItemSprite = selectedItemSlot.itemSprite;
int previousItemConunt = selectedItemSlot.itemCount;
selectedItemSlot.itemSprite = newClickedSlot.itemSprite;
selectedItemSlot.itemCount = newClickedSlot.itemCount;
newClickedSlot.itemSprite = previousItemSprite;
newClickedSlot.itemCount = previousItemConunt;
}
if (releaseClick)
{
//Release click
selectedItemSlot.slot.count.color = selectedItemSlot.slot.item.color = Color.white;
selectedItemSlot = null;
}
//Update UI
UpdateItems(playerSlots);
UpdateItems(craftSlots);
UpdateItems(new SlotContainer[] { resultSlot });
}
}
}
SlotContainer GetClickedSlot()
{
for (int i = 0; i < playerSlots.Length; i++)
{
if (playerSlots[i].slot.hasClicked)
{
playerSlots[i].slot.hasClicked = false;
return playerSlots[i];
}
}
for (int i = 0; i < craftSlots.Length; i++)
{
if (craftSlots[i].slot.hasClicked)
{
craftSlots[i].slot.hasClicked = false;
return craftSlots[i];
}
}
if (resultSlot.slot.hasClicked)
{
resultSlot.slot.hasClicked = false;
return resultSlot;
}
return null;
}
void PerformCrafting()
{
string[] combinedItemRecipe = new string[craftSlots.Length];
craftButton.colors = defaultButtonColors;
for (int i = 0; i < craftSlots.Length; i++)
{
Item slotItem = FindItem(craftSlots[i].itemSprite);
if (slotItem != null)
{
combinedItemRecipe[i] = slotItem.itemSprite.name + (craftSlots[i].itemCount > 1 ? "(" + craftSlots[i].itemCount + ")" : "");
}
else
{
combinedItemRecipe[i] = "";
}
}
string combinedRecipe = string.Join(",", combinedItemRecipe);
print(combinedRecipe);
//Search if recipe match any of the item recipe
Item craftedItem = FindItem(combinedRecipe);
if (craftedItem != null)
{
//Clear Craft slots
for (int i = 0; i < craftSlots.Length; i++)
{
craftSlots[i].itemSprite = null;
craftSlots[i].itemCount = 0;
}
resultSlot.itemSprite = craftedItem.itemSprite;
resultSlot.itemCount = 1;
UpdateItems(craftSlots);
UpdateItems(new SlotContainer[] { resultSlot });
}
else
{
ColorBlock colors = craftButton.colors;
colors.selectedColor = colors.pressedColor = new Color(0.8f, 0.55f, 0.55f, 1);
craftButton.colors = colors;
}
}
// Update is called once per frame
void Update()
{
//Slot UI follow mouse position
if (selectedItemSlot != null)
{
if (!slotTemplate.gameObject.activeSelf)
{
slotTemplate.gameObject.SetActive(true);
slotTemplate.container.enabled = false;
//Copy selected item values to slot template
slotTemplate.count.color = selectedItemSlot.slot.count.color;
slotTemplate.item.sprite = selectedItemSlot.slot.item.sprite;
slotTemplate.item.color = selectedItemSlot.slot.item.color;
}
//Make template slot follow mouse position
slotTemplate.container.rectTransform.position = Input.mousePosition;
//Update item count
slotTemplate.count.text = selectedItemSlot.slot.count.text;
slotTemplate.count.enabled = selectedItemSlot.slot.count.enabled;
}
else
{
if (slotTemplate.gameObject.activeSelf)
{
slotTemplate.gameObject.SetActive(false);
}
}
}
}
- Create a new script, name it "SC_SlotTemplate" then paste the code below inside it:
SC_SlotTemplate.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
public class SC_SlotTemplate : MonoBehaviour, IPointerClickHandler
{
public Image container;
public Image item;
public Text count;
[HideInInspector]
public bool hasClicked = false;
[HideInInspector]
public SC_ItemCrafting craftingController;
//Do this when the mouse is clicked over the selectable object this script is attached to.
public void OnPointerClick(PointerEventData eventData)
{
hasClicked = true;
craftingController.ClickEventRecheck();
}
}
Preparing Slot Templates:
- Attach the SC_SlotTemplate script to the "slot_template" object, and assign its variables (Image component on the same Object goes to the "Container" variable, child "Item" Image goes to the "Item" variable, and a child "Count" Text goes to "Count" variable)
- Repeat the same process for the "result_slot_template" Object (attach SC_SlotTemplate script to it and assign variables the same way).
Preparing Craft System:
- Attach the SC_ItemCrafting script to the Canvas object, and assign its variables ("PlayerSlots" Object goes to the "Player Slots Container" variable, "CraftingSlots" Object goes to the "Crafting Slots Container" variable, "ResultSlot" Object goes to the "Result Slot Container" variable, "CraftButton" Object goes to "Craft Button" variable, "slot_template" Object with SC_SlotTemplate script attached goes to "Slot Template" variable and "result_slot_template" Object with SC_SlotTemplate script attached goes to "Result Slot Template" variable):
As you already noticed, there are two empty arrays named "Player Slots" and "Items". "Player Slots" will contain the number of slots available (with Item or empty) and "Items" will contain all the available items along with their recipes (optional).
Setting Up Items:
Check the sprites below (in my case I will have 5 items):
(rock)
(diamond)
(wood)
(sword)
(diamond_sword)
- Download each sprite (Right Click -> Save as...) and import them to your project (In Import Settings set their Texture Type to "Sprite (2D and UI)" and Filter Mode to "Point (no filter)"
- In SC_ItemCrafting change Items Size to 5 and assign each sprite to the Item Sprite variable.
"Stackable" variable controls whether the items can be stacked together into one slot (ex. you might want to only allow stacking for simple materials such as rock, diamond and wood).
"Craft Recipe" variable controls if this item can be crafted (empty means it cannot be crafted)
- For the "Player Slots" set the Array Size to 27 (best fit for the current Crafting Panel, but you can set any number).
When you press Play, you'll notice that the slots are initialized correctly, but there are no items:
To add an item to each slot we'll need to assign an item Sprite to the "Item Sprite" variable and set the "Item Count" to any positive number (everything under 1, and/or non-stackable items will be interpreted as 1):
- Assign the "rock" sprite to Element 0 / "Item Count" 14, the "wood" sprite to Element 1 / "Item Count" 8, "diamond" sprite to Element 2 / "Item Count" 8 (Make sure the sprites are the same as in "Items" Array, otherwise it will not work).
The Items should now appear in Player Slots, you can change their position by clicking on the item, and then clicking on the slot you want to move it to.
Crafting Recipes:
Crafting recipes allows you to create an item by combining other items in a specific order:
The format for crafting recipe is as follows: [item_sprite_name]([item count])*optional... repeated 9 times, separated by comma (,)
An easy way to discover the recipe is by pressing Play, then placing the items in the order you want to craft, then pressing "Craft", after that, press (Ctrl + Shift + C) to open Unity Console and see the newly printed line (You can click "Craft" multiple times to re-print the line), the printed line is the crafting recipe.
For example, the combination below corresponds to this recipe: rock,,rock,,rock,,rock,,wood (NOTE: it may be different for you if your sprites have different names).
We will use the recipe above to craft a sword.
- Copy the printed line, and in the "Items" Array paste it in the "Craft Recipe" variable under "sword" Item:
Now when repeating that same combination you should be able to craft a sword.
A recipe for a diamond sword is the same, but instead of rock it's diamond:
The Crafting System is now ready.