creating a farming rpg harvest moon part 8

Creating a Farming RPG (like Harvest Moon) in Unity — Part 8: Regrowing and Wilting Crops

This article is a part of the series:
Creating a Farming RPG (like Harvest Moon) in Unity

Ever wanted to create a game like Harvest Moon in Unity? Check out Part 8 of our guide here, where we go through how to make crops that can be harvested multiple times, along with a system that allows for them to die. You can also find Part 7 of our guide here, where we went through how to grow and harvest crops.

A link to a package containing the project files up to Part 8 of this tutorial series can also be found at the end of this article, exclusive to Patreon supporters only.

1. Regrowable Crops

In Part 7 of the series, we managed to set up the core grow and harvest system for single-harvest crops, which would make up the bulk of crops the player will work with. Once harvested, the crop is cleared from the land.

a. Additional properties for the SeedData ScriptableObject

For regrowable crops to be a thing in the game, we need additional information in our SeedData:

  • Will the plant be able to regrow the crop after being harvested?
  • If so, how long would it take?

Hence, add the following to SeedData:

SeedData.cs

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

[CreateAssetMenu(menuName ="Items/Seed")]
public class SeedData : ItemData
{
    //Time it takes before the seed matures into a crop
    public int daysToGrow;

    //The crop the seed will yield
    public ItemData cropToYield;

    //The seedling GameObject
    public GameObject seedling;

    [Header("Regrowable")]
    //Is the plant able to regrow the crop after being harvested?
    public bool regrowable;
    //Time taken before the plant yields another crop
    public int daysToRegrow;
}

With this, we are able to create our first regrowable crops. Under Assets/Data/Tools/Seeds, create a new Seed (Create > Items > Seed) with the name ‘Tomato Seeds‘. Fill in the fields:

Creating the Tomato Seeds ScriptableObject
Write whatever you think is best for the description and give it a suitable thumbnail.

For this particular seed, it will take 9 days for the crop to mature, and it will subsequently take 3 days after harvesting it for it to be harvestable again. The player will harvest tomatoes from it, so Create > Item in the Assets/Data/Items and name it ‘Tomato’. Fill in the fields:

Creating the Tomato ScriptableObject
Give it a description and thumbnail.

In the Tomato Seeds Seed ScriptableObject, set the Crop to Yield to the newly-created Tomato.

Crop To Yield Tomato

b. Setting up Prefabs for a regrowable crop

From Assets/Imported Asset/Cartoon_Farm_Crops/Prefabs/Standard, duplicate Tomato_Plant and Tomato_Fruit, rename them to ‘Tomato Seedling‘ and ‘Tomato‘ respectively. Afterwards, move these to Assets/Prefabs/Crops. On the Tomato prefab, set its Tag and Layer to Item, and set its rotation to face upright:

Adjusting Tomato Object

As it is the Item GameObject the player will be interacting with, add InteractableObject to the Tomato prefab and set the Item to the Tomato ItemData we made from 1a.

Like what we did with the Cabbage seedling in Part 7, remove the Mesh Collider from the Tomato Seedling Prefab as the player has no business colliding with it.

tomato seedling removing the mesh collider

Set the Tomato Seeds SeedData’s seedling to this prefab:

setting the seedling prefab

In single-harvest crops, the GameObject that spawns in the crop’s Harvestable state is the same Item GameObject the player receives (E.g. Cabbage seedlings grow into cabbages, the cabbage is harvested). This is not the case for regrowable crops. In the case of the tomato, the tomato seedling yields tomatoes on it, but only the tomato itself is harvested.

To this end, we need a separate prefab to represent this harvestable state. Duplicate the Tomato Seedling Prefab and name it ‘Tomato Harvestable‘. On it, add Tomato GameObjects like this:

tomato harvestable plant
This is what it looks like when the tomato is fully grown.

The player needs to be able to interact with this object to harvest, so set the Tag and Layer to Item and give it back its Mesh Collider.

adjusting tomato harvestable

In CropBehaviour, the script knows what GameObject to instantiate for the Harvestable state from the Seed’s cropToYield ItemData. However, we currently have Tomato SeedscropToYield set to Tomato. This means that it will instantiate the Tomato prefab instead of the Tomato Harvestable prefab.

To get around this, simply create a new Item in Assets/Data/Items and call it ‘Tomato Crop‘ and set its Game Model to the Tomato Harvestable prefab.

In this tutorial, we chose to just work with whatever we have set up. However, if you don’t like having so many Items to work with, you could alternatively add an additional GameObject variable in SeedData to store the prefab information instead. Just remember to change up the logic in CropBehaviour as well.

In the Tomato Seeds SeedData, change the Crop To Yield to the Tomato Crop ItemData.

changing crop to yield

Article continues after the advertisement:


c. Creating the regrowing behaviour for the crop

By now, if you were to add the Tomato Seed to the Inventory and test it, the seed should grow into the tomato plant as intended.

Add the tomato seed to inventory
You can add items to the player’s inventory from InventoryManager in the Manager GameObject.
Cultivating a tomato plant
If you have followed the steps right, this should be the final stage of the crop.

The class InteractableObject is used for the player to pick up the harvestable crop. However, we need the crop to do more than just the simple Pickup function. It needs to also revert the crop’s state back to Seedling for it to start the regrowing process over.

Thus, create a new script that inherits from InteractableObject called ‘RegrowableHarvestBehaviour‘:

RegrowableHarvestBehaviour.cs

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

public class RegrowableHarvestBehaviour :MonoBehaviour InteractableObject
{
    
   void Start()
    {
        
    }


   public void Update()
    {
        
    }


}

Add this script to the Tomato Harvestable prefab.

Add RegrowableHarvestBehaviour to the prefab

We will need to override the Pickup function in InteractableObject, so add the virtual keyword to the function:

Learn more about the virtual keyword in C# here.

InteractableObject.cs

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

public class InteractableObject : MonoBehaviour
{
    //The item information the GameObject is supposed to represent
    public ItemData item;

    public virtual void Pickup()
    {
        //Set the player's inventory to the item
        InventoryManager.Instance.equippedItem = item;
        //Update the changes in the scene
        InventoryManager.Instance.RenderHand();
        //Destroy this instance so as to not have multiple copies
        Destroy(gameObject); 
    }
}

When the player harvests the crop from the regrowable plant, the following will happen:

  1. The yielded crop will be added to the player’s inventory
  2. The inventory changes will be rendered in the scene
  3. The crop state in the plant’s CropBehaviour will reset back to Seedling

For 3. to be done, RegrowableHarvestBehaviour must be able to identify which CropBehaviour it is supposed to work with. Add the following to RegrowableHarvestBehaviour:

RegrowableHarvestBehaviour.cs

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

public class RegrowableHarvestBehaviour : InteractableObject
{
    CropBehaviour parentCrop; 

    //Sets the parent crop
    public void SetParent(CropBehaviour parentCrop)
    {
        this.parentCrop = parentCrop;
    }

}

Call this function in the Plant() function of CropBehaviour:

CropBehaviour.cs

    //Initialisation for the crop GameObject
    //Called when the player plants a seed
    public void Plant(SeedData seedToGrow)
    {
        //Save the seed information
        this.seedToGrow = seedToGrow;

        //Instantiate the seedling and harvestable GameObjects
        seedling = Instantiate(seedToGrow.seedling, transform);

        //Access the crop item data
        ItemData cropToYield = seedToGrow.cropToYield;

        //Instantiate the harvestable crop
        harvestable = Instantiate(cropToYield.gameModel, transform);

        //Convert Days To Grow into hours
        int hoursToGrow = GameTimestamp.DaysToHours(seedToGrow.daysToGrow);
        //Convert it to minutes
        maxGrowth = GameTimestamp.HoursToMinutes(hoursToGrow);

        //Check if it is regrowable
        if (seedToGrow.regrowable)
        {
            //Get the RegrowableHarvestBehaviour from the GameObject
            RegrowableHarvestBehaviour regrowableHarvest = harvestable.GetComponent<RegrowableHarvestBehaviour>();

            //Initialise the harvestable 
            regrowableHarvest.SetParent(this);
        }

        //Set the initial state to Seed
        SwitchState(CropState.Seed); 

    }

Scroll down to the SwitchState() method of the class. As of now, it will unparent the harvestable GameObject and destroy its own GameObject upon reaching the Harvestable state. This should not be the case for our regrowable crops. Hence, add an if statement to that block of code:

CropBehaviour.cs

    //Function to handle the state changes 
    void SwitchState(CropState stateToSwitch)
    {
        //Reset everything and set all GameObjects to inactive
        seed.SetActive(false);
        seedling.SetActive(false);
        harvestable.SetActive(false);

        switch (stateToSwitch)
        {
            case CropState.Seed:
                //Enable the Seed GameObject
                seed.SetActive(true);
                break;
            case CropState.Seedling:
                //Enable the Seedling GameObject
                seedling.SetActive(true);
                break;
            case CropState.Harvestable:
                //Enable the Harvestable GameObject
                harvestable.SetActive(true);

                //If the seed is not regrowable, detach the harvestable from this crop gameobject and destroy it. 
                if (!seedToGrow.regrowable)
                {
                    //Unparent it to the crop
                    harvestable.transform.parent = null;
                    Destroy(gameObject);
                }
                
                
                break;
        }

        //Set the current crop state to the state we're switching to
        cropState = stateToSwitch; 
    }

With the planting and harvesting logic covered within the class, we now move on to the regrowing aspect. It needs to do the following:

  1. Push the growth value back by the time taken to regrow
  2. Reset the CropState back to Seedling

This can be done by adding a new function to CropBehaviour:

CropBehaviour.cs

    //Called when the player harvests a regrowable crop. Resets the state to seedling 
    public void Regrow()
    {
        //Reset the growth 
        //Get the regrowth time in hours
        int hoursToRegrow = GameTimestamp.DaysToHours(seedToGrow.daysToRegrow);
        growth = maxGrowth - GameTimestamp.HoursToMinutes(hoursToRegrow);

        //Switch the state back to seedling
        SwitchState(CropState.Seedling); 
    }

We can now return to RegrowableHarvestBehaviour and override the Pickup method:

RegrowableHarvestBehaviour.cs

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

public class RegrowableHarvestBehaviour : InteractableObject
{
    CropBehaviour parentCrop; 

    //Sets the parent crop
    public void SetParent(CropBehaviour parentCrop)
    {
        this.parentCrop = parentCrop;
    }

    public override void Pickup()
    {
        //Set the player's inventory to the item
        InventoryManager.Instance.equippedItem = item;
        //Update the changes in the scene
        InventoryManager.Instance.RenderHand();

        //Set the parent crop back to seedling to regrow it
        parentCrop.Regrow();

    }
}

It’s mostly identical with that of its parent, except instead of destroying itself afterwards, it calls the Regrow function of the parented crop. So that it knows that the player will be picking a tomato up on harvest, go to the Tomato Harvestable prefab and set its item to Tomato.

This item variable was inherited from InteractableObject.

set item to tomato

We forgot to set the Game Model for the Tomato, so go back to the Tomato ItemData and set it to the prefab we made:

Set Tomato Game Model
It will cause errors if you don’t!

With that, we are able to cultivate and harvest regrowable crops.


Article continues after the advertisement:


2. Plant Wilting and Death

As of now, whatever the player plants will be practically immortal. Let’s make it possible for crops to wilt and die.

a. Setting up the Wilted State

Duplicate the Dirt material from Assets/Imported Asset/Farmland/Dirt/Materials and rename it to ‘Wilted Plant‘.

duping dirt

Set its Albedo colour to a reddish-brown and lower its Smoothness to 0.

From dirt it was, to dirt it shall return.

Enter the Crop prefab from Assets/Prefabs/Crops and drag in the Pumpkin_Plant prefab from Assets/Imported Asset/Cartoon_Farm_Crops/Prefabs/Standard.

This game model will represent all wilted crops.

Make the following adjustments:

  1. Rename it to ‘Wilted’
  2. Give it the Wilted Plant material.
  3. Set its scale to 0.5
  4. Position it so that it is in the middle of the Seed GameObject.
wilted adjustments

Add the wilted CropState along with a reference to this GameObject in CropBehaviour:

CropBehaviour.cs

    //Information on what the crop will grow into 
    SeedData seedToGrow;

    [Header("Stages of Life")]
    public GameObject seed;
    public GameObject wilted; 
    private GameObject seedling;
    private GameObject harvestable;


    //The growth points of the crop
    int growth;
    //How many growth points it takes before it becomes harvestable
    int maxGrowth; 

    public enum CropState
    {
        Seed, Seedling, Harvestable, Wilted
    }
    //The current stage in the crop's growth
    public CropState cropState;
    

Set the reference accordingly:

set the wilted reference

Create a case for it to switch to the Wilted state in CropBehaviour's SwitchState function:

CropBehaviour.cs

    //Function to handle the state changes 
    void SwitchState(CropState stateToSwitch)
    {
        //Reset everything and set all GameObjects to inactive
        seed.SetActive(false);
        seedling.SetActive(false);
        harvestable.SetActive(false);
        wilted.SetActive(false);

        switch (stateToSwitch)
        {
            case CropState.Seed:
                //Enable the Seed GameObject
                seed.SetActive(true);
                break;
            case CropState.Seedling:
                //Enable the Seedling GameObject
                seedling.SetActive(true);
                break;
            case CropState.Harvestable:
                //Enable the Harvestable GameObject
                harvestable.SetActive(true);

                //If the seed is not regrowable, detach the harvestable from this crop gameobject and destroy it. 
                if (!seedToGrow.regrowable)
                {
                    //Unparent it to the crop
                    harvestable.transform.parent = null;
                    Destroy(gameObject);
                }
                
                
                break;
            case CropState.Wilted:
                //Enable the wilted GameObject
                wilted.SetActive(true);
                break;
        }

        //Set the current crop state to the state we're switching to
        cropState = stateToSwitch; 
    }

b. Plant health system

There are two main ways crops die in the game:

  1. If the player neglects to water the plants
  2. When the season changes and the crop is not suited to grow in that season

As we have not implemented the seasonal crops system yet, we will focus on the first way. This can be implemented in the same way we did the growth system:

  1. For every minute the Land is not watered, a function that handles the wilting process of the crop in CropBehaviour is called. We will call this Wither().
  2. The health of the plant will be tracked in Wither().
  3. When the health reaches 0, it will switch its state to Wilted.

For this tutorial, we will make it so that the crop dies after not being watered for 2 days (48 hours). Declare the following variables in CropBehaviour:

CropBehaviour.cs

    //Information on what the crop will grow into 
    SeedData seedToGrow;

    [Header("Stages of Life")]
    public GameObject seed;
    public GameObject wilted; 
    private GameObject seedling;
    private GameObject harvestable;
    

    //The growth points of the crop
    int growth;
    //How many growth points it takes before it becomes harvestable
    int maxGrowth;

    //The crop can stay alive for 48 hours without water before it dies
    int maxHealth = GameTimestamp.HoursToMinutes(48); 

    int health;

    public enum CropState
    {
        Seed, Seedling, Harvestable, Wilted
    }
    //The current stage in the crop's growth
    public CropState cropState;

The wilting process should only begin after germination, that is from the point it becomes a seedling. Hence, we will initialise the health value at that stage. Add the following to the SwitchState() function:

CropBehaviour.cs

   //Function to handle the state changes 
    void SwitchState(CropState stateToSwitch)
    {
        //Reset everything and set all GameObjects to inactive
        seed.SetActive(false);
        seedling.SetActive(false);
        harvestable.SetActive(false);
        wilted.SetActive(false);

        switch (stateToSwitch)
        {
            case CropState.Seed:
                //Enable the Seed GameObject
                seed.SetActive(true);
                break;
            case CropState.Seedling:
                //Enable the Seedling GameObject
                seedling.SetActive(true);

                //Give the seed health 
                health = maxHealth; 

                break;
            case CropState.Harvestable:
                //Enable the Harvestable GameObject
                harvestable.SetActive(true);

                //If the seed is not regrowable, detach the harvestable from this crop gameobject and destroy it. 
                if (!seedToGrow.regrowable)
                {
                    //Unparent it to the crop
                    harvestable.transform.parent = null;
                    Destroy(gameObject);
                }
                
                
                break;
            case CropState.Wilted:
                //Enable the wilted GameObject
                wilted.SetActive(true);
                break;
        }

        //Set the current crop state to the state we're switching to
        cropState = stateToSwitch; 
    }

We also want the plant to heal up whenever the soil is watered, so add the following to the Grow() function:

CropBehaviour.cs

    //The crop will grow when watered
    public void Grow()
    {
        //Increase the growth point by 1
        growth++;

        //Restore the health of the plant when it is watered
        if(health < maxHealth)
        {
            health++;
        }

        //The seed will sprout into a seedling when the growth is at 50%
        if(growth >= maxGrowth / 2 && cropState == CropState.Seed)
        {
            SwitchState(CropState.Seedling); 
        }

        //Grow from seedling to harvestable
        if(growth >= maxGrowth && cropState == CropState.Seedling)
        {
            SwitchState(CropState.Harvestable);
        }
    }

Add the Wither() function to the class:

CropBehaviour.cs

    //The crop will progressively wither when the soil is dry 
    public void Wither()
    {
        health--;
        //If the health is below 0 and the crop has germinated, kill it
        if(health <= 0 && cropState != CropState.Seed)
        {
            SwitchState(CropState.Wilted);
        }
    }

Finally, we call Wither() from Land under the ClockUpdate() function:

Land.cs

    public void ClockUpdate(GameTimestamp timestamp)
    {
        //Checked if 24 hours has passed since last watered
        if(landStatus == LandStatus.Watered)
        {
            //Hours since the land was watered
            int hoursElapsed = GameTimestamp.CompareTimestamps(timeWatered, timestamp);
            Debug.Log(hoursElapsed + " hours since this was watered");

            //Grow the planted crop, if any
            if(cropPlanted != null)
            {
                cropPlanted.Grow();
            }

            if(hoursElapsed > 24)
            {
                //Dry up (Switch back to farmland)
                SwitchLandStatus(LandStatus.Farmland);
            }
        }

        //Handle the wilting of the plant when the land is not watered
        if(landStatus != LandStatus.Watered && cropPlanted != null)
        {
            //If the crop has already germinated, start the withering
            if (cropPlanted.cropState != CropBehaviour.CropState.Seed)
            {
                cropPlanted.Wither();
            }
        }
    }

Testing the game, you should observe the following:

  1. Seeds that are consistently watered yield crops
  2. Seeds that germinate into seedlings but are then neglected wilt
  3. Seeds that are not watered at all remain unchanged
Wilting mechanic in action.
Our wilting mechanic in action.

Article continues after the advertisement:


c. Getting rid of unwanted and wilted crops

After the crop wilts, its remains are going to be stuck there until the player clears it. Hence, we need to make a tool for the player that does just that. Let’s make the shovel the tool for players to clear plant-based obstacles.

Add it as a ToolType enum value in EquipmentData:

EquipmentData.cs

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

[CreateAssetMenu(menuName = "Items/Equipment")]
public class EquipmentData : ItemData
{
    public enum ToolType
    {
        Hoe, WateringCan, Axe, Pickaxe, Shovel
    }
    public ToolType toolType; 
 
}

Under Assets/Data/Tools, create an asset for the Shovel:

creating shovel tool

Add the shovel to the player’s inventory:

add shovel to player inventory
You can add items to the player’s inventory from InventoryManager in the Manager GameObject.

Giving the shovel its functionality is simple with the interaction and tool systems we have made in the previous parts. Simply add a casein the toolType switch for it to destroy planted crops on the Interact function in Land:

Land.cs

    //When the player presses the interact button while selecting this land
    public void Interact()
    {
        //Check the player's tool slot
        ItemData toolSlot = InventoryManager.Instance.equippedTool;

        //If there's nothing equipped, return
        if (toolSlot == null)
        {
            return; 
        }

        //Try casting the itemdata in the toolslot as EquipmentData
        EquipmentData equipmentTool = toolSlot as EquipmentData; 

        //Check if it is of type EquipmentData 
        if(equipmentTool != null)
        {
            //Get the tool type
            EquipmentData.ToolType toolType = equipmentTool.toolType;

            switch (toolType)
            {
                case EquipmentData.ToolType.Hoe:
                    SwitchLandStatus(LandStatus.Farmland);
                    break;
                case EquipmentData.ToolType.WateringCan:
                    SwitchLandStatus(LandStatus.Watered);
                    break;

                case EquipmentData.ToolType.Shovel:

                    //Remove the crop from the land
                    if(cropPlanted != null)
                    {
                        Destroy(cropPlanted.gameObject);
                    }
                    break; 
            }

            //We don't need to check for seeds if we have already confirmed the tool to be an equipment
            return; 
        }

        //Try casting the itemdata in the toolslot as SeedData
        SeedData seedTool = toolSlot as SeedData; 

        ///Conditions for the player to be able to plant a seed
        ///1: He is holding a tool of type SeedData
        ///2: The Land State must be either watered or farmland
        ///3. There isn't already a crop that has been planted
        if(seedTool != null && landStatus != LandStatus.Soil && cropPlanted == null)
        {
            //Instantiate the crop object parented to the land
            GameObject cropObject = Instantiate(cropPrefab, transform);
            //Move the crop object to the top of the land gameobject
            cropObject.transform.position = new Vector3(transform.position.x, 0, transform.position.z);

            //Access the CropBehaviour of the crop we're going to plant
            cropPlanted = cropObject.GetComponent<CropBehaviour>();
            //Plant it with the seed's information
            cropPlanted.Plant(seedTool);

        }
    }

That is all you need to get the shovel up and running:

Removing wilted crops with the shovel
Our shovel in action.

Conclusion

With that, crops can be regrown, wilt, and removed. If you are a Patreon supporter, you can download the project files for what we’ve done so far. To use the files, you will have to unzip the file (7-Zip can help you do that), and open the folder with Assets and ProjectSettings as a project using Unity.

Here is the final code for all the scripts we have worked with today:

SeedData.cs

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

[CreateAssetMenu(menuName ="Items/Seed")]
public class SeedData : ItemData
{
    //Time it takes before the seed matures into a crop
    public int daysToGrow;

    //The crop the seed will yield
    public ItemData cropToYield;

    //The seedling GameObject
    public GameObject seedling;

    [Header("Regrowable")]
    //Is the plant able to regrow the crop after being harvested?
    public bool regrowable;
    //Time taken before the plant yields another crop
    public int daysToRegrow;
}

RegrowableHarvestBehaviour.cs

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

public class RegrowableHarvestBehaviour : InteractableObject
{
    CropBehaviour parentCrop; 

    //Sets the parent crop
    public void SetParent(CropBehaviour parentCrop)
    {
        this.parentCrop = parentCrop;
    }

    public override void Pickup()
    {
        //Set the player's inventory to the item
        InventoryManager.Instance.equippedItem = item;
        //Update the changes in the scene
        InventoryManager.Instance.RenderHand();

        //Set the parent crop back to seedling to regrow it
        parentCrop.Regrow();

    }
}

InteractableObject.cs

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

public class InteractableObject : MonoBehaviour
{
    //The item information the GameObject is supposed to represent
    public ItemData item;

    public virtual void Pickup()
    {
        //Set the player's inventory to the item
        InventoryManager.Instance.equippedItem = item;
        //Update the changes in the scene
        InventoryManager.Instance.RenderHand();
        //Destroy this instance so as to not have multiple copies
        Destroy(gameObject); 
    }
}

CropBehaviour.cs

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

public class CropBehaviour : MonoBehaviour
{
    //Information on what the crop will grow into 
    SeedData seedToGrow;

    [Header("Stages of Life")]
    public GameObject seed;
    public GameObject wilted; 
    private GameObject seedling;
    private GameObject harvestable;
    

    //The growth points of the crop
    int growth;
    //How many growth points it takes before it becomes harvestable
    int maxGrowth;

    //The crop can stay alive for 48 hours without water before it dies
    int maxHealth = GameTimestamp.HoursToMinutes(48); 

    int health;

    public enum CropState
    {
        Seed, Seedling, Harvestable, Wilted
    }
    //The current stage in the crop's growth
    public CropState cropState;

    //Initialisation for the crop GameObject
    //Called when the player plants a seed
    public void Plant(SeedData seedToGrow)
    {
        //Save the seed information
        this.seedToGrow = seedToGrow;

        //Instantiate the seedling and harvestable GameObjects
        seedling = Instantiate(seedToGrow.seedling, transform);

        //Access the crop item data
        ItemData cropToYield = seedToGrow.cropToYield;

        //Instantiate the harvestable crop
        harvestable = Instantiate(cropToYield.gameModel, transform);

        //Convert Days To Grow into hours
        int hoursToGrow = GameTimestamp.DaysToHours(seedToGrow.daysToGrow);
        //Convert it to minutes
        maxGrowth = GameTimestamp.HoursToMinutes(hoursToGrow);

        //Check if it is regrowable
        if (seedToGrow.regrowable)
        {
            //Get the RegrowableHarvestBehaviour from the GameObject
            RegrowableHarvestBehaviour regrowableHarvest = harvestable.GetComponent<RegrowableHarvestBehaviour>();

            //Initialise the harvestable 
            regrowableHarvest.SetParent(this);
        }

        //Set the initial state to Seed
        SwitchState(CropState.Seed); 

    }

    //The crop will grow when watered
    public void Grow()
    {
        //Increase the growth point by 1
        growth++;

        //Restore the health of the plant when it is watered
        if(health < maxHealth)
        {
            health++;
        }

        //The seed will sprout into a seedling when the growth is at 50%
        if(growth >= maxGrowth / 2 && cropState == CropState.Seed)
        {
            SwitchState(CropState.Seedling); 
        }

        //Grow from seedling to harvestable
        if(growth >= maxGrowth && cropState == CropState.Seedling)
        {
            SwitchState(CropState.Harvestable);
        }
    }

    //The crop will progressively wither when the soil is dry 
    public void Wither()
    {
        health--;
        //If the health is below 0 and the crop has germinated, kill it
        if(health <= 0 && cropState != CropState.Seed)
        {
            SwitchState(CropState.Wilted);
        }
    }

    //Function to handle the state changes 
    void SwitchState(CropState stateToSwitch)
    {
        //Reset everything and set all GameObjects to inactive
        seed.SetActive(false);
        seedling.SetActive(false);
        harvestable.SetActive(false);
        wilted.SetActive(false);

        switch (stateToSwitch)
        {
            case CropState.Seed:
                //Enable the Seed GameObject
                seed.SetActive(true);
                break;
            case CropState.Seedling:
                //Enable the Seedling GameObject
                seedling.SetActive(true);

                //Give the seed health 
                health = maxHealth; 

                break;
            case CropState.Harvestable:
                //Enable the Harvestable GameObject
                harvestable.SetActive(true);

                //If the seed is not regrowable, detach the harvestable from this crop gameobject and destroy it. 
                if (!seedToGrow.regrowable)
                {
                    //Unparent it to the crop
                    harvestable.transform.parent = null;
                    Destroy(gameObject);
                }
                
                
                break;
            case CropState.Wilted:
                //Enable the wilted GameObject
                wilted.SetActive(true);
                break;
        }

        //Set the current crop state to the state we're switching to
        cropState = stateToSwitch; 
    }

    //Called when the player harvests a regrowable crop. Resets the state to seedling 
    public void Regrow()
    {
        //Reset the growth 
        //Get the regrowth time in hours
        int hoursToRegrow = GameTimestamp.DaysToHours(seedToGrow.daysToRegrow);
        growth = maxGrowth - GameTimestamp.HoursToMinutes(hoursToRegrow);

        //Switch the state back to seedling
        SwitchState(CropState.Seedling); 
    }
}

Land.cs

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

public class Land : MonoBehaviour, ITimeTracker
{
    public enum LandStatus
    {
        Soil, Farmland, Watered
    }

    public LandStatus landStatus;

    public Material soilMat, farmlandMat, wateredMat;
    new Renderer renderer;

    //The selection gameobject to enable when the player is selecting the land
    public GameObject select;

    //Cache the time the land was watered 
    GameTimestamp timeWatered;

    [Header("Crops")]
    //The crop prefab to instantiate
    public GameObject cropPrefab;

    //The crop currently planted on the land
    CropBehaviour cropPlanted = null;

    // Start is called before the first frame update
    void Start()
    {
        //Get the renderer component
        renderer = GetComponent<Renderer>();

        //Set the land to soil by default
        SwitchLandStatus(LandStatus.Soil);

        //Deselect the land by default
        Select(false);

        //Add this to TimeManager's Listener list
        TimeManager.Instance.RegisterTracker(this);
    }

    public void SwitchLandStatus(LandStatus statusToSwitch)
    {
        //Set land status accordingly
        landStatus = statusToSwitch;

        Material materialToSwitch = soilMat; 

        //Decide what material to switch to
        switch (statusToSwitch)
        {
            case LandStatus.Soil:
                //Switch to the soil material
                materialToSwitch = soilMat;
                break;
            case LandStatus.Farmland:
                //Switch to farmland material 
                materialToSwitch = farmlandMat;
                break;

            case LandStatus.Watered:
                //Switch to watered material
                materialToSwitch = wateredMat;

                //Cache the time it was watered
                timeWatered = TimeManager.Instance.GetGameTimestamp(); 
                break; 

        }

        //Get the renderer to apply the changes
        renderer.material = materialToSwitch; 
    }

    public void Select(bool toggle)
    {
        select.SetActive(toggle);
    }

    //When the player presses the interact button while selecting this land
    public void Interact()
    {
        //Check the player's tool slot
        ItemData toolSlot = InventoryManager.Instance.equippedTool;

        //If there's nothing equipped, return
        if (toolSlot == null)
        {
            return; 
        }

        //Try casting the itemdata in the toolslot as EquipmentData
        EquipmentData equipmentTool = toolSlot as EquipmentData; 

        //Check if it is of type EquipmentData 
        if(equipmentTool != null)
        {
            //Get the tool type
            EquipmentData.ToolType toolType = equipmentTool.toolType;

            switch (toolType)
            {
                case EquipmentData.ToolType.Hoe:
                    SwitchLandStatus(LandStatus.Farmland);
                    break;
                case EquipmentData.ToolType.WateringCan:
                    SwitchLandStatus(LandStatus.Watered);
                    break;

                case EquipmentData.ToolType.Shovel:

                    //Remove the crop from the land
                    if(cropPlanted != null)
                    {
                        Destroy(cropPlanted.gameObject);
                    }
                    break; 
            }

            //We don't need to check for seeds if we have already confirmed the tool to be an equipment
            return; 
        }

        //Try casting the itemdata in the toolslot as SeedData
        SeedData seedTool = toolSlot as SeedData; 

        ///Conditions for the player to be able to plant a seed
        ///1: He is holding a tool of type SeedData
        ///2: The Land State must be either watered or farmland
        ///3. There isn't already a crop that has been planted
        if(seedTool != null && landStatus != LandStatus.Soil && cropPlanted == null)
        {
            //Instantiate the crop object parented to the land
            GameObject cropObject = Instantiate(cropPrefab, transform);
            //Move the crop object to the top of the land gameobject
            cropObject.transform.position = new Vector3(transform.position.x, 0, transform.position.z);

            //Access the CropBehaviour of the crop we're going to plant
            cropPlanted = cropObject.GetComponent<CropBehaviour>();
            //Plant it with the seed's information
            cropPlanted.Plant(seedTool);

        }
    }

    public void ClockUpdate(GameTimestamp timestamp)
    {
        //Checked if 24 hours has passed since last watered
        if(landStatus == LandStatus.Watered)
        {
            //Hours since the land was watered
            int hoursElapsed = GameTimestamp.CompareTimestamps(timeWatered, timestamp);
            Debug.Log(hoursElapsed + " hours since this was watered");

            //Grow the planted crop, if any
            if(cropPlanted != null)
            {
                cropPlanted.Grow();
            }

            if(hoursElapsed > 24)
            {
                //Dry up (Switch back to farmland)
                SwitchLandStatus(LandStatus.Farmland);
            }
        }

        //Handle the wilting of the plant when the land is not watered
        if(landStatus != LandStatus.Watered && cropPlanted != null)
        {
            //If the crop has already germinated, start the withering
            if (cropPlanted.cropState != CropBehaviour.CropState.Seed)
            {
                cropPlanted.Wither();
            }
        }
    }
}

EquipmentData.cs

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

[CreateAssetMenu(menuName = "Items/Equipment")]
public class EquipmentData : ItemData
{
    public enum ToolType
    {
        Hoe, WateringCan, Axe, Pickaxe, Shovel
    }
    public ToolType toolType; 
 
}

Article continues after the advertisement:


There are 3 comments:

  1. Really thankful for this series thus far! I’m curious why something is happening though with my regrowables:
    -when I harvest the crop and don’t move I can deposit infinite amounts of crop into my inventory(though the scene item disappears from handpoint as expected). Doesn’t happen with destroyable crops nor the regrowables if I move after harvesting. I tried adding an equipped item = null after calling the renderHand() but that just makes the item stick to my hand as expected since it doesn’t know there’s something there to be removed. Curious if anyone else has this happen (we’re usually moving as we pick things up so I almost didn’t notice)?

    1. Hi Sabrina, looks like this bug is caused by the RegrowableHarvestBehaviour object still responding to player input despite being disabled. To fix this, add this to the top of the Pickup() in RegrowableHarvestBehaviour:
      if (!isActiveAndEnabled) return;

Leave a Reply

Your email address will not be published. Required fields are marked *

Note: You can use Markdown to format your comments.

This site uses Akismet to reduce spam. Learn how your comment data is processed.