A bit of guidance needed for some custom Farming code

Apologies, sure it’s reversed. Let’s move on to the next step :slight_smile:

Understandable :sweat_smile:

Anyway, with the new saving system in the ‘NewRPGCourseBuildingSaver.cs’ script, the plants don’t instantiate in place. It just acts like normal I guess

So what do we have now? You have the interface with the custom key?

Edit
I’m going to move to DM’s so we don’t clutter this post any further

Yes I do, but I strongly believe down the line that we will need a way to create a custom unique ID down the line in Update for plant growth handler or something, because players will be instantiating and destroying patches a lot, but that’s for later. For now let’s just get the basic step done

Sounds amazing, see you there :slight_smile:

I keep saying this; it doesn’t matter. It only needs to be unique in the scope of the building part. The slots belong to a building part. It doesn’t matter if there’s 1 or 1000 building parts, each slot belongs to a single building part and that building part can’t read the data of another building part

Ahh, OK that’s fair. Sorry I didn’t understand it the first time

Because he was showing off…

A quick question… Are the planting slots fixed at run time (in other words, they’re baked into the scene as child objects and new ones are never created or destroyed) or are they dynamic.

The answer to this question informs the approach to saving…

If the slots are fixed… as in the table will have exactly 12 slots, regardless of how many of them you may be using, for example, then you can either use a simple serial number, or you could simply make each slot a JsonSaveableEntity.

If the slots, on the other hand, are dynamic, then serial numbers aren’t actually that useful. Instead, you could use just a JArray of the existing slots, with the JToken containing all of the state of that slot. Then at Restore time, you clear all of the slots and instantiate new ones by iterating over the elements in the JArray and using that data.

I can’t tell if it’s me or Sam whose being a showoff :stuck_out_tongue:

Regardless, this is what we were aiming for. We gave each slot a unique serial number/unique ID, and we restored them from the Crafting Table’s saving system.

In the construction saving system we created a while ago, we introduced a new function that saves all children that have an IJsonSaveable, which are the plant slots themselves

I still can’t understand why we didn’t do the restoring in plant growth handler, but at this point in time, if it works then don’t touch it!

The only downfall is that the plant grows about 3-10 seconds before expected time for some reason, when we restore the save, and it’s driving me nuts as of why

In the end, I almost switched to a slot-based crafting approach, but honestly speaking this is just too many changes at this point in time, and I’m not sure if players want to go to each individual slot and do their thing and what not

It turns out this didn’t matter. The crafting table restores the recipe and all we needed to do was have the crafting table restore the plants into the slots, and the PlantGrowthHandler would take care of the rest.

and there’s been some cleanups done from my side. I removed all the quantity settings from the countless places I had, and only did it when crafting opens or ends, for efficiency purposes

Then I had false resource consumption because of all the cleaning up, so I tracked the correct variable down until I realized that it wasn’t being updated, and not to mention that the initial value was wrong, so I had to mention that to the system too

Next, I had an issue getting the correct experience. To fix that, I leveraged the new saving system to get how many slots were occupied, and did that in ‘OnCraftingStarted’ and ‘OnCraftingResumed’. That way it knows what to do when returned from a save as well, so now you get a correct multiplication factor to get the correct experience when farming is done

BUT… I need to synchronize instantiating the final product with the notification. Right now, it’s in ‘PlantGrowthHandler.cs’:

            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;
                craftingQuantity = FindObjectOfType<CraftingQuantity>(true); // Regardless of whether you're crafting or not (hence the word 'true' for when it's inactive), you'll need to refind that value and re-update it when crafting is done, otherwise you'll have a mathematical bug in the quantity UI the first time after the previous farm is done
            }

but I want to shift it to ‘CraftingNotification.cs’:

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

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

That way, you only get the pickup when crafting is done and the notification pops up

The notification has some minor delays where it can come earlier than crafting is done or slightly later, and that’s only a bug when restoring from a save, and I don’t know why, but if these two match then honestly speaking it doesn’t bother me much

(and the XP reward UI. I completely forgot about that!)

@bixarrio that’ll be my last problem and this system will be clean :slight_smile:

Anyway, it’s 5 AM here. Have a good night fellas

Sam was showing off with the OnBeforeSerializationCallback handlers. Simplest fix would have been to use OnValidate(), better fix would have been to use a custom editor.

1 Like

I’ll need an OnValidation function anyway, that’ll be the first thing to do (maybe, maybe not) when I return from my vacation :slight_smile: - I know this is 100% off topic, but I just had a swim like I was training for the olympics, although I’m 100% out of shape xD

@Brian_Trotter @bixarrio remember when I told you guys I had a bit of a delay with my restoring system, as in I get the farming products before the craft is over? I just solved this problem, and whilst to you guys that may sound normal, to me this was the pinnacle of my limited engineering skills :smiley: - anyway, here’s how I solved it:

  1. I realized the notification does not have any leads or lags when notifying the player of a complete notification, so I decided I will leverage that

  2. I created a brand new event class, which contains one event, and one function to trigger it. I called this class “CraftingCompleteEstimationEvent.cs”:

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

public static class CraftingCompleteEstimationEvent
{
    public static event Action OnCraftingCompleteEstimation;

    public static void InvokeOnCraftingCompleteEstimation() 
    {
        OnCraftingCompleteEstimation?.Invoke();
    }
}
  1. When the notification is executed, we will execute this function. It will be done in my custom ‘CraftingNotification.cs’ script:
    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);
            CraftingCompleteEstimationEvent.InvokeOnCraftingCompleteEstimation(); // This is the invoking line
        }

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

  1. ‘PlantGrowthHandler.cs’ will also have the subscription to the function in step 3, so that we can actually destroy the current instance and instantiate the final product:
        void Awake()
        {
            eventBus = GetComponentInParent<ICraftingEventBus>();
            eventBus.RegisterObserver(this);

            // TEST
            CraftingCompleteEstimationEvent.OnCraftingCompleteEstimation += InstantiateFinalProduct;
        }

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

            // TEST
            CraftingCompleteEstimationEvent.OnCraftingCompleteEstimation -= InstantiateFinalProduct;
        }

and the function itself:

        // TEST - DELETE IF FAILED
        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;
                craftingQuantity = FindObjectOfType<CraftingQuantity>(true); // Regardless of whether you're crafting or not (hence the word 'true' for when it's inactive), you'll need to refind that value and re-update it when crafting is done, otherwise you'll have a mathematical bug in the quantity UI the first time after the previous farm is done
            }
        }

Needless to say, I eliminated that final step from ‘GrowthRoutine’ in replacement of this. I only copy-pasted it:

            // TIME IS UP FOR MAKING THIS PLANT (FOR TESTING, ALL HANDLED IN 'InstantiateFinalProduct')

            /* // Destroy the plant
            Destroy(currentInstance);

            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;
                craftingQuantity = FindObjectOfType<CraftingQuantity>(true); // Regardless of whether you're crafting or not (hence the word 'true' for when it's inactive), you'll need to refind that value and re-update it when crafting is done, otherwise you'll have a mathematical bug in the quantity UI the first time after the previous farm is done
            } */

and I also made some variables, such as ‘CurrentRecipe’ and ‘CurrentInstance’ global, and set them up in ‘OnCraftingStarted’ and ‘OnCraftingResumed’ for all of this to work

With that being said, my Farming system is now complete to me :slight_smile: - until I decide to add farming booster mechanics of some sort, like some water or light rays or something to ensure faster growth


Except that now I have a brand new problem… it does that for ALL farm patches, not just the one it’s supposed to do so for

I’ll find a way to only get that crafting table’s information

No

public static event Action<WhateverTheTypeOfCurrentInstanceIs, WhateverTheTypeOfCurrentRecipeIs> OnCraftingCompletionEstimation;

public static void InvokeOnCraftingCompleteEstimation(WhateverTheTypeOfCurrentInstanceIs currentInstance, WhateverTheTypeOfCurrentRecipeIs currentRecipe)
{
    OnCraftingCompletionEstimation?.Invoke(currentInstance, currentRecipe);
}

All OnCraftingCompletionEstimation handlers should take in the currentInstance and currentRecipe and act on those variables alone.

Well now I’m stuck on “How am I going to get the current instance” for ‘CraftingNotification.cs’, this script:

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(, recipe); // 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;
    }
}

}

Which is a parent of ‘PlantGrowthHandler.cs’, the one carrying the instance we seek

I still don’t understand why do we sometimes need arguments for events and sometimes we don’t either. Is there a specific reason why we did that here as well?

I don’t have the full picture of your planting system, but the reason for passing the current items along the event bus is because you need a way to conclusively determine that this is the plant/instance that you’re looking for.
Simply setting a global current is generally counterproductive, because what happens if you plant something else before the timer’s expired? The original item gets lost in the shuffle.
Looking over the script, I can’t tell what’s going on with GetNumberOfOccupiedSlots(), because I’m not sure what occupies them. I would have thought that perhaps OnCraftingStarted() would find a slot. I’m guessing some other script calls OnCraftingStarted that might know the current slot that the recipe is going into…

This is a case where you follow the method calls…

  • The Event needs the instance and the recipe
  • The event is called by the static function, so it needs the instance and the recipe
  • The static function is called by the coroutine, so it needs the instance and the recipe
  • The coroutine is called by the OnCraftingStarted/OnCraftingResumed, so they need the instance and the recipe…
  • Whatever calls OnCraftingStarted/OnCraftingResumed needs to pass the instance along with the recipe so these things can happen.

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:

Privacy & Terms