Help with simple Crafting.

Hello. :slight_smile:

I’m trying to make a simple Crafting logic for a game. It basically consists of a class of Items and
Recipes, the Items class have some settings such as Name, ID and Amount and i’m using Lists to Add/Remove the Items from the game.
What i’m trying to make is to take a set of Items from it’s Item List and combine them to make
another Item in a Craft function using the set of Items IDs (Or maybe something fancier?), and also
check if the Items amount the Recipe requires are available. all of this done in the same script.

This is what i have so far:

[System.Serializable]
    public class Item
    {
        public string itemName;
        public int itemID; //This is used to keep track of this item and maybe for crafting it?
        public int itemAmount; //This is to know how many of this Item are in the game.
        public int itemLimit; //This is the limit amount of this Item.
        public GameObject itemObject;
    }

    [System.Serializable]
    public class CraftingRecipe
    {
        public string recipeName;
        public int recipeID;
        public int recipeAmount; //The amount of the Item to output.
        public GameObject recipeObject; //The output Object to be used in the scene.
    }

    public List<Item> items = new List<Item>();
    public List<CraftingRecipe> recipes = new List<CraftingRecipe>();

    public void Craft(int id)
    {
       //Craft the item.
    }

I know it sounds like a simple thing to do, but i’ve spent a few days trying to figure this out.
Please use the settings i’ve shown or maybe improve it if you wish.
I’m really looking forward to a answer to this. :wink:

Hi @rogueghost

I haven’t ever written proper crafting system, but I’d write pseudo code myself first. It would be helpful.

List the steps and put them in correct order.

You already have item and recipe classes, you don’t need much more, your Craft is now basically one empty method.

First it is helpful to check if you have each ingredient / component you need.

If you have each component and correct amount, then you can craft the item, which basically means reducing each component amount needed for crafting from your inventory.

Then spawn craftable item result object.

1 Like

@eses Thanks for the reply! :slight_smile:

This is one thing i’m having trouble with and need a bit of help with the code.
How do i reference the Item Ingredients in the Recipe and check for it in the Craft() function? So that i can check if its Amount is enough to craft and then decrease it and other stuff needed?

So here is our crafting system in action (mind the bad colour, to reduce size this is a highly compressed gif):
4069939--354610--CraftingExample.gif

How we accomplish this is that we have ScriptableObject’s for both item’s and recipes:

InventoryItem:

using UnityEngine;
using System.Collections.Generic;

using com.spacepuppy;
using com.spacepuppy.Scenario;
using com.spacepuppy.Utils;

using com.mansion.Entities.Inventory.UI;

namespace com.mansion.Entities.Inventory
{
 
    [CreateAssetMenu(fileName = "InventoryItem", menuName = "Inventory/InventoryItem")]
    public class InventoryItem : ScriptableObject, INameable, IObservableTrigger
    {

        #region Fields

        [SerializeField]
        private InventoryUsage _usage;

        [SerializeField]
        private Sprite _icon;

        [SerializeField]
        private InventoryItemViewUI _menuVisual;
   
        [SerializeField]
        private string _title;

        [SerializeField]
        private string _description;

        [SerializeField]
        [Tooltip("Items that are unique can only be added to the InventoryPouch once. Re-adding it doesn't do anything.")]
        private bool _unique = true;

        [SerializeField]
        private bool _consumeOnUse = true;

        [SerializeField]
        private Trigger _onUsed;
   
        #endregion

        #region CONSTRUCTOR
   
        public InventoryItem()
        {
            _nameCache = new NameCache.UnityObjectNameCache(this);
        }

        #endregion

        #region Properties

        public virtual InventoryUsage Usage
        {
            get { return _usage; }
            set { _usage = value; }
        }
   
        public Sprite Icon
        {
            get { return _icon; }
            set { _icon = value; }
        }
   
        public virtual InventoryItemViewUI MenuVisual
        {
            get { return _menuVisual; }
            set { _menuVisual = value; }
        }

        public string Title
        {
            get { return _title; }
            set { _title = value; }
        }

        public string Description
        {
            get { return _description; }
            set { _description = value; }
        }

        public bool Unique
        {
            get { return _unique; }
            set { _unique = value; }
        }

        public bool ConsumeOnUse
        {
            get { return _consumeOnUse; }
            set { _consumeOnUse = value; }
        }

        public Trigger OnUsed
        {
            get { return _onUsed; }
        }

        #endregion

        #region Methods

        public void Use()
        {
            _onUsed.ActivateTrigger(this, null);
        }

        public virtual void OnItemAddedToInventory(InventoryPouch pouch)
        {
            //do nothing
        }

        public virtual void OnItemRemovedFromInventory(InventoryPouch pouch)
        {
            //do nothing
        }
   
        public virtual bool PlayerHasInInventory(PlayerEntity player, bool isEquipped)
        {
            if (isEquipped) return false;

            var inventory = player.GetComponent<InventoryPouch>();
            if (inventory == null) return false;

            return inventory.Items.Contains(this);
        }

        #endregion

        #region INameable Interface

        private NameCache.UnityObjectNameCache _nameCache;
        public new string name
        {
            get { return _nameCache.Name; }
            set { _nameCache.Name = value; }
        }
        string INameable.Name
        {
            get { return _nameCache.Name; }
            set { _nameCache.Name = value; }
        }
        public bool CompareName(string nm)
        {
            return _nameCache.CompareName(nm);
        }
        void INameable.SetDirty()
        {
            _nameCache.SetDirty();
        }

        #endregion

        #region IObservableTrigger Interface

        Trigger[] IObservableTrigger.GetTriggers()
        {
            return new Trigger[] { _onUsed };
        }

        #endregion

    }

}

ItemRecipe:

using UnityEngine;
using System.Collections.Generic;
using System.Linq;

using com.spacepuppy;
using com.spacepuppy.Scenario;
using com.spacepuppy.Utils;

namespace com.mansion.Entities.Inventory
{

    [CreateAssetMenu(fileName = "ItemRecipe", menuName = "Inventory/ItemRecipe")]
    public class ItemRecipe : ScriptableObject, INameable
    {
   
        #region Fields

        [SerializeField]
        private InventoryItem _item;

        [SerializeField]
        [ReorderableArray()]
        [DisableOnPlay()]
        private InventoryItem[] _recipe;

        [SerializeField]
        private bool _consumeItems = true;

        [SerializeField]
        private Trigger _onCreated;

        #endregion

        #region Properties

        public InventoryItem Item
        {
            get { return _item; }
            set
            {
                _item = value;
            }
        }

        public InventoryItem[] Recipe
        {
            get { return _recipe; }
            set { _recipe = value; }
        }

        public bool ConsumeItems
        {
            get { return _consumeItems; }
            set { _consumeItems = value; }
        }

        public int RecipeItemCount
        {
            get { return _recipe != null ? _recipe.Length : 0; }
        }

        public Trigger OnCreated
        {
            get { return _onCreated; }
        }

        #endregion

        #region Methods

        /// <summary>
        /// Tests if the recipe uses the item at all.
        /// </summary>
        /// <param name="item"></param>
        /// <returns></returns>
        public bool UsesItem(InventoryItem item)
        {
            if (_recipe == null || _recipe.Length == 0) return false;

            return System.Array.IndexOf(_recipe, item) >= 0;
        }

        /// <summary>
        /// Tests if this combination works for this recipe.
        /// </summary>
        /// <param name="a"></param>
        /// <param name="b"></param>
        /// <returns></returns>
        public bool Matches(InventoryItem a, InventoryItem b)
        {
            if (_recipe == null || _recipe.Length != 2) return false;

            if (_recipe[0] == a)
                return _recipe[1] == b;
            else if (_recipe[1] == a)
                return _recipe[0] == b;
            else
                return false;
        }

        /// <summary>
        /// Tests if this combination works for this recipe.
        /// </summary>
        /// <param name="arr"></param>
        /// <returns></returns>
        public bool Matches(params InventoryItem[] arr)
        {
            if (_recipe == null) return arr == null;
            if (arr == null) return _recipe == null;
            if (_recipe.Length != arr.Length) return false;

            return !_recipe.Except(arr).Any();
        }

        public bool Matches(ICollection<InventoryItem> coll)
        {
            if (_recipe == null) return coll == null;
            if (coll == null) return _recipe == null;
            if (_recipe.Length != coll.Count) return false;

            return !_recipe.Except(coll).Any();
        }

        public bool PartialMatch(params InventoryItem[] arr)
        {
            if (_recipe == null) return arr == null;
            if (arr == null) return _recipe == null;

            return !arr.Except(_recipe).Any();
        }

        public bool PartialMatch(ICollection<InventoryItem> coll)
        {
            if (_recipe == null) return coll == null;
            if (coll == null) return _recipe == null;

            return !coll.Except(_recipe).Any();
        }

        #endregion

        #region INameable Interface

        private NameCache.UnityObjectNameCache _nameCache;
        public new string name
        {
            get { return _nameCache.Name; }
            set { _nameCache.Name = value; }
        }
        string INameable.Name
        {
            get { return _nameCache.Name; }
            set { _nameCache.Name = value; }
        }
        public bool CompareName(string nm)
        {
            return _nameCache.CompareName(nm);
        }
        void INameable.SetDirty()
        {
            _nameCache.SetDirty();
        }

        #endregion

    }

}

(you may notice more than 1 version of Matches… this is because early versions of our crafting system only allowed for 2 items to combine… we later added the ability to do multi-item combination like this fishing rod)

As for the general logic for combining here is the inventory screens update cycle. Mind you that because it supports multiple interactions (using, combining, equipping, etc), there is more going on here than JUST combining.

        private void Update()
        {
            //boiler plate
            _display.Spindle.DefaultUpdate_AnimateSpindleElements(true);
            if (!_display.IsShowing || _routine != null) return;

            var input = Services.Get<IInputManager>().GetDevice<MansionInputDevice>(Game.MAIN_INPUT);
            if (input == null || !_display.IsShowing) return;

            if (input.GetExitMenu(true, true))
            {
                _display.Hide();
                return;
            }

            if (_display.Spindle.DefaultUpdate_ProcessSpindleInputRotation(input)) this.UpdateDisplayText();
       
            //process for this state
            switch(_state)
            {
                case State.Standard:
                    {
                        if (input.GetButtonState(MansionInputs.Action) == ButtonState.Down)
                        {
                            var item = _display.Spindle.GetCurrentlyFocusedInventoryView();
                            switch (item != null && item.Item != null ? item.Item.Usage : InventoryUsage.Default)
                            {
                                case InventoryUsage.Default:
                                    //do nothing
                                    break;
                                case InventoryUsage.Equippable:
                                    if (item.Item is WeaponInventoryItem)
                                    {
                                        //we close on select
                                        if (_display.CurrentlyEquippedWeaponIndex == _display.Spindle.FocusedIndex ||
                                            _display.ChangeEquippedWeapon(_display.Spindle.FocusedIndex))
                                            this.Invoke(() => _display.Hide(), 0.1f, SPTime.Real);
                                    }
                                    break;
                                case InventoryUsage.Usable:
                                    {
                                        _routine = this.StartRadicalCoroutine(this.DoConsumeItem(item));
                                    }
                                    break;
                                case InventoryUsage.Combinable:
                                    {
                                        _state = State.WaitingOnCombination;
                                        _waitingOnCombinationItems.Add(item.Item);
                                        item.SetHighlighted(true, cakeslice.OutlineEffect.OutlinePreset.B);
                                        for(int i = 0; i < _display.Items.Count; i++)
                                        {
                                            if (_display.Items[i] != item)
                                                _display.Items[i].SetHighlighted(false);
                                        }
                                    }
                                    break;
                            }
                        }
                    }
                    break;
                case State.WaitingOnCombination:
                    {
                        if (input.GetButtonState(MansionInputs.Action) == ButtonState.Down)
                        {
                            var item = _display.Spindle.GetCurrentlyFocusedInventoryView();
                            if(item != null && item.Item != null)
                            {
                                if (Game.EpisodeSettings.RecipeSet == null)
                                {
                                    _state = State.Standard;
                                    _display.SetHighlights();
                                    _waitingOnCombinationItems.Clear();
                                    _onCombinationFail.ActivateTrigger(this, null);
                                }
                                else
                                {
                                    item.SetHighlighted(true, cakeslice.OutlineEffect.OutlinePreset.B);
                                    _waitingOnCombinationItems.Add(item.Item);

                                    int foundMatch = 0; //0 = no, 1 = partial, 2 = yes
                                    var recipes = Game.EpisodeSettings.RecipeSet;
                                    foreach (var recipe in recipes.GetAllAssets<ItemRecipe>())
                                    {
                                        if (recipe.Matches(_waitingOnCombinationItems))
                                        {
                                            _routine = this.StartRadicalCoroutine(this.DoCombineItem(recipe));
                                            foundMatch = 2;
                                            break;
                                        }
                                        else if(recipe.PartialMatch(_waitingOnCombinationItems))
                                        {
                                            foundMatch = 1;
                                        }
                                    }

                                    if (foundMatch == 0)
                                    {
                                        _state = State.Standard;
                                        _display.SetHighlights();
                                        _waitingOnCombinationItems.Clear();
                                        _onCombinationFail.ActivateTrigger(this, null);
                                    }
                                }
                            }
                        }
                    }
                    break;
            }
       
        }

        private System.Collections.IEnumerator DoCombineItem(ItemRecipe recipe)
        {
            if (_display.ConfirmPrompt == null) goto Confirmed;

            var result = _display.ConfirmPrompt.Show();
            yield return result;
            yield return null; //wait one extra frame for effect

            if (result != null && result.SelectedConfirm)
                goto Confirmed;
            else
            {
                _onCombinationFail.ActivateTrigger(this, null);
                goto Cleanup;
            }

            Confirmed:
            if(recipe.ConsumeItems)
            {
                foreach (var item in _waitingOnCombinationItems)
                {
                    _display.InventoryPouch.Items.Remove(item);
                }
            }
            _waitingOnCombinationItems.Clear();

            if (recipe.Item != null) _display.InventoryPouch.Items.Add(recipe.Item);
            recipe.OnCreated.ActivateTrigger(recipe, null);
            _onCombinationSuccess.ActivateTrigger(this, null);

            yield return WaitForDuration.Seconds(0.1f, SPTime.Real);
            //_display.Hide();

            if(_display.IsShowing)
            {
                _display.SyncItemCollections();
                int newIndex = _display.GetItemIndex(recipe.Item);
                if (newIndex >= 0) _display.Spindle.SetFocusedIndex(newIndex, true);
                _display.UpdateCurrentlyEquippedHighlight(); //in-case what was combined was an euipped item
                this.UpdateDisplayText();
            }

            Cleanup:
            _state = State.Standard;
            if (_display.IsShowing) _display.SetHighlights();
            _waitingOnCombinationItems.Clear();
            _routine = null;
        }

Basically I have a state enum that tracks what state the inventory screen is in.

When someone selects the first item I determine what sort of item it is… if it’s a combinable item I go into a ‘WaitingOnCombination’ state. Then as more items are selected I test if any of the recipes on hand match the combination I currently have via the ‘matches’ method on the recipes. I keep doing this until they either cancel, or a recipe matches. At which point I remove the old items and add the new one.

Now this may not be exactly what you need… it is designed specifically for our game which has only very basic crafting. We don’t have things like using y items of x, or minecraft style crafting table/pattern stuff.

It’s a really simple “these items combine to make that item” system. Since our game is more a survival horror meets old SCUMM/Adventure games (like secret of monkey island).

1 Like

@lordofduct This is some cool stuff, i don’t understand much of what is happening in the code but seems like something i could use. Thanks for sharing!

How do you represent the crafted item if only 2 of 3 items are added? Will the resulting Item reflect this? Didnt see this in the ItemRecipe code, but I might missed something. Good approach, would do something similar myself.

edit: not often you see people use goto in C# :smile: Though I have been thinking of using them myself for coroutines, but haven’t needed it yet.

I assume you mean if like the recipe can be AB, or BC of ABC.

We currently don’t have a representation for that, as we don’t need it. If there is a corner case that we do we’d just create 2 recipes for each AB and BC that result in the same item.

I found them useful in coroutine logic flow. Since a coroutine usually represents a linear flow of time, getting to jump forward rather than into if statements reads more like the linear flow of time.

I also like C#'s goto despite people hating on goto as a whole. Jumps are common in lower level languages and as long as you know how you’re using it they’re not as evil as people make them out to be. It’s only when you’re in a language that lets you jump ANYWHERE in code and people just willy nilly jump around exploiting that, and trouble starts.

Thankfully, C# actually is pretty protective about ‘goto’. You can’t goto out of your current context. I can’t jump into another method or anything like that. The compiler puts safe limitations on it.

Of course, you still want to keep a goto readable.

Im confused as to what you gain over just having a “Cleanup()” and “Confirmed()” method that would adhere to c# standards? GOTO is really not a good mechanism and is widely avoided by most programmers, I really am struggling to get what you are gaining that you cannot do without GOTO?

I beg to differ about it not being a good mechanism.

A goto is a jump/branch. This mechanism is extremely common in code. Calling a function, yeah, that’s a branch/jump. If statement? Yeah, that’s a jump/branch. It’s just got different syntax in your high-level language, but at the end of the day… they’re jump statements!

It not being a good mechanism is a rule of thumb that comes down from abuse of GOTO. Any mechanism can be abused. Over the many years of people abusing things the internet by and large just dislikes goto… it’s not because of goto in and of itself, but because of the bad things people have seen done in it.

It’s very much related to BASIC and VB and the sort. The rise of hatred for goto corresponds with the rise of hatred for those languages. Namely because BASIC/VB use GOTO a lot in its code, it’s how you get a lot of things done in those languages.

Problem is those languages were overrun by novice/amateur programmers who didn’t know what they were doing.

Thus the code smell that arose out of it.

Thing is, you can abuse most mechanisms. I’ve seen OOP principles like inheritance/composition used poorly. I’ve seen design patterns like Singleton (oh that one has some real hate on the internet yet you find it in Unity a lot) get trashed. Bad programmers will do bad things with otherwise reasonable mechanisms.

Goto hatred is dogma rather than logic.

Now, you don’t have to use goto.

I do.

In very limitted settings.

What I gain from not just calling a function? Well… the overhead of that function for one. The disconnect from the linear logic of my coroutine for another. I described exactly why I use it in a coroutine… a coroutine is intended to represent a linear passage of time, and a goto allows me to write code that reads in a linear fashion like that.

I mean if we want to argue code smell here… the entire existence of a Coroutine is a massive code smell. Abusing the ‘iterator function’ pattern from C# to coerce it into this “coroutine” is heavily frowned upon outside of Unity.

But we do it, cause it gets the job done.

I have 12 years professional experience programming, 20+ years hobbyist experience. I don’t care what you tell me is right/wrong in the programming world. I’ve seen a lot of rights/wrongs come and go over the years. It’s all hub-bub and blustering.

If you know what it’s doing, and you do it correclty/safely… it’s not a problem in my book.

1 Like

Actually you are talking to an ex VB programmer. Also I started game development using darkBASIC which was written in BASIC so I am not unfamiliar with GOTO and have used it tons in the past, but I still wouldnt use it today in C# as I like my code to be maintainable by anyone, even if I died etc.

Anyway, I did not mean any offence, I was just interested in what you gain out of it and now you have answered. Certainly you dont need to explain how long you have programmed and blah blah blah, if your able to justify it your able to justify it and that would have been enough :smile:

And yes, you are right, I would also argue a coroutine is a bad mecanism too, as I would use async this day and age seeing as unity supports c# 4.0 onwards anyway now. Again, just interested to see what you get out of it. Was not saying you have to program my way!

My listing how long I’ve developed wasn’t a point to boast my experience. It was to point out that I’ve seen over the years the comings and goings of various bias. It was an added justification against the dogma against goto.

I already justified my use before you first responded. You just didn’t like my justification, and therefore asked me to justify it again. So I added more, since the one I originally gave clearly wasn’t enough for you.

I already said what I got out of it. Readable code.

If people can’t read goto’s, that’s on them. They’re fairly straight forward.

It’s not like my goto’s are wild spaghetti messes. They’re effectively the equivalent of a try/catch/finally ensure finally gets performed. (noting that try/catch/finally can not be performed in a coroutine since iterator functions don’t allow try/catch).

It too like readable/maintainable code. And I fail to see how my usage of goto lacks maintainability.

Okay, cool.

I asked a question and you replied, no big deal. Thanks for taking time to answer, but Im going to leave this now as your clearly taking this a bit personally, which was not my intention at all!

AndersMalmgren asked me, I explained, and then you asked me again is to what I’m referring. Meaning you either didn’t read what I had already said, or just didn’t consider it a valid justification.

This is why I said:

Blowing this out of proportion?

I’m just responding.

Fair enough, your answers read a little bit like arguing rather than answering, but maybe thats me.

Anyway, again as said before, thanks for answering. Nice crafting system and nice looking game. Good luck to you.

EDIT: OP you can have a look at this for a simple solution if your not too experienced with C# and Unity:
https://www.youtube.com/watch?v=2cjjk5PZCrU

Otherwise @lordofduct code will provide you with a more unique and probably more versatile crafting system. If I was going to choose one I would use what has been posted in here, but you mentioned you had trouble reading the code so I thought you might like a backup easier option :slight_smile:

1 Like

I can see a place for goto in coroutines. Though it could also be a sign your coroutine needs redesign.

Anyway Coroutines are mini workflows, and some workflows can be hard defining only with linear while loops and if statements. Sometimes you might need to jump from A to B. Though like I said, haven’t needed it yet.

Well yes, It’s an argument… not in the “yell and throw stuff” sense. But rather a debate argument.

You don’t agree with my usage, I do. You requested for me to explain (justify) my usage instead of using other C# mechanisms like a Cleanup method.

Thusly a debate ensued.

You may not have intended a debate. But it confused me that you’d ask me to answer a question that I already answered. That signaled as a ‘debate’. As if you had arguments for why you felt your stance was correct and wanted me to further explain mine.

1 Like

I agree with this.

I’ve only really ever needed it in minor cases. Like the one here. I wanted to keep my cleanup code at the end, and so I put it at the end. Instead of repeating it, or breaking it out into another function. A function that would only ever get called by this coroutine since it’s pertinent to only this coroutine.

Noting that the class has several other things going on. Of course that says I could probably factor this logic out of that class and into its own behaviour of some sort. But… I had a game to write and launch for halloween. A goto got the job done in the fewest lines of code and still was readable.

Hell the ‘confirmed’ portion of the code could be done if I just did a negative check in the if statement. That probably snuck in from code revisions. I bet that the first if statement at one point did something, but then it got removed, and just wasn’t refactored out.

Fair enough, probably the fact that everything is being read over text. Its hard to guage someones “tone” from text, and what you write as “not yelling and throwing stuff” I may read as “yelling and throwing stuff” haha :smile:

Anyway no harm done, everyones question was answered and OP has been given some decent resources.

The built in asynchronous tasks in C sharp doesn’t automatically make Coroutines a anti pattern. They aim to solve the same thing, execute asynchronous code in a synchronous manner.

Though the asynchronous framework is a bit neater becasue of the native compiler support, plus Task of T is really nice and not very easy to solve with Coroutines, you need to create custom yield instructions etc

I also needed a crafting system and dowloaded this free asset “Inventory Master”:

It has a item “database” to which you can add items.
It also has a recipe/blueprint “database” where you can create crafting recipes with the items.

Perhaps you can even take it like it is - when not you can learn a lot from the code.
I have my own system now but “Inventory Master” was a great help.