Unity Fruit Harvesting

PHOTO EMBED

Tue Oct 07 2025 03:55:40 GMT+0000 (Coordinated Universal Time)

Saved by @BryanJ22

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using DG.Tweening;
using EvoDbManager;

public class FruitHarvesting : MonoBehaviour
{
    [System.Serializable]
    public class FruitGroup
    {
        public List<GameObject> fruitList;
        private List<Vector3> initialFruitSizes = new List<Vector3>();

        public List<Vector3> InitialFruitSizes { get { return initialFruitSizes; } set { initialFruitSizes = value; } }
    }

    //Amount of times we need to click on the tree before we can harvest the fruit
    public int HarvestShakesRequired = 3;

    public bool UseTestTime = false;

    //Test wait time in seconds
    public float TestWaitTime = 10;

    public ParticleSystem LeafParticleSystem;
    public Sprite FruitSprite;

    //Tracks if we are able to shake the tree
    //Is true by default. Set to false while the tree is already shaking
    private bool _canShake = true;

    //Tracks completed shakes during harvesting
    private int _harvestShakesCompleted = 0;

    //Tracks if we are able to harvest the tree
    //Is false by default, only true when the timer has reached 0 and the fruit can be harvested
    private bool _canHarvest = false;

    public string EnvironmentItemID;

    public List<FruitGroup> FruitGroups;
    private int currentFruitGroupIndex = 0;

    //List that stores initial y values of harvestable fruit
    //so we can reset the y value after the fruit falls
    private List<float> harvestableFruitInitialY;

    private SpriteRenderer spriteRenderer;
    MaterialPropertyBlock mpb;
    float outlineAlpha = 0;
    Sequence outlinePulseSequence;

    EnvironmentItem_Logic environmentItem_Logic;
    float timeLeftToGrow;

    List<float> percentagesComplete;

    bool updateOutlineShader = false;

    private EVODbEnvironmentItem dbEnvironmentItem;
    private float waitTime;

    //Set up everything and run GrowFruit coroutine so fruit starts growing right away
    private void Start()
    {
        dbEnvironmentItem = EVODbManager.Shared.environmentItems[EnvironmentItemID];

        EnvironmentItemID = StringHelper.ReplaceWhitespace(EnvironmentItemID, "");
        environmentItem_Logic = new EnvironmentItem_Logic();

        if(UseTestTime && GameEnvironmentInfo.IsEditorOrDevelopmentBuild())
        {
            waitTime = TestWaitTime;
        }

        else
        {
            waitTime = (float)dbEnvironmentItem.waitTime;
        }

        //Start logic, will use wait time later for tweening
        environmentItem_Logic.Start(dbEnvironmentItem.uniqueId, waitTime);

        spriteRenderer = this.GetComponent<SpriteRenderer>();
        mpb = new MaterialPropertyBlock();
        spriteRenderer.GetPropertyBlock(mpb);

        harvestableFruitInitialY = new List<float>();

        percentagesComplete = new List<float>();

        for (int i = 0; i < FruitGroups.Count; i++)
        {
            SetInitialSizes(FruitGroups[i]);

            if (i == FruitGroups.Count - 1)
            {
                //Set inital positions for only the last group of fruit (The harvesteable fruit),
                //as this is the only fruit that will fall and will need it's position reset.
                SetInitialFruitPosition(FruitGroups[i].fruitList);
            }
        }

        StartFruitGrowing();
    }

    void SetInitialSizes(FruitGroup fruitGroup)
    {
        for (int i = 0; i < fruitGroup.fruitList.Count; i++)
        {
            fruitGroup.InitialFruitSizes.Add(fruitGroup.fruitList[i].transform.localScale);
        }
    }

    private void StartFruitGrowing()
    {
        double timeLeft = environmentItem_Logic.SetLastInteraction_TotalSeconds();

        if (timeLeft < 0)
        {
            timeLeft = 0;
        }

        //First we calculate the percentage of time we have left to wait
        //(time remaining / total time to wait) * 100
        float percentageLeft = (Convert.ToSingle(timeLeft / waitTime)) * 100;

        //We get the inverse by doing 100 - time remaining
        //so we have the percentage amount of time that has passed so far.
        float percentageCompleted = 100 - percentageLeft;

        //We calculate the percentage out of 100 of each fruit group
        //Normally we have 4 fruit groups so 100 / 4 = 25
        float oneGroupPercent = 100 / FruitGroups.Count;

        //Loops through all of the object lists
        for (int i = 0; i < FruitGroups.Count; i++)
        {
            percentagesComplete.Add(percentageCompleted);

            //Get min and max percentage of each fruit group
            float min = oneGroupPercent * i;
            float max = min + oneGroupPercent;

            //If our percentage completed is within the min and max of this fruit group
            //It means this is the fruit group that is currently growing and therefore we set its
            //size based on the percentWithinRange
            if (percentageCompleted > min && percentageCompleted <= max)
            {
                //Here we get the percentage out of 100 that the percentage completed is within each fruit groups min/max range
                //((input - min) * 100) / (max - min)
                //Example: If our time completed is 15%, we calculate ((15 - 0) * 100) / (25 - 0) = 60%
                //We can then use this percentage to calculate the size the fruit in the group should be
                //with 0% being at a size of 0, and 100% being at its largest size
                float percentWithinRange = ((percentageCompleted - min) * 100) / (max - min);
                SetInitialFruitSizes(FruitGroups[i], percentWithinRange);
                currentFruitGroupIndex = i;
            }

            //All other fruits are not growing and should therefore not be showing,
            //so we set their sizes to 0
            else
            {
                SetInitialFruitSizes(FruitGroups[i], 0);
            }
        }

        timeLeftToGrow = Convert.ToSingle(timeLeft);
        StartCoroutine(GrowFruit());
    }

    //Save list of initial fruit sizes so we know what their end size should be when scaling them up
    private void SetInitialFruitSizes(FruitGroup fruitGroup, float scale)
    {
        for (int i = 0; i < fruitGroup.fruitList.Count; i++)
        {
            //If we pass in a scale of zero, set scale to zero with no special logic
            if (scale == 0)
            {
                fruitGroup.fruitList[i].transform.localScale = new Vector3(0, 0, 1);
            }

            //Otherwise, we will calculate the fruit's size based on the percentage we pass in
            else
            {
                //Here we calculate each fruits size based on percentWithinRange and the fruits maximum/starting size
                //Value = percentage * max / 100
                //Example: If our fruit is 60% grown, then our fruit size would be 60 * .5f / 100 = .3f,
                //so .3 is 60% in a range of 0-.5f
                float currentScaleValueX = scale * fruitGroup.fruitList[i].transform.localScale.x / 100;
                float currentScaleValueY = scale * fruitGroup.fruitList[i].transform.localScale.y / 100;

                fruitGroup.fruitList[i].transform.localScale = new Vector3(currentScaleValueX, currentScaleValueY, 1);
            }
        }
    }

    //Save list of initial harvesteable fruit y positions so we can reset them later after they fall
    private void SetInitialFruitPosition(List<GameObject> objList)
    {
        for (int i = 0; i < objList.Count; i++)
        {
            harvestableFruitInitialY.Add(objList[i].transform.localPosition.y);
        }
    }

    //Used to loop through objects and call a function for each object.
    //We pass in a tween function to be run
    private void TweenFruitGroup(Func<GameObject, float, Tween> TweenFunction, FruitGroup fruitGroup, Sequence s, float duration)
    {
        for (int i = 0; i < fruitGroup.fruitList.Count; i++)
        {
            s.Join(TweenFunction.Invoke(fruitGroup.fruitList[i], duration));
        }
    }

    //Override with object sizes
    private void TweenFruitGroup(Func<Vector2, GameObject, float, Tween> TweenFunction, FruitGroup fruitGroup, Sequence s, float duration)
    {
        for (int i = 0; i < fruitGroup.fruitList.Count; i++)
        {
            s.Join(TweenFunction.Invoke(fruitGroup.InitialFruitSizes[i], fruitGroup.fruitList[i], duration));
        }
    }

    //Reset all fruits after you harvest them
    private void ResetFruit(FruitGroup fruitGroup)
    {
        bool resetY = false;

        //If this is the last fruit group (which is the harvesteable group), reset the y pos for that group
        if (FruitGroups.IndexOf(fruitGroup) == FruitGroups.Count - 1)
        {
            resetY = true;
        }

        for (int i = 0; i < fruitGroup.fruitList.Count; i++)
        {
            if (resetY)
            {
                //Reset y pos back to initial
                Vector3 pos = Vector3.zero;
                pos.x = fruitGroup.fruitList[i].transform.localPosition.x;
                pos.y = harvestableFruitInitialY[i];
                pos.z = fruitGroup.fruitList[i].transform.localPosition.z;

                fruitGroup.fruitList[i].transform.localPosition = pos;
            }

            //Reset size to zero
            fruitGroup.fruitList[i].transform.localScale = Vector3.zero;

            //Reset sprite alpha to 1
            SpriteRenderer sRenderer = fruitGroup.fruitList[i].GetComponent<SpriteRenderer>();
            Color c = sRenderer.color;
            c.a = 1;

            sRenderer.color = c;
        }
    }

    //Coroutine where we "grow" the fruits by scaling them based on the environment item's wait time
    private IEnumerator GrowFruit()
    {
        //Create new Last fruit group list that will hold the previous list
        FruitGroup LastFruitGroup = null;

        //Loop through all object lists and call tween logic for each list
        //pass in our sequence so we can append/join tweens to our sequence
        for (int i = currentFruitGroupIndex; i < FruitGroups.Count; i++)
        {
            //Create a new sequence. We will use this sequence for all of the object scale tweens
            //so we control when the objects tween and what to do when they are all finished tweening.
            Sequence growFruitSequence = DOTween.Sequence();
            currentFruitGroupIndex = i;

            if (LastFruitGroup != null)
            {
                TweenFruitGroup(TweenHelper.FadeOut, LastFruitGroup, growFruitSequence, 1.3f);
            }

            float timeCompleted = Convert.ToSingle(waitTime - timeLeftToGrow);
            float timeToCompleteOneGroup = Convert.ToSingle(waitTime) / FruitGroups.Count;

            //Get min and max time to grow for each fruit group
            float min = timeToCompleteOneGroup * i;
            float max = min + timeToCompleteOneGroup;

            TweenFruitGroup(TweenHelper.Scale, FruitGroups[i], growFruitSequence, max - timeCompleted);

            //Set last fruit group 
            LastFruitGroup = FruitGroups[i];

            //If next index is = to the count, we are currently on our last group
            if (i + 1 == FruitGroups.Count)
            {
                //After all of our tweens are finished scaling,
                //we set our _canHarvest bool to true which means we are now able to start clicking on the tree to harvest it
                growFruitSequence.OnComplete(() =>
                {
                    //Create the outline pulsing in and out effect
                    //This will loop infinitely until the sequence is killed after the tree has been harvested
                    CreateOutlineTween();
                    updateOutlineShader = true;
                    _canHarvest = true;
                });
            }

            //Wait for all objects in current group to finish tweening
            //Once finished we can continue the loop and move onto the next group list
            yield return growFruitSequence.WaitForCompletion();
        }
    }

    void CreateOutlineTween()
    {
        //Create the outline pulsing in and out effect
        //This will loop infinitely until the sequence is killed after the tree has been harvested
        outlinePulseSequence = DOTween.Sequence();
        outlinePulseSequence.SetAutoKill(false);

        outlinePulseSequence.Append(DOTween.To(() => outlineAlpha, x => outlineAlpha = x, 1, .6f).SetEase(Ease.InQuad));
        outlinePulseSequence.AppendInterval(.12f);

        outlinePulseSequence.Append(DOTween.To(() => outlineAlpha, x => outlineAlpha = x, 0, .6f).SetEase(Ease.InQuad));
        outlinePulseSequence.AppendInterval(.12f);

        outlinePulseSequence.OnComplete(() => outlinePulseSequence.Restart());
    }

    //ShakePlant is called by the OnTouch event triggered by the DistanceTouchEventTrigger script
    //When we are allowed to tap on the item based on our distance from it, this function is called
    public void ShakePlant()
    {
        //The ShakePlant function is run from the OnTouch() event handler in the DistanceTouchEventTrigger script
        //Only run shake logic when the tweens aren't currently running
        if (_canShake)
        {
            //Can not shake the tree while fruits are tweening
            _canShake = false;

            if (_canHarvest)
            {
                InputManager.Instance.DisableRigidbodyMovement();
                _ = QuestionSceneLoader.LoadQuestionScene(DoPostQuestionLogic, QuestionContext.Vegetation);
            }

            else
            {
                Sequence shakeSequence = DoShakeTweenSequence();
                shakeSequence.OnComplete(() =>
                {
                    //If fruit is not falling to the ground and this is just a transition tween,
                    //player can shake tree again after tweening is finished
                    _canShake = true;
                });
            }
        }
    }

    private Sequence DoShakeTweenSequence()
    {
        //Add tweens to tween sequence
        Sequence shakeSequence = TweenHelper.ShakeSequence(this.gameObject, .8f, new Vector2(.02f, .02f), 3);

        LeafParticleSystem.Play();
        //Shake tree

        //Tween fruit shake for all the current group
        TweenFruitGroup(TweenHelper.ShakeRot, FruitGroups[currentFruitGroupIndex], shakeSequence, .8f);

        return shakeSequence;
    }

    void DoPostQuestionLogic(QuestionAnswerInfo callback)
    {
        DoShakeTweenSequence();

        InputManager.Instance.EnableRigidbodyMovement();

        //increment the amount of shakes we have completed so far
        _harvestShakesCompleted++;

        //Check for which TweenFall we should run
        CheckFruitFall();
    }

    //Set Fruit fall logic for harvesting based on if this is our final shake or not
    void CheckFruitFall()
    {
        Sequence fruitFallSequence = DOTween.Sequence();

        //if we are not on our final shake,
        //tween the fruits without the fall to ground effect
        if (_harvestShakesCompleted != HarvestShakesRequired)
        {
            //Tween fruit small fall for all the current group
            TweenFruitGroup(TweenHelper.SmallFall, FruitGroups[currentFruitGroupIndex], fruitFallSequence, .5f);
            fruitFallSequence.OnComplete(() =>
            {
                _canShake = true;
            });
        }

        //if we ARE on our final shake,
        //tween the fruits WITH the fall to ground effect
        else
        {
            //Tween fruit fall to ground for all the current group
            TweenFruitGroup(TweenHelper.GroundFall, FruitGroups[currentFruitGroupIndex], fruitFallSequence, .4f);

            //after fruit falls to the group, show rewards
            fruitFallSequence.OnComplete(() =>
            {
                NavigationUIController.Instance.ShowVegetationRewardsUI(environmentItem_Logic, dbEnvironmentItem, FruitSprite);
                RestartTreeHarvesting();
            });
        }
    }

    //Restart logic to restart everything after you have finished harvesting
    void RestartTreeHarvesting()
    {
        //Tree has been harvested and apples have fallen to ground
        //reset _canHarvest bool and reset values so fruit can start growing again
        _canHarvest = false;

        _harvestShakesCompleted = 0;

        outlinePulseSequence.OnComplete(() => updateOutlineShader = false);
        outlinePulseSequence.SetAutoKill(true);

        //Fade out apples on ground
        Sequence fadeOutSequence = DOTween.Sequence();
        TweenFruitGroup(TweenHelper.FadeOut, FruitGroups[currentFruitGroupIndex], fadeOutSequence, 1.3f);

        //After apples have faded out, set their positions back to their initial positions,
        //set all groups scale back to zero, and set all groups alpha back to 1. This gets them ready for growing again
        fadeOutSequence.OnComplete(() =>
        {
            _canShake = true;

            //Loop through all groups
            for (int i = 0; i < FruitGroups.Count; i++)
            {
                //Reset y pos of current group (harvestable group)
                //Reset sizes and alpha of all items in all groups
                ResetFruit(FruitGroups[i]);
            }

            environmentItem_Logic.UpdateInteractionTime();

            StartFruitGrowing();
        });
    }

    private void Update()
    {
        if (updateOutlineShader)
        {
            UpdateOutlineAlpha();
        }
    }

    //Set alpha property of outline in our shader to the alpha we are tweening to create a phasing effect
    void UpdateOutlineAlpha()
    {
        mpb.SetFloat("_OutlineAlpha", outlineAlpha);
        spriteRenderer.SetPropertyBlock(mpb);
    }
}
content_copyCOPY