Sharp Coder is reader-supported, meaning when you buy through links on our site, we may earn an affiliate commission.
HomeBlogUnityProgramming

Unity 3D Inventory and Item Crafting System

0 Comments - Jan 2, 2021
1556

In this tutorial, I will be showing how to make a Minecraft-style inventory and item crafting system in Unity 3D.

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 is fully automated, meaning it will work with any UI layout and with the ability to create custom crafting recipes.

Sharp Coder Video Player

Let's begin.

Part 1: UI

Setting up the UI:

  • Create new Canvas (Unity Top Taskbar: GameObject -> UI -> Canvas)
  • Create new Image by Right-Clicking on Canvas Object -> UI -> Image
  • Rename the Image Object to "CraftingPanel" and change it's 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 first Object to "CraftingSlots" and change it's 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 second Object to "PlayerSlots" and change it's 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 "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 "SectionTitle" Object, change its Text to "Crafting" and move it under "CraftingSlots" Object, then set the same RectTransform values as previous "SectionTitle".

Crafting Slot:

Crafting Slot will consist of a background image, item image, and a count text:

  • Create new Image by Right-Clicking on Canvas Object -> UI -> Image
  • Rename 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 result):

  • Duplicate "slot_template" Object and rename it to "result_slot_template"
  • Change Width and Height of "result_slot_template" to 50

Crafting Button and Additional Graphics:

  • Create 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"

Craft Button

  • Create 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)"

Arrow Icon Pixelated

  • Right Click on "CraftingSlots" -> Create Empty and rename it to "ResultSlot", this object will contain 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.

Part 2: Programming 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();
    //Lis 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 SC_SlotTemplate script to "slot_template" Object, assign its variables (Image component on the same Object goes to "Container" variable, child "Item" Image goes to "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 SC_ItemCrafting script to Canvas Object, assign its variables ("PlayerSlots" Object goes to "Player Slots Container" variable, "CraftingSlots" Object goes to "Crafting Slots Container" variable, "ResultSlot" Object goes to "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 Item (rock)

Diamond Item (diamond)

Wood Item (wood)

Sword Item (sword)

Diamond 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)"

Items in Unity Project View

  • 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 Array Size to 27 (best fit for current Crafting Panel, but you can set any number).

If you press Play, you will 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 "rock" sprite to Element 0 / "Item Count" 14, "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, then clicking on the slot you want to move it to.

Crafting Recipes:

Crafting recipes allow 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)

Sword Crafting Recipe

We will use the recipe above to craft a sword.

  • Copy 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:

Diamond Sword Recipe

Unity Inventory System and Item Crafting

Crafting System is now ready.

You Do Not Have To Do Everything Yourself
Search from thousands of ready-to-use Unity assets.
Visit Asset Store