Home > Back-end >  how to make Items spawn when you combine two other items together
how to make Items spawn when you combine two other items together

Time:08-04

i am making a 2D game and i want to make users combine two Herbs from the collection to make a certain potion i have no idea how to achieve that though i'm stuck in this step after i made the scene and the spawning script. Any idea how to spawn Potions after i throw for example 2 Herbs combination in the pot to mix them and get a result that is a certain potion that i assign to them?

Picture from the game i'm making

CodePudding user response:

Okay well, bear with me this is going to be a hefty one, I'll start by explaining a bit the thoughs that went into my solution, then you'll find my code as well as a demonstration of how it works.

I went for a "drag and drop" style, ingredients being represented as gameobject that can be dragged up to the cauldron.

When an ingredient touches the cauldron, it is added to it. To keep thinkgs simple I did this with colliders.

As for the making of the potion, the logic is separated into two classes :

  • The PotionRecipe class will hold the combinaison of ingredients needed to make a potion as well as the Prefab that should be spawned when this recipe is crafted.
  • The PotionCrafter class is in charge of making a potion out of the ingredients in the cauldron

Recipes can need different amount of the same ingredient, ingredients are lost if we try to make a potion when the ingredients in the cauldron don't match any recipe

For the sake of complexity and time, I made a static "repository" of handcrafted potion recipes that cannot be edited via the inspector, this solution is not viable for a big project but it help illustrate how does my solution work.

Enough with all the talks, here's some code

(To make the code easier to use, I removed all of my namespaces and put everything except for the MonoBehaviour in a big blob)

The Potion Brewing logic :

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

public enum Ingredient
{
    Garlic,
    Aloe,
    Lichen
}

public class PotionRecipe
{
    public string Name { get; }
    public uint NumberOfIngredients { get; }

    // A dictionary containing the quantity of
    // each ingredient needed to make this potion
    private Dictionary<Ingredient, uint> Recipe { get; }

    // The Potion GameObject can contain the prefab
    // corresponding to this potion and its components
    private readonly GameObject _potionPrefab;

    public PotionRecipe(string name, Dictionary<Ingredient, uint> recipe, GameObject potionPrefab = null)
    {
        Name = name;
        Recipe = recipe;
        NumberOfIngredients = (uint)Recipe.Values.Sum(u => u);
        _potionPrefab = potionPrefab;
    }

    // Check if the recipe is made with the given ingredient and if the amount of it is correct
    public bool IsMadeWith(Ingredient ingredient, uint quantity)
    {
        if (!Recipe.ContainsKey(ingredient)) return false;

        return Recipe[ingredient] == quantity;
    }

    public GameObject CreatePotion()
    {
        // Instantiate the potion prefab or create a new empty object for demonstration
        return _potionPrefab ? Object.Instantiate(_potionPrefab) : new GameObject(Name);
    }
}

public class PotionBrewer
{
    private readonly HashSet<PotionRecipe> _potionRecipes;

    public PotionBrewer()
    {
        // Get the list of recipes from the repository
        _potionRecipes = PotionRecipeRepository.Recipes;
    }

    public GameObject MakePotion(Queue<KeyValuePair<Ingredient, uint>> ingredients, uint numberOfIngredients)
    {
        if (ingredients.Count == 0) return null;

        // Only test recipes that have the same number of ingredients in them
        foreach (var recipe in _potionRecipes.Where(recipe => recipe.NumberOfIngredients == numberOfIngredients))
        {
            // Make a copy of the ingredient queue for each loop
            var ingredientsCopy = ingredients;

            Ingredient ingredient;
            uint quantity;

            // Iterate over the queue as long as the ingredients are matching
            do
            {
                // If the ingredient Queue is empty, we matched all the ingredients
                if (ingredientsCopy.Count == 0)
                {
                    // Return the potion associated with this recipe
                    return recipe.CreatePotion();
                }

                (ingredient, quantity) = ingredientsCopy.Dequeue();

            } while (recipe.IsMadeWith(ingredient, quantity));

        }

        // Otherwise we failed to make a potion out of this recipe
        return null;
    }
}

// This is a static repository made for this example
// It would be probably best to replace is by something configurable in the editor
static class PotionRecipeRepository
{
    public static HashSet<PotionRecipe> Recipes { get; } = new();

    static PotionRecipeRepository()
    {
        var healingPotion = new Dictionary<Ingredient, uint>()
        {
            [Ingredient.Garlic] = 2,
            [Ingredient.Aloe] = 1
        };

        Recipes.Add(new PotionRecipe("Healing Potion", healingPotion));

        var sicknessPotion = new Dictionary<Ingredient, uint>()
        {
            [Ingredient.Lichen] = 1,
            [Ingredient.Garlic] = 1
        };

        Recipes.Add(new PotionRecipe("Sickness Potion", sicknessPotion));
    }
}

The Cauldron.cs component :

To be attached to the cauldron GameObject, it uses a SphereCollider

public interface IBrewingCauldron
{
    public void AddIngredient(Ingredient ingredient);
    public GameObject BrewPotion();
}


[RequireComponent(typeof(SphereCollider))]
[RequireComponent(typeof(Rigidbody))]
public class Cauldron : MonoBehaviour, IBrewingCauldron
{
    public Dictionary<Ingredient, uint> Ingredients { get; private set; } = new();

    [SerializeField] private SphereCollider cauldronCollider;
    private readonly PotionBrewer _potionBrewer = new();
    private uint _numberOfIngredients;

    private void Awake()
    {
        cauldronCollider ??= GetComponent<SphereCollider>();
        // Set the collider as trigger to interact with ingredients GameObject
        cauldronCollider.isTrigger = true;
    }


    public void AddIngredient(Ingredient ingredient)
    {
        // Keep track of the number of ingredients added
        _numberOfIngredients  ;

        if (!Ingredients.ContainsKey(ingredient))
        {
            Ingredients[ingredient] = 1;
        }
        else
        {
            Ingredients[ingredient]   ;
        }
    }

    public GameObject BrewPotion()
    {
        var ingredientQueue = new Queue<KeyValuePair<Ingredient, uint>>(Ingredients);

        var potionObject = _potionBrewer.MakePotion(ingredientQueue, _numberOfIngredients);

        if (potionObject is not null)
        {
            Debug.Log($"We made a {potionObject.name} !");
            potionObject.transform.position = transform.position;
        }
        else
        {
            Debug.Log("We failed to make any potion !!!");
        }

        Ingredients = new Dictionary<Ingredient, uint>();
        _numberOfIngredients = 0;

        return potionObject;
    }
}

The PotionIngredient.cs component

To be attached to every ingredient GameObject, they need the Cauldron's GameObject to function, if they don't have it or if the GameObject doesn't contains the Cauldron's script, they will disable themselves.

public class PotionIngredient: MonoBehaviour
{
    [SerializeField] private GameObject cauldronGameObject;

    [SerializeField] private Ingredient ingredient;

    private SphereCollider _cauldronCollider;

    private IBrewingCauldron _cauldron;

    private void Awake()
    {
        if (cauldronGameObject is not null)
        {
            _cauldron = cauldronGameObject.GetComponent<IBrewingCauldron>();

            if (_cauldron is not null) return;
        }

        var ingredientObject = gameObject;
        ingredientObject.name  = " [IN ERROR]";
        ingredientObject.SetActive(false);

        throw new MissingComponentException($"{ingredientObject.name} is missing the cauldron gameobject");
    }

    private void Start()
    {
        _cauldronCollider = cauldronGameObject.GetComponent<SphereCollider>();

        gameObject.name = ingredient.ToString();
    }

    private void OnTriggerEnter(Collider other)
    {
        if (other != _cauldronCollider) return;

        _cauldron.AddIngredient(ingredient);
        Destroy(gameObject);
    }
}

Finally, a small custom editor I made to test my code :

[CustomEditor(typeof(Cauldron))]
public class CauldronEditor : UnityEditor.Editor
{
    private Cauldron _cauldron;

    private void OnEnable()
    {
        _cauldron = (Cauldron) target;
    }

    public override void OnInspectorGUI()
    {
        base.OnInspectorGUI();


        EditorGUILayout.Space();
        var ingredients = _cauldron.Ingredients;

        if (ingredients.Any())
        {
            GUILayout.Label("Ingredients :");

            EditorGUILayout.Space();

            foreach (var (ingredient, quantity) in ingredients)
            {
                GUILayout.Label($"{ingredient} : {quantity}");
            }
        }

        EditorGUILayout.Space();
        if (GUILayout.Button("BrewPotion"))
        {
            _cauldron.BrewPotion();
        }
    }
}

Here's a small gif illustrating how my solution is working inside the editor.

Demonstration of the solution

There's quite a bit of code so if you find yourself having troubling using it, I can also share a github link of a working project.

Hope this helps.

  • Related