Crafting System

Alright as the title suggests, this is the thread where I attempt to program a Crafting Table with @Brian_Trotter - it’s a continuation off this thread’s comment. Brian we can proceed here :slight_smile:

Alright here’s the first update. I implemented the code you mentioned earlier, and assigned all the variables to their respectful positions. This worked, and the Crafting UI showed up exactly as expected (although frankly speaking, I still don’t understand how the lambda and fly functions work yet, so the arrow => in the script below still has me baffled. This line to be specific):

        public bool HasCraftingTable => craftingTableUI != null;

Anyway, the Crafting UI works as expected now, fortunately without any major collisions with the banking, inventory or equipment systems, but hovering over the slots alone results in a Null Reference Exception error, the one below here:

NullReferenceException: Object reference not set to an instance of an object
GameDevTV.UI.Inventories.InventorySlotUI.GetItem () (at Assets/GameDev.tv Assets/Scripts/UI/Inventories/InventorySlotUI.cs:44)
GameDevTV.UI.Inventories.ItemTooltipSpawner.CanCreateTooltip () (at Assets/GameDev.tv Assets/Scripts/UI/Inventories/ItemTooltipSpawner.cs:16)
GameDevTV.Core.UI.Tooltips.TooltipSpawner.UnityEngine.EventSystems.IPointerEnterHandler.OnPointerEnter (UnityEngine.EventSystems.PointerEventData eventData) (at Assets/GameDev.tv Assets/Scripts/Utils/UI/Tooltips/TooltipSpawner.cs:57)
UnityEngine.EventSystems.ExecuteEvents.Execute (UnityEngine.EventSystems.IPointerEnterHandler handler, UnityEngine.EventSystems.BaseEventData eventData) (at Library/PackageCache/com.unity.ugui@1.0.0/Runtime/EventSystem/ExecuteEvents.cs:29)
UnityEngine.EventSystems.ExecuteEvents.Execute[T] (UnityEngine.GameObject target, UnityEngine.EventSystems.BaseEventData eventData, UnityEngine.EventSystems.ExecuteEvents+EventFunction`1[T1] functor) (at Library/PackageCache/com.unity.ugui@1.0.0/Runtime/EventSystem/ExecuteEvents.cs:272)
UnityEngine.EventSystems.EventSystem:Update() (at Library/PackageCache/com.unity.ugui@1.0.0/Runtime/EventSystem/EventSystem.cs:514)

And when I click on any of them, the debugger I set up for the inventory slot numbers displays that they’re all the exact same slot number (I’m concerned here), and it also gives me another Null Reference Exception Error, as shown below:

NullReferenceException: Object reference not set to an instance of an object
GameDevTV.UI.Inventories.InventorySlotUI.TryHandleRightClick () (at Assets/GameDev.tv Assets/Scripts/UI/Inventories/InventorySlotUI.cs:60)
UnityEngine.Events.InvokableCall.Invoke () (at <ba783288ca164d3099898a8819fcec1c>:0)
UnityEngine.Events.UnityEvent.Invoke () (at <ba783288ca164d3099898a8819fcec1c>:0)
UnityEngine.UI.Button.Press () (at Library/PackageCache/com.unity.ugui@1.0.0/Runtime/UI/Core/Button.cs:70)
UnityEngine.UI.Button.OnPointerClick (UnityEngine.EventSystems.PointerEventData eventData) (at Library/PackageCache/com.unity.ugui@1.0.0/Runtime/UI/Core/Button.cs:114)
UnityEngine.EventSystems.ExecuteEvents.Execute (UnityEngine.EventSystems.IPointerClickHandler handler, UnityEngine.EventSystems.BaseEventData eventData) (at Library/PackageCache/com.unity.ugui@1.0.0/Runtime/EventSystem/ExecuteEvents.cs:57)
UnityEngine.EventSystems.ExecuteEvents.Execute[T] (UnityEngine.GameObject target, UnityEngine.EventSystems.BaseEventData eventData, UnityEngine.EventSystems.ExecuteEvents+EventFunction`1[T1] functor) (at Library/PackageCache/com.unity.ugui@1.0.0/Runtime/EventSystem/ExecuteEvents.cs:272)
UnityEngine.EventSystems.EventSystem:Update() (at Library/PackageCache/com.unity.ugui@1.0.0/Runtime/EventSystem/EventSystem.cs:514)

My question now is, how do we get these slots to act properly? It’s the next step for me, and if we can get this fixed then I think we can start programming the recipes and the proper craft function (let’s not dive into deep details of the system just yet…). Another step I will want to reconsider coding as well will be getting interrupted through battles, or walking away from the table

And finally, this little hot fix made my ‘interrupt by walking away’ system not work properly. Can you kindly help me with this as well? This is the function (‘showHideUI’ in the script below was replaced with ‘UIToOpen’ before we fixed it):

// If you click outside the Rect Transform of your Crafting UI, automatically shut the UI Down:
                if (!RectTransformUtility.RectangleContainsScreenPoint(showHideUI.GetComponent<RectTransform>(), mousePosition)) {

                    CloseCraftingUI();

                    }

This is just a shortcut when you have a property that is only read only and one line or a method that is only one line.
It’s equivalent to:

public bool HasCraftingTable
{
    get
    {
         return craftingTableUI!=null;
    }
}

You’re using an InventorySlotUI for this, but without a backing Inventory, so when it tries to get the item, there’s no backing field for an item to get. You’ll likely need to do like we do in EquipmentSlotUI and ActionSlotUI and create a new UI that uses slots on the CraftingTable as backing fields. (Did I mention a few weeks back that creating Crafting is a big challenge?)

Because they haven’t been set up properly, That’s all done in InventorySlotUI’s setup, but an InventorySlotUI is designed to talk to an Inventory, not a Crafting Table (See above).

Um… this makes no sense… because you would never be able to drag inventory into the crafting slots…

The correct way to handle walking away is to make Crafting an IAction, and that IAction.Cancel() would cause the window to be closed.

That I fully agree with… but it’s also a rewarding system to have, hence why I really want one involved. I bought another course that doesn’t properly allign with ours for both Crafting and Resource Gathering systems, and whilst resource gathering was fine, Crafting is… still challenging, like VERY VERY CHALLENGING. Anyway… doesn’t hurt to try (ok it hurts tbh)

Well I thought it would work for the time being back then… Apparently not

Really wanting one involved is great… but I don’t want to get on the same level of unintended consequences as we did with the dialogues and respawnables. Crafting, TBH, is something that could become it’s own entire supplemental course (or a section of it, much like Dialogues and Quests comprises two topics). Expect this to be a great deal of work.

Honestly, at this point in time, I’m more than ready to go along with you (I’m way too invested in this :sweat_smile:). We did create some complex functions before.

You’re assuming I have the spare time… There’s a reason I haven’t already written a Crafting system… I’ll help where I can.

Apologies Brian, I mean no bad assumptions, please don’t take this the wrong way :slight_smile:

Sure, anything works for now. I can use as much help as I can, even if it’s very little

1 Like

Well… I spent the past few hours with ChatGPT trying to set this script up (We wrote 4 new scripts, in total. ChatGPT writes the algorithm we need, and I tune it to get it to work with our scripts), and this is what we got (which is still not working, but I highly suspect it’s an Inspector over coding issue now). Whenever you have some time, please have a look and let me know what went wrong:

using GameDevTV.Core.UI.Dragging;
using GameDevTV.Inventories;
using GameDevTV.UI.Inventories;
using UnityEngine;
using UnityEngine.EventSystems;

namespace RPG.Crafting
{

    public class CraftingSlotUI : MonoBehaviour, IItemHolder, IDragContainer<InventoryItem>, IPointerClickHandler
    {

        [SerializeField] InventoryItemIcon icon = null;
        [SerializeField] int index = 0;

        Inventory playerInventory; // The crafting Inventory, to store our items

        // CACHE
        CraftingSystem craftingSystem;
        private static CraftingSlotUI LastUIClicked;

        public void Awake()
        {
            craftingSystem = GetComponentInParent<CraftingSystem>();
            playerInventory = Inventory.GetPlayerInventory(); // Get the player's inventory
        }

        public void AddItems(InventoryItem item, int number)
        {
            craftingSystem.AddCraftItem(index, item, number);
        }

        public InventoryItem GetItem()
        {
            return craftingSystem.GetCraftItem(index);
        }

        public int GetNumber()
        {
            return craftingSystem.GetCraftItemNumber(index);
        }

        public int MaxAcceptable(InventoryItem item)
        {
            return craftingSystem.MaxAcceptable(index, item);
        }

        public void OnPointerClick(PointerEventData eventData)
        {
            if (LastUIClicked != this)
            {
                LastUIClicked = this;
                Invoke(nameof(TimesUp), 0.5f);
                Debug.Log($"{index} was clicked once");
            }
            else
            {
                HandleDoubleClick();
            }
        }

        private void TimesUp()
        {
            if (LastUIClicked == this) LastUIClicked = null;
        }

        public void HandleDoubleClick()
        {
            TimesUp();
            InventoryItem item = GetItem();
            int number = GetNumber();
            if (item == null || number < 1) return;

            if (craftingSystem.PlayerIsCraftingUIOpen())
            {
                TryTransferToPlayerInventory();
            }
            else
            {
                TryTransferToCraftingTable();
            }
        }

        private void TryTransferToPlayerInventory()
        {
            InventoryItem item = GetItem();
            if (item != null)
            {
                if (playerInventory.HasSpaceFor(item))
                {
                    playerInventory.AddToFirstEmptySlot(item, 1);
                    RemoveItems(1);
                }
            }
        }

        private void TryTransferToCraftingTable()
        {
            InventoryItem item = GetItem();
            if (item != null)
            {
                craftingSystem.AddCraftItem(index, item, 1);
                playerInventory.RemoveItem(item, 1);
            }
        }

        public void RemoveItems(int number)
        {
            craftingSystem.RemoveCraftItem(index, number);
        }

        public void UpdateUI()
        {
            icon.SetItem(GetItem(), GetNumber());
        }
    }
}

and here is the new ‘CraftingSystem.cs’ it keeps calling:

using UnityEngine;
using GameDevTV.Inventories;

namespace RPG.Crafting {

public class CraftingSystem : MonoBehaviour {

    [SerializeField] int maxCraftingSlots = 15;
    [SerializeField] Inventory craftingInventory;

    private InventoryItem[] craftItems;
    private int[] craftItemsCount;

    private void Awake() {

        InitializeCraftingSlots();
    
    }

    private void InitializeCraftingSlots() {

        craftItems = new InventoryItem[maxCraftingSlots];
        craftItemsCount = new int[maxCraftingSlots];

    }

    public bool PlayerIsCraftingUIOpen() {

        return false;

    }

    public void AddCraftItem(int slot, InventoryItem item, int number) {

        if (slot < 0 || slot >= maxCraftingSlots) {

            Debug.Log("Invalid Crafting Slot Index");
            return;

        }

        craftItems[slot] = item;
        craftItemsCount[slot] = number;

    }

    public InventoryItem GetCraftItem(int slot) {

        if (slot < 0 || slot >= maxCraftingSlots) {

            Debug.Log("Invalid Crafting Slot Index");
            return null;

        }

        return craftItems[slot];

    }

    public int GetCraftItemNumber(int slot) {

        if (slot < 0 || slot >= maxCraftingSlots) {

            Debug.Log("Invalid Crafting Slot Index");
            return 0;

        }

        return craftItemsCount[slot];

    }

    public int MaxAcceptable(int slot, InventoryItem item) {

        if (slot < 0 || slot >= maxCraftingSlots) {

            Debug.Log("Invalid Crafting Slot Index");
            return 0;

        }

        // Logic to determine the maximum number of items that can be added to the crafting slot
        // Consider factors such as available space, existing items, etc
        // and then return the appropriate value based on the requirements

        return 0;

    }

    public void RemoveCraftItem(int slot, int number) {

        if (slot < 0 || slot >= maxCraftingSlots) {

            Debug.Log("Invalid Crafting Slot Index");
            return;

        }

        craftItems[slot] = null;
        craftItemsCount[slot] = 0;

    }

}

}

So to test it, I deleted one of the slots and placed a variant slot I created, known as ‘Crafting Slot’, but it still gives out the same NRE error I mentioned above. For the ‘CraftingInventorySlot’, we have replaced the ‘InventorySlotUI.cs’ script with the ‘CraftingSlotUI.cs’ script, and the ‘OnClick()’ event in the button has been replaced from ‘InventorySlotUI.TryHandleRightClick()’ with ‘CraftingSlotUI.HandleDoubleClick()’ script instead, but none of that worked out well yet. Can you please have a look and let me know what went wrong?

Lots went wrong…
I’ve been trying to unpack it, and I’m left with more questions than answers…
ChatGPT is a great source of information on how C# concepts or Unity Engine classes work, but not necessarily as great when trying to integrate into an API it knows nothing about…

For example: MaxAcceptable will always return 0, meaning it will be impossible to ever drag and drop items into the slot…

In HandleDoubleClick, you check for if the crafting system UI is open… which… it quite literally MUST be, as this component shouldn’t be visible if the crafting system isn’t open, but the method is hard coded to be false… which takes us to TryTransferToCraftingTable… which first checks to see if there is an item in the crafting table, then without checking the Player inventory, adds another item and removes the one that may or may not exist…

I’m still left with a lot of questions…

  • Do we just transfer any old item into the Crafting Table? How do we know which items will contribute to the recipe?
  • Where or what is the recipe?

OK I really don’t mean this in any ridiculous way, but the plan was that you’d be able to drag (or click and it’ll automatically go to the first crafting slot) any item into the table. If, for instance, you drag a bunch of items that do create a special recipe, the ‘Output box’ would display the item ready to be crafted, based on what it sees (more like a cross-match kinda thing)

Are you talking about the Algorithm, or…?

OK fine… I’ll try the hard way and take a look at the code and try replicate our scripts to match what similar scripts of UI are doing (P.S: might cause more errors than good, but let’s try anyway)

OK so here’s a little update. I fixed the ‘TryHandleRightClick()’ NRE issue by going into the Inventory Slot’s prefab, and placing the ‘CraftingSlotUI.cs’ script into the InventorySlot Prefab itself (because the Prefab Variant just won’t budge…), and then re-arranging the sequence of execution of order of my code (i.e: I placed the ‘CraftingSlotUI.cs’ script right under the ‘InventorySlotUI.cs’ script, so that it can function properly. Naturally though, it also introduced the problem of my inventory clicks all being double-clicks, so clicking on the inventory slot once registers as two clicks, which now reverses the entire benefit of faster banking times we introduced earlier, by a bit). I went through the entire ‘InventorySlotUI.cs’ script, and it’s about 90% similar, with a few missing lines because… well… I’m not considering equipment in the ‘CraftingSlotUI.cs’ script for example.

However, the ‘GetItem()’ function NRE still exists, and it’s still driving me nuts…

Edit: I fixed the whole ‘CraftingSlotUI Prefab Variant’ issue… I deleted the Prefab Variant, duplicated the original prefab, deleted the ‘InventorySlotUI.cs’ script there, replaced it with the ‘CraftingSlotUI.cs’ script, and now it works quite well, so now each Inventory button is registered once, and each Crafting button click is registered once as well. Problem is, the ‘GetItem()’ NRE still exists, and here is my ‘CraftingSlotUI.cs’ script (again, some stuff is deleted from the ‘InventorySlotUI.cs’ because it’s not considering equipment or action slots, only the inventory transfer operations):

using UnityEngine;
using System.Linq;
using GameDevTV.Inventories;
using GameDevTV.Core.UI.Dragging;
using UnityEngine.EventSystems;
using GameDevTV.UI.Inventories;

namespace RPG.Crafting {

    public class CraftingSlotUI : MonoBehaviour, IItemHolder, IDragContainer<InventoryItem>, IPointerClickHandler
    {

        // CONFIG DATA
        [SerializeField] InventoryItemIcon icon = null;

        // STATE
        int index;
        InventoryItem item;
        Inventory inventory;

        // PUBLIC

        public void Setup(Inventory inventory, int index) {

            this.inventory = inventory;
            this.index = index;
            icon.SetItem(inventory.GetItemInSlot(index), inventory.GetNumberInSlot(index));

        }

        public int MaxAcceptable(InventoryItem item) {

            if (inventory.HasSpaceFor(item)) return int.MaxValue;
            return 0;

        }

        public void AddItems(InventoryItem item, int number)
        {
            inventory.AddItemToSlot(index, item, number);
        }

        public InventoryItem GetItem()
        {
            return inventory.GetItemInSlot(index);
        }

        public int GetNumber()
        {
            return inventory.GetNumberInSlot(index);
        }

        private static CraftingSlotUI LastUIClicked;

        public void OnPointerClick(PointerEventData eventData)
        {
            if (LastUIClicked != this) {

                LastUIClicked = this;
                Invoke(nameof(TimesUp), .5f);
                Debug.Log($"{index} was clicked once");

            }

            else HandleDoubleClick();

        }

        private void TimesUp() {

            if (LastUIClicked == this) LastUIClicked = null;

        }

        private void HandleDoubleClick() {

            TimesUp();  // avoids triple clicking from starting another 'HandleDoubleClick()'
            InventoryItem item = GetItem();
            int number = GetNumber();

            if (item == null || number < 1) return;

            if (inventory.gameObject.CompareTag("Player")) {

                var otherInventoryUI = FindObjectsOfType<InventoryUI>().FirstOrDefault(ui => ui.IsOtherInventory);
                if (otherInventoryUI != null && otherInventoryUI.gameObject.activeSelf && otherInventoryUI.SelectedInventory != null) {

                    Inventory otherInventory = otherInventoryUI.SelectedInventory;
                    TransferToOtherInventory(otherInventory, item, 1);

                }

                else TransferToOtherInventory(Inventory.GetPlayerInventory(), item, 1);

            }

        }

        private void TransferToOtherInventory(Inventory otherInventory, InventoryItem item, int number) {

            if (otherInventory.HasSpaceFor(item)) {

                otherInventory.AddToFirstEmptySlot(inventory.GetItemInSlot(index), number);
                inventory.RemoveItem(item, number);
                Setup(inventory, index);

            }

        }

        public void TryHandleRightClick() {

            InventoryItem item = inventory.GetItemInSlot(index);
            int number = inventory.GetNumberInSlot(index);
            if (item == null || number < 1) return; // empty slot, do nothing

            if (!inventory.gameObject.CompareTag("Player")) {

                TransferToOtherInventory(Inventory.GetPlayerInventory(), item, 1);
                return;

            }

            var otherInventoryUI = FindObjectsOfType<InventoryUI>().FirstOrDefault(ui => ui.IsOtherInventory);

            if (otherInventoryUI != null && otherInventoryUI.gameObject.activeSelf && otherInventoryUI.SelectedInventory != null) {

                Inventory otherInventory = otherInventoryUI.SelectedInventory;
                TransferToOtherInventory(otherInventory, item, 1);
                return;

            }

        }

        public void RemoveItems(int number)
        {
            inventory.RemoveFromSlot(index, number);
        }
    }
}

Edit 2: I reversed the changes because my project had a serious bug from my attempted fixes, and the NRE is back (I still can’t figure out what I did back then, but both NREs exist)…

I spent the afternoon making a quick-and-dirty Valheim-esque crafting system. It’s not the Minecraft-esque one you wanted, but it’s something. I haven’t touched the RPG course in forever and can’t remember much of what we did there, and it is very rough around the edges. Probably loads that can change to make it better.


Recipes

I started by creating the recipes. These are simple scriptable objects that will hold the required ingredients as well as the resulting inventory item

using GameDevTV.Inventories;
using UnityEngine;

namespace RPG.Crafting
{
    [CreateAssetMenu(menuName = "RPG / Crafting / Recipe")]
    public class Recipe : ScriptableObject
    {
        [SerializeField] IngredientConfig[] _ingredients;
        [SerializeField] InventoryItem _result;
        [SerializeField] float _craftDuration = 1f;

        public Sprite GetIcon() => _result.GetIcon();
        public string GetDisplayName() => _result.GetDisplayName();
        public string GetResultDescription() => _result.GetDescription();

        public IngredientConfig[] GetIngredients() => _ingredients;
        public InventoryItem GetResult() => _result;
        public float GetCraftDuration() => _craftDuration;

        [System.Serializable]
        public struct IngredientConfig
        {
            public InventoryItem Ingredient;
            public int Amount;
        }
    }
}

Create recipes that go into a Resource folder somewhere (Assets/Game/Crafting/Resources/ in my case)
image
I couldn’t be bothered to create pickups and stuff for the ingredients so I made do with what I already had
image


Crafting Table

Next, I created a simple crafting table. It’s just a model of a table with a script on it. I made the crafting table also serve as the recipe db, but this is probably not ideal.

using GameDevTV.Inventories;
using RPG.Control;
using System;
using UnityEngine;

namespace RPG.Crafting
{
    public class CraftingTable : MonoBehaviour, IRaycastable
    {
        public static event Action<Recipe[]> OnCrafting;

        private Recipe[] _allRecipes;

        private void Start()
        {
            _allRecipes = Resources.LoadAll<Recipe>("");
        }

        public static bool CanCraftRecipe(Recipe recipe)
        {
            var playerInventory = Inventory.GetPlayerInventory();
            foreach (var ingredientConfig in recipe.GetIngredients())
            {
                var hasIngredient = playerInventory.HasItem(ingredientConfig.Ingredient, out var inventoryAmount);
                if (!hasIngredient) return false;
                var hasEnough = inventoryAmount >= ingredientConfig.Amount;
                if (!hasEnough) return false;
            }
            return true;
        }

        public CursorType GetCursorType() => CursorType.Crafting;

        public bool HandleRaycast(PlayerController callingController)
        {
            if (Input.GetMouseButtonDown(0))
                OnCrafting?.Invoke(_allRecipes);
            return true;
        }
    }
}

I smacked a table somewhere in the scene with this script on it
image
I also added Crafting to the CursorType and selected the Bonus_03 icon for it.


UI

This is where all the work happens.

First, I made a UI screen that will hold a list of all the recipes. Selecting one would display some details in a panel to the right and. if it is craftable, enable the ‘craft’ button for crafting
At runtime, the whole UI looks like this


On the left is a list of all the recipes. I initially wanted to filter those to only recipes that the player has unlocked, but I am too lazy. Selecting a recipe will display the inventory item details on the right, the ingredients required and the crafting button. If the player is missing some ingredients, the amount will flash red. The button is also disabled, so it is not possible to craft the item if there aren’t enough ingredients.
This UI is a mess, but here’s what it is

CraftingUI

The whole crafting UI has a script that will listen for the event from the crafting table. When the crafting table is interacted with, the UI will populate with the recipes from the crafting table and open up

using UnityEngine;

namespace RPG.Crafting.UI
{
    public class CraftingUI : MonoBehaviour
    {
        private RecipeListUI _recipeList;

        private void Awake()
        {
            gameObject.SetActive(false);

            _recipeList = GetComponentInChildren<RecipeListUI>();

            CraftingTable.OnCrafting += OnCrafting;
        }

        private void OnDestroy()
        {
            CraftingTable.OnCrafting -= OnCrafting;
        }

        private void OnCrafting(Recipe[] recipes)
        {
            _recipeList.Setup(recipes);
            gameObject.SetActive(true);
        }
    }
}


The script is on the ‘Crafting UI’ gameObject.

RecipeListUI

Recipe list gets the list of recipes, and creates entries in the list

using System.Collections.Generic;
using UnityEngine;

namespace RPG.Crafting.UI
{
    public class RecipeListUI : MonoBehaviour
    {
        [SerializeField] Transform _container;
        [SerializeField] RecipeItemUI _recipePrefab;

        private List<RecipeItemUI> _itemsInUI = new List<RecipeItemUI>();

        private void Awake()
        {
            foreach (Transform child in _container)
                Destroy(child.gameObject);
        }

        public void Setup(Recipe[] recipes)
        {
            foreach (var item in _itemsInUI)
            {
                item.transform.SetParent(null);
                Destroy(item.gameObject);
            }
            _itemsInUI.Clear();

            for (int i = 0; i < recipes.Length; i++)
            {
                var recipeItemUI = Instantiate(_recipePrefab, _container);
                _itemsInUI.Add(recipeItemUI);
                recipeItemUI.Setup(recipes[i]);
            }
        }
    }
}


This is on the content of the recipe list.

RecipeItemUI

This is the script for each recipe in the UI. It is a button that will update the description panel when clicked

using GameDevTV.Inventories;
using System;
using TMPro;
using UnityEngine;
using UnityEngine.UI;

namespace RPG.Crafting.UI
{
    public class RecipeItemUI : MonoBehaviour
    {
        public static event Action<Recipe> OnRecipeChanged;

        [SerializeField] Image _image;
        [SerializeField] TextMeshProUGUI _itemName;
        [SerializeField] Button _button;

        private Recipe _recipe;

        private void Awake()
        {
            CraftingItemUI.ItemCrafted += OnItemCrafted;
        }

        private void OnDestroy()
        {
            CraftingItemUI.ItemCrafted -= OnItemCrafted;
        }

        public void OnSelect() => OnRecipeChanged?.Invoke(_recipe);

        public void Setup(Recipe recipe)
        {
            _recipe = recipe;
            UpdateUI();
            _button.onClick.AddListener(OnSelect);
        }

        private void OnItemCrafted() => UpdateUI();

        private void UpdateUI()
        {
            _image.sprite = _recipe.GetIcon();
            _itemName.text = _recipe.GetDisplayName();

            if (CraftingTable.CanCraftRecipe(_recipe)) return;
            _itemName.alpha = 0.5f;
        }
    }
}


Goes on the Recipe Item UI prefab

CraftingItemUI

This script displays some details of the resulting item, as well as the required ingredients. It is also the script that does the crafting. Probably not ideal, but I did this in a few hours.
Clicking the ‘Craft’ button will start a progress bar. Clicking the progress bar will cancel the crafting - Just like in Valheim. Once the progress bar is full, the ingredients are removed from the player’s inventory and the crafted item is added.

using GameDevTV.Inventories;
using RPG.Crafting.UI;
using System.Collections;
using TMPro;
using UnityEngine;
using UnityEngine.UI;

namespace RPG.Crafting.UI
{
    public class CraftingItemUI : MonoBehaviour
    {
        public static event System.Action ItemCrafted;

        [SerializeField] Image _itemIcon;
        [SerializeField] TextMeshProUGUI _itemName;
        [SerializeField] TextMeshProUGUI _itemDescription;
        [SerializeField] Transform _ingredientContainer;
        [SerializeField] IngredientUI _ingredientPrefab;
        [SerializeField] Button _craftButton;
        [SerializeField] Image _craftProgress;

        private Recipe _recipe;

        private void Awake()
        {
            _itemIcon.gameObject.SetActive(false);
            _itemName.gameObject.SetActive(false);
            _itemDescription.gameObject.SetActive(false);
            foreach (Transform child in _ingredientContainer)
                Destroy(child.gameObject);
        }

        private void OnEnable()
        {
            RecipeItemUI.OnRecipeChanged += OnRecipeChanged;
        }

        private void OnDisable()
        {
            RecipeItemUI.OnRecipeChanged -= OnRecipeChanged;
        }

        public void CraftItem()
        {
            StartCoroutine(CraftItem(_recipe));
        }

        public void CancelCrafting()
        {
            StopAllCoroutines();
            _craftProgress.gameObject.SetActive(false);
            _craftButton.interactable = CraftingTable.CanCraftRecipe(_recipe);
        }

        private void OnRecipeChanged(Recipe recipe)
        {
            StopAllCoroutines();
            _recipe = recipe;
            UpdateUI();
        }

        private void UpdateUI()
        {
            _itemIcon.sprite = _recipe.GetIcon();
            _itemName.text = _recipe.GetDisplayName();
            _itemDescription.text = _recipe.GetResultDescription();

            foreach (Transform child in _ingredientContainer)
                Destroy(child.gameObject);

            foreach (var ingredient in _recipe.GetIngredients())
            {
                var ingredientUI = Instantiate(_ingredientPrefab, _ingredientContainer);
                ingredientUI.Setup(ingredient.Ingredient, ingredient.Amount);
            }

            _itemIcon.gameObject.SetActive(true);
            _itemName.gameObject.SetActive(true);
            _itemDescription.gameObject.SetActive(true);

            _craftButton.interactable = CraftingTable.CanCraftRecipe(_recipe);
        }

        private IEnumerator CraftItem(Recipe recipe)
        {
            _craftButton.interactable = false;
            _craftProgress.fillAmount = 0;
            _craftProgress.gameObject.SetActive(true);
            for (var timer = 0f; timer / recipe.GetCraftDuration() <= 1f; timer += Time.deltaTime)
            {
                _craftProgress.fillAmount = timer / recipe.GetCraftDuration();
                yield return null;
            }
            _craftButton.interactable = CraftingTable.CanCraftRecipe(recipe);
            _craftProgress.gameObject.SetActive(false);

            var inventory = Inventory.GetPlayerInventory();
            foreach (var ingredient in recipe.GetIngredients())
                inventory.RemoveItem(ingredient.Ingredient, ingredient.Amount);
            inventory.AddToFirstEmptySlot(recipe.GetResult(), 1);

            UpdateUI();
            ItemCrafted?.Invoke();
        }
    }
}


Sits on this panel. The craft button’s click event is bound to the CraftItem() method, and the ‘Image’ below it is actually a button that is bound to the CancelCrafting() method

IngredientUI

This script is for the ingredients list on the details panel. It also checks if the player has that specific ingredient (or enough if we need more than 1) and will flash the number red if it’s missing or insufficient

using GameDevTV.Inventories;
using System.Collections;
using TMPro;
using UnityEngine;
using UnityEngine.UI;

public class IngredientUI : MonoBehaviour
{
    [SerializeField] Image _image;
    [SerializeField] TextMeshProUGUI _nameText;
    [SerializeField] TextMeshProUGUI _amountText;

    private int _amount;
    private InventoryItem _ingredient;

    public void Setup(InventoryItem item, int amount)
    {
        _amount = amount;
        _ingredient = item;

        _amountText.text = _amount.ToString();
        _image.sprite = _ingredient.GetIcon();
        _nameText.text = _ingredient.GetDisplayName();

        if (CanCraftItem()) return;
        StartCoroutine(FlashAmountRoutine());
    }

    private bool CanCraftItem()
    {
        var playerInventory = Inventory.GetPlayerInventory();
        var hasIngredient = playerInventory.HasItem(_ingredient, out var inventoryAmount);
        var hasEnough = inventoryAmount >= _amount;
        return hasIngredient && hasEnough;
    }

    private IEnumerator FlashAmountRoutine()
    {
        var isOn = false;
        var defaultColor = _amountText.color;
        var waitSome = new WaitForSeconds(0.25f);
        while (true)
        {
            _amountText.color = isOn ? Color.red : defaultColor;
            isOn = !isOn;
            yield return waitSome;
        }
    }
}


Goes on the Ingredient UI prefab

I think that’s all the new scripts. I added a new function to the Inventory script so that I not only get whether or not the player has the item, but how many of it is in the inventory

using System;
using UnityEngine;
using GameDevTV.Saving;
using RPG.Core;

namespace GameDevTV.Inventories
{
    /// <summary>
    /// Provides storage for the player inventory. A configurable number of
    /// slots are available.
    ///
    /// This component should be placed on the GameObject tagged "Player".
    /// </summary>
    public class Inventory : MonoBehaviour, ISaveable
    {

        // ... other code omitted ...

        /// <summary>
        /// Is there an instance of the item in the inventory? Also returns the amount
        /// </summary>
        public bool HasItem(InventoryItem item, out int amount)
        {
            amount = 0;
            var hasItem = false;
            for (int i = 0; i < slots.Length; i++)
            {
                if (object.ReferenceEquals(slots[i].item, item))
                {
                    amount += slots[i].number;
                    hasItem = true;
                }
            }
            return hasItem;
        }
    }
}

Now we craft. Pick up the stuff and find the crafting table and craft. Here’s a short video of the system

1 Like

Hey @bixarrio, thank you so so so much for taking the time and effort to help me out with this system. Believe me, I desperately needed that (I almost gave up on it, at least for today. 36 hours no results? Give it a break…). I’ll finish preparation for tomorrow’s class (I recently got a job as a high school JS and Python programming teacher :stuck_out_tongue_winking_eye: ) and have a look at this system either by today or tomorrow. Again, thank you for helping me out :slight_smile:

(P.S: do you mind if we try improve it together one way or the other down the line? Maybe add trait level constraints or something, that would be amazing!)

In return, I can help you set your character up to be able to move to the Crafting Table (and properly open the UI when he arrives) prior to crafting anything. It’s the only algorithm I got running for me anyway…

Another serious question though, where’d you get the wind effects and water shaders from? Mine are bland, I could really use a refreshing view

Regarding this foreach loop in ‘CraftingTable.cs’, for some reason, my ‘recipe.GetIngredients()’ parameter at the top of the foreach loop is giving me an error that says the following:

foreach statement cannot operate on variables of type 'CraftingRecipe.IngredientConfig' because 'CraftingRecipe.IngredientConfig' does not contain a public instance or extension definition for 'GetEnumerator'CS1579

How do you solve that? So far I only coded the crafting recipe and currently walking through this script

What is recipe.GetIngredients returning?

@Bahaa You might have seen me mention starting here with the recipe first. :slight_smile:

Umm… just ‘ingredients’? This is how I defined it in my ‘CraftingRecipe.cs’ (I still get a little confused by the arrow, so for now I’m just taking the longer form):

public IngredientConfig GetIngredients() {

            return ingredients;

        }

Don’t worry about the => syntactic sugar just now… but you might want to go back to the Recipe section and look again at the type of ingredients (and the type of GetIngredients)…

Yes you did, but… I never created a crafting system before, so I’m still delusional on this system :stuck_out_tongue_winking_eye:

Privacy & Terms