A bit of guidance needed for some custom Farming code

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.

Found the excel sheet, but I don’t think the searches directly correspond to the files anymore. I’ll just use the drinking one I found, they look similar, until I can make my own animation

Other than that, I’ll get back to coding. Thanks again Ed!

Just btw, we only settled on using ‘Slot1’, ‘Slot2’, etc. because we were going off on a tangent of how to ensure the identifiers are unique and already something like 5 hours into a Sunday I didn’t want to spend doing all this. Because I knew they only needed to be unique in scope (at that point in time) I was happy to just name them ‘Slot1’, ‘Slot2’, etc. Now, obviously, this need to change.
Also, after we did the change to have the Crafting Table restore the planted crops, these slots did not need to be saved anymore, so the whole identifier thing could’ve (should’ve) been removed again

These are all mocap animations that have not been cleaned up. Free is much cheaper than spending $2,000 on a rokoko motion capture suit and doing them yourself. You’d still need to clean it up in the end

Ehh, at this point in time it’s done. It works for multiple types of plants and I see no bugs, so I’ll call it a day. I have two insanely difficult concepts coming up (making the MalberS animals NPCs we can interact with, and Parkour (for Parkour, I’ll try integrate what I learned from a third party course into my project one way or the other. Somehow, I’ll find a way)), and not to mention the teaming combat will need a bit of a re-write, because some systems are currently acting weird

Agreed with you on that one. I’ll take any sort of help I can find, by all means

However, for my game idea (I came up with an interesting idea recently), I will have to create robotic animations from scratch for Synty’s Polygon Mech pack. I want to mix modern war machines with medieval times, and other ideas to make something quite engaging :smiley:

For that, I’ll purchase Cascadeur down the line for it’s auto-posing system and make them myself. I don’t want to deal with animations in extreme detail if I’m being honest

Cascadeur is still the same. Cleanup is slightly easier because of the built in IK and physics, but it’s still not much easier than animating by hand

Privacy & Terms