[JUST SKIP TILL THE END, PLEASE]
I had a problem under this comment, and to an extent, I solved the problem. I’m solving the next one now
Anyway, here’s the ‘PlantGrowthHandler.cs’ before I made any changes:
using System;
using System.Collections;
using System.Linq;
using RPG.Crafting;
using UnityEngine;
namespace RPG.Farming
{
public class PlantGrowthHandler : MonoBehaviour, ICraftingObserver
{
private ICraftingEventBus eventBus;
private float[] slicePoints;
public GameObject currentPlant;
void Awake()
{
eventBus = GetComponentInParent<ICraftingEventBus>();
eventBus.RegisterObserver(this);
}
void OnDestroy()
{
eventBus.DeregisterObserver(this);
}
public void OnCraftingStarted(Recipe recipe)
{
slicePoints = ComputeRandomGrowthSlices(recipe);
StartCoroutine(GrowthRoutine(recipe, recipe.GetCraftDuration()));
}
public void OnCraftingResumed(Recipe recipe, float remaining)
{
slicePoints = ComputeRandomGrowthSlices(recipe);
StartCoroutine(GrowthRoutine(recipe, remaining));
}
private float[] ComputeRandomGrowthSlices(Recipe recipe)
{
var stages = recipe.GetStages.Length - 1; // we don't want to wait for the final product (pickup) so we don't add a slice for it
var duration = recipe.GetCraftDuration();
var avg = duration / stages;
var deviations = new float[stages];
var slices = new float[stages];
// Seed the randomiser for consistent slices
var currentState = UnityEngine.Random.state; // cache the current state
UnityEngine.Random.InitState(transform.position.GetHashCode()); // seed based on the position
// Generate deviations within 30% of the average length
for (int i = 0; i < stages; i++)
{
deviations[i] = avg + (UnityEngine.Random.value * 2f - 1f) * avg * 0.3f;
}
// Restore the randomiser state
UnityEngine.Random.state = currentState; // restore to the cached state
// Normalize deviations to ensure the sum equals duration
var totalDeviation = deviations.Sum();
for (int i = 0; i < stages; i++)
{
slices[i] = deviations[i] * duration / totalDeviation;
}
// Adjust the slices to ensure the sum is exactly duration
const float deviation = 0.001f;
var sumSlices = slices.Sum();
var difference = duration - sumSlices;
// Distribute the difference across the slices
for (int i = 0; i < Mathf.Abs(difference / deviation); i++)
{
int index = i % stages;
if (difference > 0) slices[index] += deviation;
else slices[index] -= deviation;
}
// Accumulate the slices
var accumulator = slices[0];
for (var i = 1; i < slices.Length; i++)
{
accumulator += slices[i];
slices[i] = accumulator;
}
return slices;
}
private IEnumerator GrowthRoutine(Recipe recipe, float duration)
{
// Determine where to start the timer
var totalDuration = recipe.GetCraftDuration();
var startTime = totalDuration - duration;
// Determine the current stage, based on the elapsed time (if any)
var currentStage = DetermineStage(startTime);
var currentInstance = Instantiate(recipe.GetStages[currentStage], transform.position, Quaternion.identity);
for (var timer = startTime; timer / totalDuration <= 1f; timer += Time.unscaledDeltaTime /* <-- using 'unscaledDeltaTime' instead of 'deltaTime' because our notification, crafting and all systems work based on real time, so it has to match */)
{
var newStage = DetermineStage(timer);
if (currentStage != newStage)
{
// Update the current stage index
currentStage = newStage;
// Destroy the old instance
Destroy(currentInstance);
// Instantiate the new instance
currentInstance = Instantiate(recipe.GetStages[currentStage], transform.position, Quaternion.identity);
}
yield return null;
}
// Destroy the plant
Destroy(currentInstance);
// Spawn the pickup (pickup should be the last stage of the growth)
recipe.GetResult().Item.SpawnPickup(transform.position, 1); // Spawn 1 plant per spot
}
private int DetermineStage(float time)
{
var stage = Array.BinarySearch(slicePoints, time); // Blazingly-fast solution of determine the growth stage of the Farming Plant
if (stage < 0)
{
stage = ~stage;
}
return stage;
}
public GameObject GetCurrentPlant()
{
return currentPlant;
}
public void SetCurrentPlant(GameObject currentPlant)
{
this.currentPlant = currentPlant;
}
}
}
and here is my new ‘PlantGrowthHandler.GrowthRoutine()’ function, after making a few functions:
private IEnumerator GrowthRoutine(Recipe recipe, float duration)
{
// Determine where to start the timer
var totalDuration = recipe.GetCraftDuration();
var startTime = totalDuration - duration;
// Determine the current stage, based on the elapsed time (if any)
var currentStage = DetermineStage(startTime);
// var currentInstance = Instantiate(recipe.GetStages[currentStage], transform.position, Quaternion.identity);
GameObject currentInstance = null;
if (currentPlant != null)
{
currentInstance = Instantiate(recipe.GetStages[currentStage], transform.position, Quaternion.identity);
}
for (var timer = startTime; timer / totalDuration <= 1f; timer += Time.unscaledDeltaTime /* <-- using 'unscaledDeltaTime' instead of 'deltaTime' because our notification, crafting and all systems work based on real time, so it has to match */)
{
var newStage = DetermineStage(timer);
if (currentStage != newStage)
{
// Update the current stage index
currentStage = newStage;
// Destroy the old instance
Destroy(currentInstance);
// Instantiate the new instance
// currentInstance = Instantiate(recipe.GetStages[currentStage], transform.position, Quaternion.identity);
if (currentPlant != null)
{
currentInstance = Instantiate(recipe.GetStages[currentStage], transform.position, Quaternion.identity);
currentPlant = currentInstance;
}
}
yield return null;
}
// Destroy the plant
Destroy(currentInstance);
// Spawn the pickup (pickup should be the last stage of the growth)
// recipe.GetResult().Item.SpawnPickup(transform.position, 1); // Spawn 1 plant per spot
if (currentPlant != null)
{
recipe.GetResult().Item.SpawnPickup(transform.position, 1 /* <-- one output per slot */);
currentPlant = null;
}
}
Essentially, these changes make sure that we don’t plant a new plant in an occupied spot
Well… I do have a bit of a small problem, that will probably be troublesome to solve and solve a very minor detail that probably nobody truly cares about, but… let’s say I want to clear off the ‘currentPlant’ variable only after I picked it up, what’s the best way to do that? (I can see myself completely messing the pickup system if I try this one alone)
Again, it’s a nice little touch, but I can absolutely skip this one!
Anyway, I dealt with the Crafting Max Quantity handler in ‘CraftingTable.CraftRecipe()’, and a few details of the Crafting Quantity in ‘PlantGrowthHandler’, and then to get the initial max quantity, I dealt with it in ‘CraftingSystem.OnCraftingInteraction’
So now, we get modifications in the total value when we open the crafting table (CraftingSystem.OnCraftingInteraction()):
// I stronly recommend you read the interface part at the bottom of this comment first
// Crafting Quantity
if (craftingQuantity.gameObject.activeSelf)
{
var availableSlots = currentCraftingTable.GetGameObject().GetComponentsInChildren<PlantGrowthHandler>().Where(handler => handler.GetCurrentPlant() == null);
if (availableSlots.Count() > 1)
{
craftingQuantity.SetMaxQuantity(availableSlots.Count());
}
}
, when we craft something (CraftingTable.CraftRecipe()):
if (UseQuantityUI) // If you're using the Quantity UI (Farming only for this case)
{
var availableSlots = GetComponentsInChildren<PlantGrowthHandler>().Where(handler => handler.GetCurrentPlant() == null);
Debug.Log($"Total number of available slots is: {availableSlots.Count()}");
if (availableSlots.Count() > 1)
{
var firstAvailableSlot = availableSlots.First();
firstAvailableSlot.SetCurrentPlant(recipe.GetResult().Item.GetPickup().gameObject);
var newAvailableSlots = GetComponentsInChildren<PlantGrowthHandler>().Where(handler => handler.GetCurrentPlant() == null);
Debug.Log($"New number of available slots is: {newAvailableSlots.Count()}");
craftingQuantity.SetMaxQuantity(newAvailableSlots.Count()); // Decrement, based on how many slots were used
}
}
, and then the product is done (PlantGrowthHandler.GrowthRoutine(), in the end when we instantiate a pickup item):
// in 'OnCraftingStarted()' and 'OnCraftingResumed()', we get the 'craftingQuantity' gameObject:
craftingQuantity = FindObjectOfType<CraftingQuantity>(); // when you start crafting (I can't get it to link to 'NotifyCrafting' without resulting in a circular dependency, so this is my next best option)
// and in the end of 'GrowthRoutine()':
if (currentPlant != null)
{
recipe.GetResult().Item.SpawnPickup(transform.position, 1 /* <-- one output per slot */);
currentPlant = null;
craftingQuantity.SetMaxQuantity(craftingQuantity.GetMaxQuantity() + 1); // Increment the max quantity to indicate that this plant spot is free to use
}
I still need to test for saving though, before we can call this system done
I know it’s all over the place, but if it works it works I suppose. I haven’t played around with the system enough, but I think this setup has no circular dependencies. If you got any better ideas, please let me know
and here comes the huge problem @bixarrio - how do I get access to the crafting table’s children from ‘CraftingSystem.cs’? We had a similar problem yesterday, but we solved it in it’s own way. It’s not the same now, for this part:
If I can access the table, I can access it’s kids, and then I can get the right maximum value for that
Edit: I figured it out. @Brian_Trotter taught me a trick to be able to get the gameObject of an interface not too long ago (when we were turning our pickups from an animation-based system to an inventory-based system), where all you gotta do really is include it in your interface, and include it of course in the inheritors of the interface. So here’s what I did:
// get the UnityEngine library for the interface script (in this case, 'ICraftingTable.cs'):
using UnityEngine;
// in 'ICraftingTable':
GameObject GetGameObject();
// in 'CraftingTable.cs', the inheritor of the Interface:
public GameObject GetGameObject()
{
return this.gameObject;
}
// and now you can use it wherever you like :)
Anyway, all seems to be working fine now