This crafting system was created by me, Adir Baron, with knowledge from the RPG Combat and Inventory courses. The system uses most of the already existing functionalities in the RPG project, for example, UI elements, character movement and cursor types. Using this system, designers will be able to create crafting recipes with Scriptable Objects and specify ingredients and a result item.


System Functionality Overview

In order to use the system, the player first must be close enough to the workbench in order to access it. If not close enough, the character will automatically move to the bench, by checking if the player character is within the set minimum distance. Once close enough, when the player clicks on the bench again, he will be presented with the crafting UI.

In the UI itself, there are two panels. On the left is the crafting panel, and on the right is a duplicate of the inventory panel from the RPG Inventory course. In the crafting panel, you can see a list of recipes. Each recipe consists of at least two ingredients and a result item. Recipes can require more than one of the same ingredient, for example the health potion in the second recipe presented above.

After clicking on the craft button, the system will check if the player has the required ingredients in their inventory, and if they do, it will remove the items from their inventory and supply them with the crafted item.

Lastly, if the player character gets too far from the table, the crafting UI will close automatically.

System Architecture

As shown in the diagram above, the crafting system is mostly using the inventory system from the RPG Inventory course. While I will briefly explain what each of the used classes of the inventory system do, I will not go into each class of the system.

The crafting system also uses a couple of systems from the RPG Combat course. The character movement (CharacterMovement) and mouse interaction (IRaycastable).

Scripts

In this section we will go through the scripts that are being used to create this system. You can find the complete code in a repository at the end of this post.

Craft

The Craft class is the class that handles the crafting process. It checks that the players have the required ingredient items in their inventory. If they do, the system will remove the ingredient items from the inventory and add the result item.

The biggest part of the Craft class is the HasIngredients method. When I first wrote this class, it only handled non stackable items without taking into account that a stackable item can be required as an ingredient. So to add the missing part, I introduced a new method called GetItemSlot in the Inventory class of the Inventory system. GetItemSlot checks that the player has the stackable required items by going through all of the inventory slots while checking for stacks with the same amount number as the required ingredient. If the item is NOT stackable, it simply looks for the item in the inventory using the already built in method HasItem in the inventory class.

private bool HasIngredients(Inventory inventory, CraftingRecipe.Recipes recipe)
{
    // Create boolean to store result.
    bool hasItem = false;
    // Iterate through all of the ingredients in the specified recipe in the parameter.
    foreach(CraftingRecipe.Ingredients ingredient in recipe.ingredients)
    {
        // Check if the current ingredient iteration's item is stackable.
        if(ingredient.item.IsStackable())
        {
            // If the item is stackable, get the inventory slot index number that the stack is in.
            // We check if the returned value equals to or is greater than 0, because GetItemSlot returns -1 by default if no slot is found.
            // Assign the if statement value to the boolean variable.
            hasItem = inventory.GetItemSlot(ingredient.item, ingredient.number) >= 0;
        }
        else
        {
            // If the item is NOT Stackable, check if the required item is in the player's inventory.
            // Assign the if statement value to the boolean variable.
            hasItem = inventory.HasItem(ingredient.item, ingredient.number);
        }
        // If the assigned value is false, the method will return false.
        if(!hasItem) return false;
    }
    // The method will return true by default.
    return true;
}

GetItemSlot is the method that I introduced in the Inventory class from the RPG Inventory course. It finds a slot by the specified parameters and either returns the slot number or -1. It returns -1 if not found anything, because the inventory slots start from 0. It does that by iterating through the player's inventory and checking if the specified parameter item value is the same as the current iteration item, and if the number of items found in the slot is greater or equals to the specified number parameter value.

public int GetItemSlot(InventoryItem item, int number)
{
    // Loop through all of the inventory slots. 
    for (int i = 0; i < slots.Length; i++)
    {
        // Check if the current iteration's item equals to the parameter item.
        // Check if the current iteration's item number equals to or greater than the parameter number.
        if (object.ReferenceEquals(slots[i].item, item) && GetNumberInSlot(i) >= number)
        {
            // If true, return the slot number
            return i;
        }
    }
    // Return -1 by default.
    // When using this method to check if a slot is found, we simply check if the returned value equals to or greater than 0.
    return -1;
}

Next, in the RemoveItems method we remove the ingredient items from the player's inventory on successful crafting. We do that by iterating through the ingredients of the specified recipe parameter value, and here like in the HasIngredients method, we check if the current ingredient iteration item is stackable. We do that to make sure that we handle cases where recipes can require more than one item of the same ingredient (like the red potion). If the item is not stackable, we use a for loop to iterate through the ingredient amount number and for each iteration get the slot index number of the item we found and remove the item from the slot. That way we make sure to remove each item that match the recipe ingredients from the inventory.

private void RemoveItems(Inventory inventory, CraftingRecipe.Recipes recipe)
{
    // Iterate through all of the ingredients in the specified recipe in the parameter.
    foreach (CraftingRecipe.Ingredients ingredient in recipe.ingredients)
    {
        // Check if the current ingredient iteration's item is stackable.
        if (ingredient.item.IsStackable())
        {
            // If the item is stackable, get the slot index number the item is found in.
            // Assign the slot index number in a variable.
            int itemSlot = inventory.GetItemSlot(ingredient.item, ingredient.number);
            // Remove the items from the player's inventory slot with the amount number.
            inventory.RemoveFromSlot(itemSlot, ingredient.number);
        }
        else
        {
            // If the item is NOT stackable, loop through the current foreach loop iteration's ingredient amount number.
            for (int i = 0; i < ingredient.number; i++)
            {
                // Get the slot index number that the item is found in.
                // Because we know that the item is not stackable, we can simply say that the amount is 1.
                int itemSlot = inventory.GetItemSlot(ingredient.item, 1);
                // Remove the items from the player's inventory slot.
                inventory.RemoveFromSlot(itemSlot, 1);
            }
        }
    }
}

CraftItem is the last method in the Craft class. In this method we utilize all the methods we already created. We check if the player has the ingredient items in the inventory and if so, remove the items from the inventory and add the recipe result item to the first empty slot in the player's inventory. AddToFirstEmptySlot is a method in the Inventory class.

public void CraftItem(Inventory inventory, InventoryItem inventoryItem, CraftingRecipe.Recipes recipe)
{
    // Check if the player has the ingredients in the inventory.
    if(HasIngredients(inventory, recipe))
    {
        // If true
        // Rremove the items from the inventory.
        RemoveItems(inventory, recipe);
        // Add the recipe result item to the first empty slot in the inventory.
        inventory.AddToFirstEmptySlot(inventoryItem, 1);
    }
}

CraftingRecipe

The CraftingRecipe class is a ScriptableObject class and it is used to create and store recipe data such as ingredients, ingredient amounts and result items. When I first created this scriptable object class, it was only possible to require one item of each ingredient, but as I mentioned before, this system does allow a recipe to require more than one item of the same recipe. This way designers can create a lot more variations of different recipes, or even the same recipe with better or worse result items.

The CraftingRecipe class introduces two different classes nested in it:
Recipes - The Recipes class has two properties:

  • item (InventoryItem): The result item that the player will get after successfully crafting.
  • ingredients (Ingredients[]): An array of the class Ingredients. Ingredients contain the ingredient properties (item and number).

Ingredients - The Ingredients class also has two properties:

  • Item (InventoryItem): The ingredient item that the player is required to possess in their inventory.
  • number (int): The amount of the specific item that the player is required to possess.

The classes have the System.Serializable attribute in order to expose them in the Unity inspector. We use this instead of Unity’s SerializeField because Unity does not recognise classes as serializable objects.

We have only one method in the CraftingRecipe class and that is GetCraftingRecipes. It returns the Recipes array from the recipes variable. This method is used to retrieve all of the recipes data, but you can create other methods to retrieve more specific data.

CraftingUI

The next class is the class that actually handles the UI creation.
The class does that by destroying the UI elements and creating them every time the player interacts with the crafting table and shows the UI. This class includes member variables which you can see in the Github repository.

void Awake():
In Awake we simply assign the cached references.

void SetupRecipes(CraftingRecipe recipe):
This method is called from the ShowCraftingUI class when clicking on a crafting table. It assigns the recipe parameter value to the recipe variable in the local scope of the SetupRecipes class. Then it starts the UI construction by calling the Redraw method.

As mentioned above, we call the Redraw method call from SetupRecipes in order to make sure that the crafting UI updates correctly with new recipes in case the scriptableobject changes during runtime. We do that by first destroying all of the child elements under the script's gameobject. Then we iterate through all of the crafting recipes using the GetCraftingRecipes method in the CraftingRecipe class. For each recipe that is found, we instantiate UI elements and assign values like icons and amount numbers.

private void Redraw()
{
    // Destroy all of the child elements in the current gameobject's transform.
    DestroyChild(transform);
 
    // Iterate through all of the crafting recipes.
    for (int i = 0; i < craftingRecipe.GetCraftingRecipes().Length; i++)
    {
        // For each crafting recipe:
        // Create a recipe holder gameobject in under the current transform.
        var recipeHolder = Instantiate(recipePrefab, transform);
        // Destroy all of the child elements in the recipe holder gameobject.
        DestroyChild(recipeHolder.transform);
 
        // Assign the current recipe iteration to a new variable.
        var recipe = craftingRecipe.GetCraftingRecipes()[i];
 
        // Create and setup recipe ingredient gameobject elements under the recipe holder gameobject transform.
        CreateRecipeIngredients(recipe, recipeHolder.transform);
 
        // Create and setup recipe objects. Arrow image, recipe result item and craft button.
        CreateRecipeObjects(craftingRecipe.GetCraftingRecipes()[i].item, recipeHolder.transform, recipe);
    }
}
.

In CreateRecipeIngredients we create all of the ingredient item UI elements. We first loop through all of the ingredients in the recipe we get from the first parameter (recipe), then for each iteration we instantiate the prefab we reference in the itemSlot variable. Lastly we use the Setup method in the CraftingSlotUI class to set up the item’s (ingredientItem) icon and number amount.

private void CreateRecipeIngredients(CraftingRecipe.Recipes recipe, Transform recipeHolder)
{
    // Store recipe ingredients length in a variable.
    int ingredientsSize = recipe.ingredients.Length;
    // Loop through all of the ingredients in the recipe.
    for (int ingredient = 0; ingredient < ingredientsSize; ingredient++)
    {
        // Create the itemSlot prefab and make it a child under the recipeHolder transform.
        var ingredientItem = Instantiate(itemSlot, recipeHolder);
 
        // Set up the item’s (ingredientItem) icon and number amount.
        ingredientItem.Setup(recipe.ingredients[ingredient].item, recipe.ingredients[ingredient].number);
    }
}

In CreateRecipeObjects we create the rest of the UI elements in the crafting UI. We first create the arrow image after the ingredient items. Then we create the result item and set it up to have the item image. Lastly we create the crafting button, get its Craft script component, and add a listener to its OnClick event for the CraftItem method in the Craft class.

private void CreateRecipeObjects(InventoryItem inventoryItem, Transform recipeHolder, CraftingRecipe.Recipes recipe)
{
    // Create the arrow image UI element and make it a child under the recipeHolder transform.
    var arrow = Instantiate(recipeArrow, recipeHolder);
    
    // Instantiate the itemSlot prefab and make it a child under the recipeHolder transform.
    var item = Instantiate(itemSlot, recipeHolder);
    // Set up the item’s (item) icon and number amount.
    item.Setup(inventoryItem, 1);
 
    // Create the crafting button UI element and make it a child under the recipeHolder transform.
    var button = Instantiate(craftButton, recipeHolder);
    // Get the Craft script component from the button gameobject and store it in a variable.
    var craft = button.GetComponent<Craft>();
    // Add a listener to the button onClick event using a lambda expression.
    button.onClick.AddListener(() => craft.CraftItem(inventory, inventoryItem, recipe));
}

void DestroyChild(Transform transform):
DestroyChild is the method we use to remove all child elements in transforms. It works by iterating through all of the child gameobjects of the parameter specified transform and removing them via Unity’s Destroy method.

ShowCraftingUI

ShowCraftingUI is responsible for showing and hiding the crafting UI. This script is a component for the table GameObjects, and it is using the IRayastable interface. This script uses some member variables which you can see in the Github repository.

CraftingSlotUI

In this method we set the crafting UI items (ingredients and result item).
We set the item images and the amount icons numbers that show on the item UI images. We also implement the IItemHolder interface to return the inventory item for the tooltip system. This class includes member variables which you can see in the Github repository.

Getting The Code

You can find the source code for the system in this Github Gist.
In order to download the files, click on the 'Download ZIP' button.