A bit of guidance needed for some custom Farming code

I’ll send over the code as soon as I’m home. I’ll be honest as well, I tried understanding how the event bus works a few times based on what @bixarrio supplied me with, to debug this problem, but I failed to understand what’s going on multiple times. To me, it looks like something that magically works (and I’m not a fan of magic. I believe everything has a reason)

Again, I’ll send you over a copy of all the required scripts as soon as I’m home :grinning_face_with_smiling_eyes:

OK so this might be a little lengthy, but here we go:

  1. Here is the ‘PlantGrowthHandler.cs’ script. This is responsible for handling how plants grow, apart from the delay that happens when you return (which I want to fix):
using System;
using System.Collections;
using System.Linq;
using GameDevTV.Saving;
using Newtonsoft.Json.Linq;
using RPG.Crafting;
using UnityEngine;

namespace RPG.Farming
{
    public class PlantGrowthHandler : MonoBehaviour, ICraftingObserver, IUniqueIdentifierInterface, IJsonSaveable
    {
        [field: SerializeField] public string UniqueIdentifier { get; private set; }

        private ICraftingEventBus eventBus;
        private float[] slicePoints;

        public GameObject currentPlant;

        // TEST
        private Recipe currentRecipe;
        private GameObject currentInstance;

        void Awake()
        {
            eventBus = GetComponentInParent<ICraftingEventBus>();
            eventBus.RegisterObserver(this);
            CraftingCompleteEstimationEvent.OnCraftingCompleteEstimation += InstantiateFinalProduct;
        }

        void OnDestroy()
        {
            eventBus.DeregisterObserver(this);
            CraftingCompleteEstimationEvent.OnCraftingCompleteEstimation -= InstantiateFinalProduct;
        }

        public void OnCraftingStarted(Recipe recipe)
        {
            slicePoints = ComputeRandomGrowthSlices(recipe);
            StartCoroutine(GrowthRoutine(recipe, recipe.GetCraftDuration()));
            SetCurrentRecipe(recipe);
        }

        public void OnCraftingResumed(Recipe recipe, float remaining)
        {
            slicePoints = ComputeRandomGrowthSlices(recipe); // Makes sure our Plants don't just grow at the exact same time simultaneously, with a little bit of Randomness
            StartCoroutine(GrowthRoutine(recipe, remaining));
            SetCurrentRecipe(recipe);
        }

        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 remainingDuration)
        {
            // Determine where to start the timer
            var totalDuration = recipe.GetCraftDuration();
            var startTime = totalDuration - remainingDuration;

            Debug.Log($"Remaining duration is: {remainingDuration}");

            // Determine the current stage, based on the elapsed time (if any)
            var currentStage = DetermineStage(startTime);
            
            // GameObject currentInstance = null;

            if (currentPlant != null)
            {
                // Instantiate the first phase, only for plants that are currently being grown (empty slots get nothing)
                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);

                    if (currentPlant != null)
                    {
                        // Instantiate next phase, only for plants that are currently being grown (empty slots get nothing)
                        currentInstance = Instantiate(recipe.GetStages[currentStage], transform.position, Quaternion.identity);
                    }
                }

                yield return null;
            }

            // Destroying the current instance and instantiating the final product are handled in
            // 'InstantiateFinalProduct' below, which is called through events in 'CraftingNotification.CraftingCompleteEstimation()'.
            // That way we do not get any early products before the crafting is complete,
            // when we restore the game, as it matches exactly when the notification is called
        }

        public void InstantiateFinalProduct()
        {
            Destroy(GetCurrentInstance());

            if (currentPlant != null)
            {
                // Instantiate the final phase, only for plants that are being grown (empty slots get nothing)
                GetCurrentRecipe().GetResult().Item.SpawnPickup(transform.position, 1 /* <-- one output per farming slot */);
                currentPlant = null;
            }
        }

        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;
        }

        public JToken CaptureAsJToken()
        {
            Debug.Log($"Capturing from PlantGrowthHandler {UniqueIdentifier}");
            return UniqueIdentifier;
        }

        public void RestoreFromJToken(JToken state)
        {
            Debug.Log($"Restoring from PlantGrowthHandler {state.ToString()}");
        }

        public GameObject GetCurrentInstance() 
        {
            return currentInstance;
        }

        public void SetCurrentInstance(GameObject currentInstance) 
        {
            this.currentInstance = currentInstance;
        }

        public Recipe GetCurrentRecipe() 
        {
            return currentRecipe;
        }

        public void SetCurrentRecipe(Recipe currentRecipe) 
        {
            this.currentRecipe = currentRecipe;
        }
    }
}

For the time being, the recipe and the current instances have been moved to public, as I try figure out how to fix this issue

  1. Here is ‘CraftingNotification.cs’, which has ‘GetNumberOfOccupiedSlots()’:
using System.Collections;
using RPG.Crafting;
using RPG.Farming;
using RPG.Skills;
using UnityEngine;

namespace RPG.CraftingNotifications
{
// This class is responsible for firing a Notification to the player when crafting is done, and he's not around
// (it's been refined to avoid a 3-way couple of 'CraftingNotification.cs', 'CraftingTable.cs' and 'PlantGrowthHandler.cs')
public class CraftingNotification : MonoBehaviour, ICraftingObserver
{
    private ICraftingEventBus eventBus;
    private CurrentQuantityHolder currentQuantityHolder;

    private int numberOfOccupiedSlots;

    void Awake() 
    {
        eventBus = GetComponentInParent<ICraftingEventBus>();
        eventBus.RegisterObserver(this);
    }

    void OnEnable() 
    {
        currentQuantityHolder = FindObjectOfType<CurrentQuantityHolder>(true); // Find the 'currentQuantityHolder' when you spawn the farming patch (hence 'OnEnable()'), so we can use its value to get the correct xp
    }

    void OnDestroy() 
    {
        eventBus.DeregisterObserver(this);
    }

    public void OnCraftingStarted(Recipe recipe)
    {
        StartCoroutine(CraftingCompleteEstimation(recipe, recipe.GetCraftDuration()));
        numberOfOccupiedSlots = GetNumberOfOccupiedSlots();
    }

    public void OnCraftingResumed(Recipe recipe, float remainingTime)
    {
        StopAllCoroutines();
        StartCoroutine(CraftingCompleteEstimation(recipe, remainingTime));
        numberOfOccupiedSlots = GetNumberOfOccupiedSlots();
    }

    private IEnumerator CraftingCompleteEstimation(Recipe recipe, float delay) 
    {
        yield return new WaitForSecondsRealtime(delay);

        // FARMING ONLY, as farming is the only skill that will have 'recipe.GetStages' not be equal to zero
        // (Other Crafting-UI Based skills will be covered in 'CraftingSystem.cs')
        if (recipe.GetStages != null && recipe.GetStages.Length != 0)
        {
            GameObject.FindWithTag("Player").GetComponent<SkillExperience>().GainExperience(recipe.GetRequiredSkill(), recipe.GetXPReward() * numberOfOccupiedSlots);

            // Find a way to identify only the farming patch/crafting table this caller has, instead of everyone involved
            CraftingCompleteEstimationEvent.InvokeOnCraftingCompleteEstimation(); // Called from 'PlantGrowthHandler.InstantiateFinalProduct' event subscription, so we can instantiate the final product when the notification is invoked
        }

        MalbersAnimations.InventorySystem.NotificationManager.Instance.OpenNotification($"Crafting Complete:\n{recipe.name}");
    }

    private int GetNumberOfOccupiedSlots()
    {
        // Iterate through all Plant Growth Handler kids of this script, and
        // any occupied ones with plants will be added, so we can properly multiply
        // the experience

        int totalOccupiedSlots = 0;

        foreach (var slot in GetComponentsInChildren<PlantGrowthHandler>())
        {
            if (slot.GetCurrentPlant())
            {
                totalOccupiedSlots++;
            }
        }
        return totalOccupiedSlots;
    }
}

}

‘GetNumberOfOccupiedSlots()’ checks how many slots have a ‘currentPlant’ in ‘PlantGrowthHandler.cs’, and counts them as an ‘occupied slot’

  1. I reversed the changes you suggested last night just to be safe until the morning, but here is the current ‘OnCraftingCompleteEstimation’ event:
using System;

public static class CraftingCompleteEstimationEvent
{
    public static event Action OnCraftingCompleteEstimation;

    public static void InvokeOnCraftingCompleteEstimation() 
    {
        OnCraftingCompleteEstimation?.Invoke();
    }
}
  1. Here is the ‘CraftingEventBus.cs’, which has something to do with ‘CraftingStarted’ and ‘CraftingResumed’:
using System.Collections;
using System.Collections.Generic;
using RPG.Crafting;
using UnityEngine;

namespace RPG.CraftingNotifications 
{
    // An 'Event Bus' is a central bus that allows communication between two scripts,
    // without the need of them coupling each other

public class CraftingEventBus : MonoBehaviour, ICraftingEventBus
{
    private List<ICraftingObserver> observers = new List<ICraftingObserver>();

    public void CraftingStarted(Recipe recipe) 
    {
        foreach (var observer in observers) 
        {
            observer.OnCraftingStarted(recipe);
        }
    }

    public void CraftingResumed(Recipe recipe, float remainingTime) 
    {
        foreach (var observer in observers) 
        {
            observer.OnCraftingResumed(recipe, remainingTime);
        }
    }

    public void RegisterObserver(ICraftingObserver observer) 
    {
        if (observers.Contains(observer)) return;
        observers.Add(observer);
    }

    public void DeregisterObserver(ICraftingObserver observer) 
    {
        observers.Remove(observer);
    }
}

}

and 5. Here is my crafting saving system, in case this is needed. The main problem I have is the restoring there has some inaccuracies with the timing, so I want the plant to be created when the notification is out, as the notification does not seem to be having this kind of problem:

        JToken IJsonSaveable.CaptureAsJToken()
        {
            // Save the state, start time, recipe, and output (if any)
            var saveData = new CraftingTableSaveData
            {
                CraftingState = CurrentState,
                CraftingStartTime = craftingStartTime,
                CraftingFailed = craftingFailed,
                FailedAtPercentage = failedAtPercentage
            };
            if (CurrentRecipe != null)
            {
                saveData.RecipeID = CurrentRecipe.GetRecipeID();
            }
            if (CraftedOutput != null && CraftedOutput.Item != null)
            {
                saveData.OutputItemID = CraftedOutput.Item.GetItemID();
                saveData.OutputAmount = CraftedOutput.Amount;
            }

            // GET THE CURRENT QUANTITY OF PLANTS THAT ARE BEING PLANTED, SO WE CAN RESTORE THEM WHEN WE RETURN TO THE GAME:
            var currentQuantity = GetComponentsInChildren<PlantGrowthHandler>().Count(handler => handler.GetCurrentPlant() != null);
            saveData.CurrentQuantity = currentQuantity;

            return JToken.FromObject(saveData);
        }

        void IJsonSaveable.RestoreFromJToken(JToken state)
        {
            // Restore the state, start time, recipe and output (if any)
            var saveData = state.ToObject<CraftingTableSaveData>();

            craftingStartTime = 0;
            CurrentRecipe = default;
            CurrentState = saveData.CraftingState;
            currentAction = IdleAction;
            craftingFailed = saveData.CraftingFailed;
            failedAtPercentage = saveData.FailedAtPercentage;

            // If we saved an output item, restore it
            if (!string.IsNullOrWhiteSpace(saveData.OutputItemID) && saveData.OutputAmount > 0)
            {
                CraftedOutput = new CraftedItemSlot(InventoryItem.GetFromID(saveData.OutputItemID), saveData.OutputAmount);
            }

            // If we are idle, we are done here
            if (CurrentState == CraftingState.Idle)
            {
                return;
            }

            // If we are here, we were crafting something the last time we got saved
            // Update the table state to continue crafting
            craftingStartTime = saveData.CraftingStartTime;

            CurrentRecipe = Recipe.GetFromID(saveData.RecipeID);

            // Plant the plants that are supposed to be there, based on where they are in time:
            if (CurrentRecipe != null && saveData.CurrentQuantity > 0)
            {
                for(int i = 0; i < saveData.CurrentQuantity; i++)
                {
                    var availableSlots = GetComponentsInChildren<PlantGrowthHandler>().Where(handler => handler.GetCurrentPlant() == null);
                    var firstAvailableSlot = availableSlots.First();
                    firstAvailableSlot.SetCurrentPlant(CurrentRecipe.GetResult().Item.GetPickup().gameObject);
                }
            }

            currentAction = CraftingAction;

            // Notification Trigger, in case the player was gone when crafting was pending, and now he's back:
            var elapsedTime = (float) timeKeeper.GetGlobalTime() - (float) craftingStartTime;
            // CraftingResumed?.Invoke(CurrentRecipe.GetCraftDuration() - elapsedTime); // Replaced by a more generic 'Crafting Notification' System below
            eventBus.CraftingResumed(CurrentRecipe, CurrentRecipe.GetCraftDuration() - elapsedTime); // Allows proper notification timing when we return to the game
        }

        [Serializable]
        struct CraftingTableSaveData
        {
            public CraftingState CraftingState;
            public double CraftingStartTime;
            public string RecipeID;
            public string OutputItemID;
            public int OutputAmount;
            public bool CraftingFailed;
            public float FailedAtPercentage;
            public int CurrentQuantity;
        }

and 6. This one is crucial for all these systems to work together (except that it doesn’t have the ‘GameObject currentInstance’, I just added that to try and compensate for what I’m trying to do):

using RPG.Crafting;
using UnityEngine;

public interface ICraftingObserver 
{
    void OnCraftingStarted(/* GameObject currentInstance, */ Recipe recipe);
    void OnCraftingResumed(/* GameObject currentInstance, */ Recipe recipe, float remainingTime);
}

and 7, the Crafting Event Bus original interface:

using RPG.Crafting;

public interface ICraftingEventBus 
{
    void CraftingStarted(Recipe recipe);
    void CraftingResumed(Recipe recipe, float remainingTime);

    void RegisterObserver(ICraftingObserver observer);
    void DeregisterObserver(ICraftingObserver observer);
}

If you need anything else, please let me know

Just a heads-up, the way my farming works, is we got the ‘CraftingTable.cs’ (which does the farming) on the top of the hierarchy, and each farming slot available on the farming patch has a ‘PlantGrowthHandler.cs’ script on it, which handles each plant individually

I’m slowly thinking of making a small change and making the slots themselves as crafting tables, so I can plant each plant individually. I admit, I should’ve thought of this earlier. This was a big mistake from my side!

That ‘PlantGrowthHandler.cs’ script has a ‘currentPlant’ variable that gets occupied when farming is occuring somewhere. Once it’s empty, the pickup spawns and that slot will be available for use again

And the more I try to dive into this on my own, the more dangerous it gets. This is territory I’d rather not be doing on my own without a little bit of guidance to be honest, and I’ve learned the hard way to never move on to new ideas until your previous ones are polished

When you hit the craft button, you assign to the slot what you need to be there. This is done in the ‘CraftingTable.CraftRecipe()’ function:

                for (int i = 0; i < craftingQuantity.GetCurrentQuantity(); i++)
                {
                    // Plant quantity based on what the 'GetCurrentQuantity()' has available, 
                    // which is controlled in 'CraftingSystem.OnCraftingInteraction()', and the 
                    // value is also updated below, after you hit the 'Craft' button on the UI
                    var availableSlots = GetComponentsInChildren<PlantGrowthHandler>().Where(handler => handler.GetCurrentPlant() == null);
                    var firstAvailableSlot = availableSlots.First();
                    firstAvailableSlot.SetCurrentPlant(recipe.GetResult().Item.GetPickup().gameObject); // Just a way to indicate that this slot is occupied
                }

In the meanwhile I shall go and try watch a few videos from your “Unity Programming Designer Patterns” course. I purchased that a while ago for cases like this, where I actually need to learn something new because it’s getting quite hard with what I know so far

After quite a bit of reading and some head scratching, I realize that currentInstance is unique to each PlantGrowthHandler, and changes as the plant grows… so it’s not really relevant to the event bus…

Since your PlantGrowthHandler has a UniqueIdentifier, that’s really all you would need to pass along the chain to your CraftingCompleteEstimationEvent.

1 Like

Please give me an hour or so, I’ll be home soon. Kinda enjoying the bacon here :woozy_face:

OK so if we are passing the unique identifier down the chain, I’ll need @bixarrio 's help for how to assign that in ‘CraftingTable.cs’, because… well… the restoring system, and the ‘CraftRecipe’ function in ‘CratingTable.cs’ needs to know that value too

@bixarrio please help me out with this last part :slight_smile:

Aside from that,this is one insanely lengthy chain to modify… I’m not even sure if what I’m doing is right or wrong, but I’ll post it here anyway:

  1. in ‘CraftingCompleteEstimationEvent.cs’:
using System;

public static class CraftingCompleteEstimationEvent
{
    public static event Action<string[]> OnCraftingCompleteEstimation;

    public static void InvokeOnCraftingCompleteEstimation(string[] plantGrowthUniqueIDs) 
    {
        OnCraftingCompleteEstimation?.Invoke(plantGrowthUniqueIDs);
    }
}
  1. in ‘CraftingTable.cs’:
// in 'RestoreFromJToken()':

            // TEST - DELETE IF FAILED
            string[] uniqueIdentifiers = new string[saveData.CurrentQuantity];

            // Plant the plants that are supposed to be there, based on where they are in time:
            if (CurrentRecipe != null && saveData.CurrentQuantity > 0)
            {
                for(int i = 0; i < saveData.CurrentQuantity; i++)
                {
                    var availableSlots = GetComponentsInChildren<PlantGrowthHandler>().Where(handler => handler.GetCurrentPlant() == null);
                    var firstAvailableSlot = availableSlots.First();
                    firstAvailableSlot.SetCurrentPlant(CurrentRecipe.GetResult().Item.GetPickup().gameObject);

                    // TEST - DELETE IF FAILED (FOR IDENTIFYING PLANT GROWTH HANDLERS)
                    uniqueIdentifiers[i] = GetComponentsInChildren<PlantGrowthHandler>()[i].UniqueIdentifier;
                }
            }

// in 'CraftRecipe()':

            // TEST - DELETE IF FAILED (FOR IDENTIFYING PLANT GROWTH HANDLERS)
            string[] uniqueIdentifiers = new string[craftingQuantity.GetCurrentQuantity()];

            if (UseQuantityUI) // If you're using the Quantity UI (The code below is dedicated for Farming only for this case)
            {
                for (int i = 0; i < craftingQuantity.GetCurrentQuantity(); i++)
                {
                    // Plant quantity based on what the 'GetCurrentQuantity()' has available, 
                    // which is controlled in 'CraftingSystem.OnCraftingInteraction()', and the 
                    // value is also updated below, after you hit the 'Craft' button on the UI
                    var availableSlots = GetComponentsInChildren<PlantGrowthHandler>().Where(handler => handler.GetCurrentPlant() == null);
                    var firstAvailableSlot = availableSlots.First();
                    firstAvailableSlot.SetCurrentPlant(recipe.GetResult().Item.GetPickup().gameObject); // Just a way to indicate that this slot is occupied

                    // TEST - DELETE IF FAILED (FOR IDENTIFYING PLANT GROWTH HANDLERS)
                    uniqueIdentifiers[i] = GetComponentsInChildren<PlantGrowthHandler>()[i].UniqueIdentifier;
                }
            }

  1. in ‘ICraftingEventBus.cs’:
// there's two new 'string[] uniqueIdentifier' parameters in here:

using RPG.Crafting;

public interface ICraftingEventBus 
{
    void CraftingStarted(Recipe recipe, string[] uniqueIdentifier);
    void CraftingResumed(Recipe recipe, float remainingTime, string[] uniqueIdentifier);

    void RegisterObserver(ICraftingObserver observer);
    void DeregisterObserver(ICraftingObserver observer);
}
  1. in ‘CraftingEventBus.cs’:
// again, new 'string[] uniqueIdentifiers' parameters here:

    public void CraftingStarted(Recipe recipe, string[] uniqueIdentifier) 
    {
        foreach (var observer in observers) 
        {
            observer.OnCraftingStarted(recipe, uniqueIdentifier);
        }
    }

    public void CraftingResumed(Recipe recipe, float remainingTime, string[] uniqueIdentifier) 
    {
        foreach (var observer in observers) 
        {
            observer.OnCraftingResumed(recipe, remainingTime, uniqueIdentifier);
        }
    }

  1. in ‘ICraftingObserver.cs’:
// you get the idea by now... new 'string[] uniqueIdentifier' parameters:

using RPG.Crafting;
using UnityEngine;

public interface ICraftingObserver 
{
    void OnCraftingStarted(Recipe recipe, string[] uniqueIdentifier);
    void OnCraftingResumed(Recipe recipe, float remainingTime, string[] uniqueIdentifier);
}
  1. in ‘CraftingNotification.cs’:
// same drill... 

    public void OnCraftingStarted(Recipe recipe, string[] uniqueIdentifier)
    {
        StartCoroutine(CraftingCompleteEstimation(recipe, recipe.GetCraftDuration(), uniqueIdentifier));
        numberOfOccupiedSlots = GetNumberOfOccupiedSlots();
    }

    public void OnCraftingResumed(Recipe recipe, float remainingTime, string[] uniqueIdentifier)
    {
        StopAllCoroutines();
        StartCoroutine(CraftingCompleteEstimation(recipe, remainingTime, uniqueIdentifier));
        numberOfOccupiedSlots = GetNumberOfOccupiedSlots();
    }

    private IEnumerator CraftingCompleteEstimation(Recipe recipe, float delay, string[] uniqueIdentifier)
    {
        yield return new WaitForSecondsRealtime(delay);

        // FARMING ONLY, as farming is the only skill that will have 'recipe.GetStages' not be equal to zero
        // (Other Crafting-UI Based skills will be covered in 'CraftingSystem.cs')
        if (recipe.GetStages != null && recipe.GetStages.Length != 0)
        {
            GameObject.FindWithTag("Player").GetComponent<SkillExperience>().GainExperience(recipe.GetRequiredSkill(), recipe.GetXPReward() * numberOfOccupiedSlots);
            // TEST - DELETE IF FAILED
            CraftingCompleteEstimationEvent.InvokeOnCraftingCompleteEstimation(uniqueIdentifier);
        }

        MalbersAnimations.InventorySystem.NotificationManager.Instance.OpenNotification($"Crafting Complete:\n{recipe.name}");
    }

AND 7. In ‘PlantGrowthHandler.cs’:

        // TEST - DELETE IF FAILED
        private GameObject currentInstance;
        private Recipe currentRecipe;

        // TEST FUNCTION - DELETE IF FAILED
        public GameObject GetCurrentInstance() 
        {
            return currentInstance;
        }

        // TEST FUNCTION - DELETE IF FAILED
        public void SetCurrentInstance(GameObject currentInstance) 
        {
            this.currentInstance = currentInstance;
        } 

        // TEST FUNCTION - DELETE IF FAILED
        public Recipe GetCurrentRecipe() 
        {
            return currentRecipe;
        }

        // TEST FUNCTION - DELETE IF FAILED
        public void SetCurrentRecipe(Recipe currentRecipe) 
        {
            this.currentRecipe = currentRecipe;
        }

        void Awake()
        {
            eventBus = GetComponentInParent<ICraftingEventBus>();
            eventBus.RegisterObserver(this);

            // TEST - DELETE IF FAILED
            CraftingCompleteEstimationEvent.OnCraftingCompleteEstimation += InstantiateFinalProduct;
        }

        void OnDestroy()
        {
            eventBus.DeregisterObserver(this);

            // TEST - DELETE IF FAILED
            CraftingCompleteEstimationEvent.OnCraftingCompleteEstimation -= InstantiateFinalProduct;
        }

        public void OnCraftingStarted(Recipe recipe, string[] uniqueIdentifier /* <-- TEST - DELETE IF FAILED */)
        {
            slicePoints = ComputeRandomGrowthSlices(recipe);
            StartCoroutine(GrowthRoutine(recipe, recipe.GetCraftDuration()));
            // TEST - DELETE IF FAILED
            SetCurrentRecipe(recipe);
        }

        public void OnCraftingResumed(Recipe recipe, float remainingDuration, string[] uniqueIdentifier  /* <-- TEST - DELETE IF FAILED */)
        {
            StopAllCoroutines();
            slicePoints = ComputeRandomGrowthSlices(recipe); // Makes sure our Plants don't just grow at the exact same time simultaneously, with a little bit of Randomness
            StartCoroutine(GrowthRoutine(recipe, remainingDuration));
            // TEST - DELETE IF FAILED
            SetCurrentRecipe(recipe);
        }

        private IEnumerator GrowthRoutine(Recipe recipe, float remainingDuration)
        {
            // Determine where to start the timer
            var totalDuration = recipe.GetCraftDuration();
            var startTime = totalDuration - remainingDuration;

            Debug.Log($"Remaining duration is: {remainingDuration}");

            // Determine the current stage, based on the elapsed time (if any)
            var currentStage = DetermineStage(startTime);
            
            // GameObject currentInstance = null; // TEST REPLACED THIS (MAKING IT GLOBAL)

            if (currentPlant != null)
            {
                // Instantiate the first phase, only for plants that are currently being grown (empty slots get nothing)
                currentInstance = Instantiate(recipe.GetStages[currentStage], transform.position, Quaternion.identity);
                // TEST - DELETE IF FAILED
                SetCurrentInstance(currentInstance);
            }

            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);

                    if (currentPlant != null)
                    {
                        // Instantiate next phase, only for plants that are currently being grown (empty slots get nothing)
                        currentInstance = Instantiate(recipe.GetStages[currentStage], transform.position, Quaternion.identity);
                        // TEST - DELETE IF FAILED
                        SetCurrentInstance(currentInstance);
                    }
                }

                yield return null;
            }

            /* Destroy(currentInstance); // TEMPORARY TEST IN 'INSTANTIATEFINALPRODUCT' REPLACED THIS - RESTORE THIS BLOCK IF FAILED

            if (currentPlant != null)
            {
                // Instantiate the final phase, only for plants that are being grown (empty slots get nothing)
                recipe.GetResult().Item.SpawnPickup(transform.position, 1 /* <-- one output per farming slot * /);
                currentPlant = null;
            } */
        }

        // TEST FUNCTION - DELETE IF FAILED
        private void InstantiateFinalProduct(string[] uniqueIdentifiers) 
        {
            if (uniqueIdentifiers.Contains(UniqueIdentifier)) 
            {
                Destroy(currentInstance);

                if (currentPlant != null) 
                {
                    Destroy(currentPlant);
                }

                if (currentRecipe != null) 
                {
                    currentRecipe.GetResult().Item.SpawnPickup(transform.position, 1);
                }

                currentPlant = null;
            }
        }

and that should do the trick. Time to test it out

The test worked, thank you for your suggestion @Brian_Trotter

But I have a duplication bug when returning from a save, because the unique identifier of each plant growth handler was exactly the same for different parts. I need to find a way to not only make them unique, but also save and restore that, best done in ‘PlantGrowthHandler.cs’

Here’s an example of why this is terrible. Right now, each farming patch has 12 plant growth handlers, and literally each one has a unique id called ‘Slot1’, ‘Slot2’, ‘Slot3’, etc… and this is honestly terrible (but I needed that for testing purposes, now I need to fix it!):

Let’s say you had two farming plots before you quit the game. On the first one, you had 4 plants, and on the second one, you had 6 plants, and let’s say you quit the game and came back later

Now, when you return, let’s say the 4 plants in the first patch are done. What happens is not only does the first patch instantiate the final products, but the second patch, which is incomplete, BUT HAS THE EXACT SAME UNIQUE ID AS THE FIRST PATCH, Instantiates the final products as well, because the saving ID is exactly the same, AT THE EXACT SAME POSITIONS AS THE FIRST PATCH, BUT ON THE SECOND PATCH, although it is not done yet

And now let’s say a while later, the 6 watermelons on the second patch are done. Can you guess what happens? If you guessed “the first patch will instantiate 6 new watermelons out of nowhere”, you’d be correct

So yes, I need to find a way to save and restore the correct unique identifiers, and potentially create new identifiers as well for those plant growth handlers when we create a new piece in the game world. And because this is all dynamic, it’s probably not just a simple check for a string in an ‘OnBeforeSerialize()’ function

And why is this happening? Because the save and restore of the registered building parts parents all slots, so it didn’t classify them

And the exact same problem happens even if you don’t save the game


Edit: To fix this problem though, it was a bit of an insanely easy fix. I went to ‘Awake()’ in ‘PlantGrowthHandler.cs’, and created a new Guid when the plant growth handler is first instantiated (I’m not sure of this, please revise, but so far it doesn’t seem to create any new issues):

        void Awake()
        {
            eventBus = GetComponentInParent<ICraftingEventBus>();
            eventBus.RegisterObserver(this);

            // TEST - DELETE IF FAILED
            CraftingCompleteEstimationEvent.OnCraftingCompleteEstimation += InstantiateFinalProduct;
            UniqueIdentifier = Guid.NewGuid().ToString();
        }

        void OnDestroy()
        {
            eventBus.DeregisterObserver(this);

            // TEST - DELETE IF FAILED
            CraftingCompleteEstimationEvent.OnCraftingCompleteEstimation -= InstantiateFinalProduct;
            UniqueIdentifier = null; // So the Unique ID can be used again later
        }

and of course, don’t forget to save and restore that new Unique ID:

        public JToken CaptureAsJToken()
        {
            Debug.Log($"Capturing from PlantGrowthHandler {UniqueIdentifier}");
            return JToken.FromObject(UniqueIdentifier);
        }

        public void RestoreFromJToken(JToken state)
        {
            Debug.Log($"Restoring from PlantGrowthHandler {state.ToString()}");
            UniqueIdentifier = state.ToObject<string>();
        }

Before I count this system as complete, though, let me go ahead and create a new plant, and test it, and see what happens if we mix and match, just to make sure that nothing weird happens

It shouldn’t have the same ID… that’s the whole point of a UniqueID…

tell that to bixarrio (I wasn’t convinced of that part if I’m being totally honest at the start, but we were temporarily filling up slots just to get the ball rolling):

(Sorry man, we’re just joking, I promise xD)

The slots don’t get saved in the same way that other items gets saved. They become part of the build part and the unique id is really just to identify them again when restoring. It’s the same with the saving system. Type names are not unique across the scene, only in the scope of the saveable entity. They only need to be unique in scope of the building part. Using it for anything else was never a requirement.

The problem was that, because all the slots are saved to the building part, there were 12 components with the same type name and so we just added a way to identify each in a different way than using the type name

Except that with the added code complexity @bahaa wants, we need a Unique Identifier to avoid his problem with everything getting the wakeup call at once.

It’s inspired me, however, to work on a new class called UniqueIdentifier, which will actually be serialized as a UniqueIdentifier in scripts, but will have implicit conversion to strings so in most cases, changing the type won’t even be noticed… but under the hood, they’ll ensure that they are Unique (at least within a scene, which is really about the best you can hope for).

I have the framework written, I just have to write the Editor code and I’ll post it… but… I have to go to Sacramento for a physical therapy appointment, so this innovative solution (which will be in the Saving System Rewrite, ultimately) will have to wait till tomorrow.

see? I can also be inspiring :smiley:

Anyway, I saved the unique ID twice, but I want to know which one to keep. This one, in ‘CraftingTable.cs’, as an array of retrieved values:

            // TEST - DELETE IF FAILED
            string[] uniqueIdentifiers = new string[saveData.CurrentQuantity];

            // Plant the plants that are supposed to be there, based on where they are in time:
            if (CurrentRecipe != null && saveData.CurrentQuantity > 0)
            {
                for(int i = 0; i < saveData.CurrentQuantity; i++)
                {
                    var availableSlots = GetComponentsInChildren<PlantGrowthHandler>().Where(handler => handler.GetCurrentPlant() == null);
                    var firstAvailableSlot = availableSlots.First();
                    firstAvailableSlot.SetCurrentPlant(CurrentRecipe.GetResult().Item.GetPickup().gameObject);

                    // TEST - DELETE IF FAILED (FOR IDENTIFYING PLANT GROWTH HANDLERS)
                    uniqueIdentifiers[i] = GetComponentsInChildren<PlantGrowthHandler>()[i].UniqueIdentifier;
                }
            }

or this one, in ‘PlantGrowthHandler.cs’:

        public JToken CaptureAsJToken()
        {
            Debug.Log($"Capturing from PlantGrowthHandler {UniqueIdentifier}");
            return JToken.FromObject(UniqueIdentifier);
        }

        public void RestoreFromJToken(JToken state)
        {
            Debug.Log($"Restoring from PlantGrowthHandler {state.ToString()}");
            UniqueIdentifier = state.ToObject<string>();
        }

I’m lowkey also thinking of introducing a system where animals can attack your plants for food in the future, and you can assign NPCs from your teams to go and defend your food patches for a fee or something, at which point if you don’t pay them later they can just let your plants go to be eaten, and you’ll just be losing out on useful resources. Again, future plans… My animals aren’t still as alive as I’d like them to be to begin with. I need to carefully set them up as NPCs you can interact with, first of all, and this mix of malbers code and my code is a little terrifying if I’m being honest

Something else I find interesting, is adding water towers or something to temporarily (or permanently) speed up the farming process, for whatever reason you got in the game

For the time being, these plants can be turned into some food. The output is already a ‘Food.cs’ script, just gotta find a way to boost your health and consume them when they are clicked, probably through an ‘OnPointerClick’ function or something

Get well soon

@Brian_Trotter side question, any chance you know where I can get simple eating animations for my character for free or for cheap? I’m literally searching the internet for one, can’t find any (trying to turn the farming products into food. Mechanics are fine, but no animations)

There are a lot of animations in this pack. The pack is so large they had to split it into three parts. Get all three. There is a spreadsheet that tells what they all are. I know there are some eating ones in it. The quality varies a lot and you may have to adjust some, but it gives you a lot for free. A little extra work just helps with learning how animations work!

1 Like

You shouldn’t need to save the unique identifiers in the PlantGrowthHandler… as this should already be serialized (I feel like we’ve had this discussion before)…

You need the uniqueIdentifier for the static notification handler so that the PlantGrowthHandler can compare its own uniqueIdentifier to the one being broadcast.

The first review I read, is someone broke their project with this one. I’ll open it up in a seperate project and see what I can find. Thanks Ed :smiley:

so keep the modifications I did in ‘CraftingTable.cs’ and remove it from ‘PlantGrowthHandler.cs’? (I deleted the saving in ‘PlantGrowthHandler.cs’. All seems to be working just fine)

Just in case I need it later though, I’ll leave it here:

        public JToken CaptureAsJToken()
        {
            Debug.Log($"Capturing from PlantGrowthHandler {UniqueIdentifier}");
            return JToken.FromObject(UniqueIdentifier);
        }

        public void RestoreFromJToken(JToken state)
        {
            Debug.Log($"Restoring from PlantGrowthHandler {state.ToString()}");
            UniqueIdentifier = state.ToObject<string>();
        }

These animations are so lengthy, they can be classified into an 8 hour movie in and of itself :stuck_out_tongue: - and more importantly, they’re not simple and straight to the point. It’s like someone used them to research for something and then simply decided to post them on the internet for fun without a second revision

I can’t complain though, they’re free and I’m sure they have their uses :slight_smile:

(For folder number 5 though, I can see someone using these as cheerleading animations in their project)

Edit: 13_07, 13_08, 13_09 and 14_37 are drinking animations, and 22_13 has a little bit of potential, so food should hopefully be nearby. If not, take one of the drinking animations, trim it, speed it up and call it a day.

I’ll buy Cascadeur a few months down the line and just create the animations anyway, if needed

(13_24 is for sweeping the castle, 13_30 is Jumping Jacks)

Other than that, there’s not a single animation relevant to food in all 40 files, about 1000+ animations in total

Are you sure you downloaded all three parts? There are over 2400 animations in the package.

Using the spreadsheet and searching for eating gave me 5 results.

I can’t find parts 2 and 3…

Can’t find a spreadsheet in the downloaded file either, just a ‘Readthis’

Here is a link to their asset store page. The spreadsheet is in the folder in Unity when you import the pack. There are two, one for Excel and one for Google Sheets.

Privacy & Terms