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?
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.
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.