Creating a Farming RPG in Unity - Part 11: Saving Farmland Data

Creating a Farming RPG (like Harvest Moon) in Unity — Part 11: Saving Farmland Data

Ever wanted to create a game like Harvest Moon in Unity? Check out Part 11 of our guide here, where we go through how to save our farmland’s data. You can also find Part 10 of our guide here, where we went through how to set up scene transitions.

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

Video authored by Jonathan Teo, edited and subtitled by Hsin Yi.

1. Bug fixes

Before starting on our agenda in this article, we need to first fix some bugs.

  1. Make tilling the land a requirement before watering it instead of being able to directly change the soil to watered with the watering can
  2. Make our seeds visible after planting them.
seed hidden in ground
The seed is hidden in the ground.

To fix the seed visibility, we just need to increase the y-value of its initial position; as that value controls the height of the GameObject. For the other issue, adding an if statement to check the LandStatus will fix the bug.

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.GetEquippedSlotItem(InventorySlot.InventoryType.Tool);

        //If there's nothing equipped, return
        if (!InventoryManager.Instance.SlotEquipped(InventorySlot.InventoryType.Tool))
        {
            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:
                    //The land must be tilled first
                    if (landStatus != LandStatus.Soil)
                    {
                        SwitchLandStatus(LandStatus.Watered);
                    }
                    
                    break;

                case EquipmentData.ToolType.Shovel:

                    //Remove the crop from the land
                    if(cropPlanted != null)
                    {
                        cropPlanted.RemoveCrop();
                    }
                    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 0.1f, 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);

            //Consume the item
            InventoryManager.Instance.ConsumeItem(InventoryManager.Instance.GetEquippedSlot(InventorySlot.InventoryType.Tool));

        }
    }

With those changes, the player can’t water before tilling it and the seeds planted are visible.

seed visible above ground

Another thing to fix is the To Town Transform. Change the y value of the parent’s Transform to 0, and the y value of the returning point Transform to 1.

Changing starting point Transform
Top: Parent Transform, Bottom: StartingPoint Transform

2. Tracking Land information

a. Registering land plots

Now, we can start saving our farmlands data. Currently, everything in the farm resets whenever we leave the scene. Thus, we need to make the state of our farm persistent.

First, create a new class called LandManager in the Farming folder (in Assets/Scripts/Farming). Add the following code to LandManager to make it a static singleton class.

LandManager.cs

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

public class LandManager : MonoBehaviour
{
    public static LandManager Instance { get; private set; } 

    private void Awake()
    {
        //If there is more than one instance, destroy the extra
        if (Instance != null && Instance != this)
        {
            Destroy(this);
        }
        else
        {
            //Set the static instance to this instance
            Instance = this;
        }
    }


    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

Then, place it in the Farming Area GameObject.

add LamdManager to Farming Area

We are going to use this script to keep track of the state changes in the farm. Since we had placed this in Farming Area, the parent of all the Land GameObjects, we can use a foreach loop to get all of the children and register them. Thus, add the following function and call it in Start():

LandManager.cs

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

public class LandManager : MonoBehaviour
{
    public static LandManager Instance { get; private set; }

    List<Land> landPlots = new List<Land>();

    private void Awake()
    {
        //If there is more than one instance, destroy the extra
        if (Instance != null && Instance != this)
        {
            Destroy(this);
        }
        else
        {
            //Set the static instance to this instance
            Instance = this;
        }
    }


    void Start()
    {
        RegisterLandPlots();  
    }

    //Get all the Land Objects in the scene and cache it
    void RegisterLandPlots()
    {
        foreach(Transform landTransform in transform)
        {
            Land land = landTransform.GetComponent<Land>();
            landPlots.Add(land);
        }
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

Next, we also need the Land GameObjects to keep track of its own id. This will allow us to reference it easily in the future if we need it.

Land.cs

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

public class Land : MonoBehaviour, ITimeTracker
{
    public int id; 
    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()
    {
        ...
    }

    public void SwitchLandStatus(LandStatus statusToSwitch)
    {
        ...
    }

    public void Select(bool toggle)
    {
        ...
    }

    //When the player presses the interact button while selecting this land
    public void Interact()
    {
        ...
    }

    public void ClockUpdate(GameTimestamp timestamp)
    {
        ...
    }
}

To set the id, assign each land a number based on its index. Do this in LandManager.

LandManager.cs

    //Get all the Land Objects in the scene and cache it
    void RegisterLandPlots()
    {
        foreach(Transform landTransform in transform)
        {
            Land land = landTransform.GetComponent<Land>();
            landPlots.Add(land);

            //Assign it an id based on its index
            land.id = landPlots.Count - 1; 
        }
    }

Example for setting landID: when the first Land Instance is added, the id would be zero in the array; as landPlot.Count is 1 at that time (landPlots.Count – 1). Likewise, for the second instance, you will get an increasing number for the id of 1 and counting.


Article continues after the advertisement:


b. Serializing Land and Crop information

The next thing we need to do is to represent our Land instances in a serializable format. Reason being all Land instances are destroyed whenever the player leaves the scene, leading us to be unable to work with them. Another reason is that when we want to create a save system in the future, having the Land instances in a serializable format makes it easier to write to disk.

We are going to start working on the land first. Create a folder called “Save” (i.e. Assets/Scripts/Save) and create a new class called “LandSaveState” in it.

Create Save folder and script

Open the class and make it a serializable struct. We are only going to keep 2 pieces of information in it: LandStatus and the last time it was watered; as everything else can be generated based on these two fields. Then, create a constructor with the fields.

LandSaveState.cs

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

[System.Serializable]
public class struct LandSaveState : MonoBehaviour
{
    public Land.LandStatus landStatus;
    public GameTimestamp lastWatered;

    public LandSaveState(Land.LandStatus landStatus, GameTimestamp lastWatered)
    {
        this.landStatus = landStatus;
        this.lastWatered = lastWatered;
    }
}

We also need to make a struct for the crops as well, because we want to save the state of each plot of land and the crops planted on it. Thus, create a new script called “CropSaveState” in the Save folder.

Similar to LandSaveState, make this a serializable struct. However, the saved variables are different. The variables we are going to save are:

  1. landID (to know which land the crop is planted on)
  2. the seed that it is growing on the land
  3. current growth of the crop
  4. stage of growth (CropState)
  5. health (to check for wilting)

Just like before, everything else can be calculated from these. Thus, add these variables and create a constructor in CropSaveState.

CropSaveState.cs

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

[System.Serializable]
public class struct CropSaveState : MonoBehaviour
{
    //The index of the land the crop is planted on 
    public int landID;

    public string seedToGrow;
    public CropBehaviour.CropState cropState;
    public int growth;
    public int health;

    public CropSaveState(int landID, string seedToGrow, CropBehaviour.CropState cropState, int growth, int health)
    {
        this.landID = landID;
        this.seedToGrow = seedToGrow;
        this.cropState = cropState;
        this.growth = growth;
        this.health = health;
    }
}

Notice that we are saving seedToGrow as a string instead of a ScriptableObject. This is to save it in an easier reference if we want to write it to disk.

c. Updating Land information

Now that we have our LandSaveState and CropSaveState, declare them in LandManager. At the same time, for every new part of land that is registered, create its counterpart in serializable format.

LandManager.cs

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

public class LandManager : MonoBehaviour
{
    public static LandManager Instance { get; private set; } 

    List<Land> landPlots = new List<Land>();

    //The save states of our land and crops
    List<LandSaveState> landData = new List<LandSaveState>();
    List<CropSaveState> cropData = new List<CropSaveState>(); 

    private void Awake()
    {
        ...
    }


    void Start()
    {
        RegisterLandPlots();       
    }

    //Get all the Land Objects in the scene and cache it
    void RegisterLandPlots()
    {
        foreach(Transform landTransform in transform)
        {
            Land land = landTransform.GetComponent<Land>();
            landPlots.Add(land);

            //Create a corresponding LandSaveState
            landData.Add(new LandSaveState()); 

            //Assign it an id based on its index
            land.id = landPlots.Count - 1; 
        }
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

Next, the Land Instance needs to inform the LandManager of any changes made to it. To do that, create a new function that updates the correct land (based on landID) with the LandStatus and the last time it was watered.

LandManager.cs

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

public class LandManager : MonoBehaviour
{
    public static LandManager Instance { get; private set; } 

    List<Land> landPlots = new List<Land>();

    //The save states of our land and crops
    List<LandSaveState> landData = new List<LandSaveState>();
    List<CropSaveState> cropData = new List<CropSaveState>(); 

    private void Awake()
    {
        ...
    }


    void Start()
    {
        RegisterLandPlots();        
    }

    //Get all the Land Objects in the scene and cache it
    void RegisterLandPlots()
    {
        ...
    }

    //Update the corresponding Land Data on ever change to the Land's state
    public void OnLandStateChange(int id, Land.LandStatus landStatus, GameTimestamp lastWatered)
    {
        landData[id] = new LandSaveState(landStatus, lastWatered);
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

As mentioned before, this function needs to be called wherever there is a change to the Land Instance. Luckily, all the changes that can happen to Land Instance (changes in LandStatus and time watered) are all handled in SwichLandStatus(). Thus, we can call OnLandStateChange() in this function with landID, LandStatus and timeWatered as parameters.


Article continues after the advertisement:


Land.cs

    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;

        LandManager.Instance.OnLandStateChange(id, landStatus, timeWatered);
    }

When you test it out, there should be 432 land plots and 432 landID. It should also correspond accordingly when you change the state of the land.

landPlots landID LandState
Left to right: Land plots, land data, and different LandStatus based on what you have on the scene.

3. Saving Land information

Moving on, we are going to handle the saving of the data. We are using a static variable for this; because static variables operate independent of their instance. This means that even when LandManager does not exist in the scene, it is still accessible.

Since we need to save both LandSaveState and CropSaveState, we will store it in one static variable called “farmData” in the form of a Tuple to make things easier.

Tuple is a versatile, lightweight data container that can store multiple data elements. In this case, we are using System Tuple under the “System” namespace, because it can be nullable. This way, we do not need to set up a class to make the variable null.

In the Tuple, we are taking in the list of the LandSaveState and CropSaveState as type 1 and type 2 respectively, with the default value being null. This is important because we need to check if farmData is null before loading any saved data. Thus, add the following code to LandManager:

LandManager.cs

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

public class LandManager : MonoBehaviour
{
    public static LandManager Instance { get; private set; }

    public static Tuple<List<LandSaveState>, List<CropSaveState>> farmData = null; 

    List<Land> landPlots = new List<Land>();

    //The save states of our land and crops
    List<LandSaveState> landData = new List<LandSaveState>();
    List<CropSaveState> cropData = new List<CropSaveState>(); 

    private void Awake()
    {
        ...
    }


    void Start()
    {
        RegisterLandPlots();
        if (farmData != null)
        {
            //Load in any saved data 
        }       
    }

    //Get all the Land Objects in the scene and cache it
    void RegisterLandPlots()
    {
        ...
    }

    //Update the corresponding Land Data on ever change to the Land's state
    public void OnLandStateChange(int id, Land.LandStatus landStatus, GameTimestamp lastWatered)
    {
        landData[id] = new LandSaveState(landStatus, lastWatered);
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

Now, we face a question: when do we save the changes to farmData? 

There are 2 options:

  1. Save the changes immediately to farmData whenever the LandState changes
  2. Save the changes when the player leaves the scene

We will be going with option 2. The main reason for this is landSaveState and cropSaveState will save the changes made to Instance while the player is on the scene, so there would not be any issues when we write them to farmData when the player is somewhere else; nor will there be issues when writing the data back to the save states once the player goes back to the Farm scene. 

Another reason is SwichLandStatus() is called in Start() when we register the land, making it messy if we have to continuously save it to farmData [option 1].

Thus, add the code to OnDestroy() since the manager will be always destroyed when the player leaves the scene.

LandManager.cs

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

public class LandManager : MonoBehaviour
{
    public static LandManager Instance { get; private set; }

    public static Tuple<List<LandSaveState>, List<CropSaveState>> farmData = null; 

    List<Land> landPlots = new List<Land>();

    //The save states of our land and crops
    List<LandSaveState> landData = new List<LandSaveState>();
    List<CropSaveState> cropData = new List<CropSaveState>(); 

    private void Awake()
    {
        ...
    }


    void Start()
    {
        ...      
    }

    private void OnDestroy()
    {
        //Save the Instance variables over to the static variable
        farmData = new Tuple<List<LandSaveState>, List<CropSaveState>>(landData, cropData);        
    }

    //Get all the Land Objects in the scene and cache it
    void RegisterLandPlots()
    {
        ...
    }

    //Update the corresponding Land Data on ever change to the Land's state
    public void OnLandStateChange(int id, Land.LandStatus landStatus, GameTimestamp lastWatered)
    {
        landData[id] = new LandSaveState(landStatus, lastWatered);
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

With that, we have saved our land and crop data.


Article continues after the advertisement:


4. Loading Land information

a. Set up data import functions, load land data

Now, we will handle the loading of the data.

First, we will create a function to load the land data by taking in everything in the serializable format and convert and implement it into the scene. Add it to Land:

Land.cs

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

public class Land : MonoBehaviour, ITimeTracker
{
    public int id; 
    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()
    {
        ...
    }

    public void LoadLandData(LandStatus statusToSwitch, GameTimestamp lastWatered)
    {
        //Set land status accordingly
        landStatus = statusToSwitch;
        timeWatered = lastWatered;
    }

    public void SwitchLandStatus(LandStatus statusToSwitch)
    {
        ...
    }

    public void Select(bool toggle)
    {
        ...
    }

    //When the player presses the interact button while selecting this land
    public void Interact()
    {
        ...
    }

    public void ClockUpdate(GameTimestamp timestamp)
    {
        ...
    }
}

However, we need to switch the land correctly in the scene as well. Thus, copy and paste the highlighted code from SwitchLandStatus() to LoadLandData().

Land.cs

    public void LoadLandData(LandStatus statusToSwitch, GameTimestamp lastWatered)
    {
        //Set land status accordingly
        landStatus = statusToSwitch;
        timeWatered = lastWatered;

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

        }

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

    }

The only difference between these 2 functions is that LoadLandData() only renders the soil, while SwitchLandStatus() both renders the soil and records data.

With the loading function set up, we now need another function to import the land data. It will take in the list of LandSaveState that we had saved in the Tuple and set its data to the instance of the land using a foreach loop. Add this function to LandManager.

LandManager.cs

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

public class LandManager : MonoBehaviour
{
    public static LandManager Instance { get; private set; }

    public static Tuple<List<LandSaveState>, List<CropSaveState>> farmData = null; 

    List<Land> landPlots = new List<Land>();

    //The save states of our land and crops
    List<LandSaveState> landData = new List<LandSaveState>();
    List<CropSaveState> cropData = new List<CropSaveState>(); 

    private void Awake()
    {
        ...
    }


    void Start()
    {
        ...       
    }

    private void OnDestroy()
    {
        ...
    }

    //Get all the Land Objects in the scene and cache it
    void RegisterLandPlots()
    {
        ...
    }

    //Update the corresponding Land Data on ever change to the Land's state
    public void OnLandStateChange(int id, Land.LandStatus landStatus, GameTimestamp lastWatered)
    {
        ...
    }

    //Load over the static farmData onto the Instance's landData
    public void ImportLandData(List<LandSaveState> landDatasetToLoad)
    {
        for(int i =0; i < landDatasetToLoad.Count; i++)
        {
            //Get the individual land save state
            LandSaveState landDataToLoad = landDatasetToLoad[i];
            //Load it up onto the Land instance
            landPlots[i].LoadLandData(landDataToLoad.landStatus, landDataToLoad.lastWatered);
            
        }

        landData = landDatasetToLoad; 
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

Similarly, do the same for the crops.

LandManager.cs

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

public class LandManager : MonoBehaviour
{
    public static LandManager Instance { get; private set; }

    public static Tuple<List<LandSaveState>, List<CropSaveState>> farmData = null; 

    List<Land> landPlots = new List<Land>();

    //The save states of our land and crops
    List<LandSaveState> landData = new List<LandSaveState>();
    List<CropSaveState> cropData = new List<CropSaveState>(); 

    private void Awake()
    {
        ...
    }


    void Start()
    {
        ...       
    }

    private void OnDestroy()
    {
        ...
    }

    //Get all the Land Objects in the scene and cache it
    void RegisterLandPlots()
    {
        ...
    }

    //Update the corresponding Land Data on ever change to the Land's state
    public void OnLandStateChange(int id, Land.LandStatus landStatus, GameTimestamp lastWatered)
    {
        ...
    }

    //Load over the static farmData onto the Instance's landData
    public void ImportLandData(List<LandSaveState> landDatasetToLoad)
    {
        ...
    }

    //Load over the static farmData onto the Instance's cropData
    public void ImportCropData(List<CropSaveState> cropDatasetToLoad)
    {
        cropData = cropDatasetToLoad;      
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

Lastly, in Start(), load the farmData by calling the import function.

LandManager.cs

    void Start()
    {
        RegisterLandPlots();

        //Load farm data if any
        if (farmData != null)
        {
            //Load in any saved data 
            ImportLandData(farmData.Item1);
            ImportCropData(farmData.Item2);          
        }      
    }

If you are wondering why LandSaveState is Item1 and CropSaveState is Item2, it is because it is following the argument order in the farmData Tuple, where we had placed LandSaveState as the first argument and CropSaveState as the second argument.

b. Delay the data import

Even though our import is set up correctly, there will still be errors. This is due to LandManager trying to import the farm data before Land Instance has finished calling its Start() function to set up the land.

To fix this, we need to make a Coroutine to delay the calling of LandManager‘s Start() function until Land Instance has completed setting up. Remember to leave RegisterLandPlots() out of the coroutine because it needs to be registered before the land does its first update.

LandManager.cs

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

public class LandManager : MonoBehaviour
{
    public static LandManager Instance { get; private set; }

    public static Tuple<List<LandSaveState>, List<CropSaveState>> farmData = null; 

    List<Land> landPlots = new List<Land>();

    //The save states of our land and crops
    List<LandSaveState> landData = new List<LandSaveState>();
    List<CropSaveState> cropData = new List<CropSaveState>(); 

    private void Awake()
    {
        ...
    }


    void Start OnEnable()
    {
        RegisterLandPlots();
        StartCoroutine(LoadFarmData());
        //Load farm data if any
        if (farmData != null)
        {
            //Load in any saved data 
            ImportLandData(farmData.Item1);
            ImportCropData(farmData.Item2);
        }
    }

    IEnumerator LoadFarmData()
    {
        yield return new WaitForEndOfFrame();
        //Load farm data if any
        if (farmData != null)
        {
            //Load in any saved data 
            ImportLandData(farmData.Item1);
            ImportCropData(farmData.Item2);           
        }
    }

    private void OnDestroy()
    {
        ...
    }

    //Get all the Land Objects in the scene and cache it
    void RegisterLandPlots()
    {
        ...
    }

    //Update the corresponding Land Data on ever change to the Land's state
    public void OnLandStateChange(int id, Land.LandStatus landStatus, GameTimestamp lastWatered)
    {
        landData[id] = new LandSaveState(landStatus, lastWatered);
    }

    #region Loading Data
    //Load over the static farmData onto the Instance's landData
    public void ImportLandData(List<LandSaveState> landDatasetToLoad)
    {
        ...
    }

    //Load over the static farmData onto the Instance's cropData
    public void ImportCropData(List<CropSaveState> cropDatasetToLoad)
    {
        ...       
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

Now, each of the land should be preserved correctly.


Article continues after the advertisement:


c. Handling crop data

Next, we will load the cropData. Similar to landData and their functions, we need to register the crop and update its data whenever there are changes. To do that, create the functions called RegisterCrop() and OnCropStateChange() in LandManager.

LandManager.cs

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

public class LandManager : MonoBehaviour
{
    public static LandManager Instance { get; private set; }

    public static Tuple<List<LandSaveState>, List<CropSaveState>> farmData = null; 

    List<Land> landPlots = new List<Land>();

    //The save states of our land and crops
    List<LandSaveState> landData = new List<LandSaveState>();
    List<CropSaveState> cropData = new List<CropSaveState>(); 

    private void Awake()
    {
        ...
    }


    void OnEnable()
    {
        ...      
    }

    IEnumerator LoadFarmData()
    {
        ...
    }

    private void OnDestroy()
    {
        ...
    }

    //Get all the Land Objects in the scene and cache it
    void RegisterLandPlots()
    {
        ...
    }

    //Registers the crop onto the Instance
    public void RegisterCrop(int landID, SeedData seedToGrow, CropBehaviour.CropState cropState, int growth, int health)
    {
        cropData.Add(new CropSaveState(landID, seedToGrow.name, cropState, growth, health));
    }

    //Update the corresponding Land Data on ever change to the Land's state
    public void OnLandStateChange(int id, Land.LandStatus landStatus, GameTimestamp lastWatered)
    {
        ...
    }

    //Update the corresponding Crop Data on ever change to the Land's state
    public void OnCropStateChange(int landID, SeedData seedToGrow, CropBehaviour.CropState cropState, int growth, int health)
    {
        
    }

    //Load over the static farmData onto the Instance's landData
    public void ImportLandData(List<LandSaveState> landDatasetToLoad)
    {
        ... 
    }

    //Load over the static farmData onto the Instance's cropData
    public void ImportCropData(List<CropSaveState> cropDatasetToLoad)
    {
        ...       
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

However, crops are different from the land plots as they can be harvested and destroyed. This means we also require a deregister function to find and remove the crop from a matching land( using landID).

LandManager.cs

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

public class LandManager : MonoBehaviour
{
    public static LandManager Instance { get; private set; }

    public static Tuple<List<LandSaveState>, List<CropSaveState>> farmData = null; 

    List<Land> landPlots = new List<Land>();

    //The save states of our land and crops
    List<LandSaveState> landData = new List<LandSaveState>();
    List<CropSaveState> cropData = new List<CropSaveState>(); 

    private void Awake()
    {
        ...
    }


    void OnEnable()
    {
        ...       
    }

    IEnumerator LoadFarmData()
    {
        ...
    }

    private void OnDestroy()
    {
        ...
    }

    //Get all the Land Objects in the scene and cache it
    void RegisterLandPlots()
    {
        ...
    }

    //Registers the crop onto the Instance
    public void RegisterCrop(int landID, SeedData seedToGrow, CropBehaviour.CropState cropState, int growth, int health)
    {
        ...
    }

    public void DeregisterCrop(int landID)
    {
        //Find its index in the list from the landID and remove it
        cropData.RemoveAll(x => x.landID == landID); 
    }

    //Update the corresponding Land Data on ever change to the Land's state
    public void OnLandStateChange(int id, Land.LandStatus landStatus, GameTimestamp lastWatered)
    {
        ...
    }

    //Update the corresponding Crop Data on ever change to the Land's state
    public void OnCropStateChange(int landID, SeedData seedToGrow, CropBehaviour.CropState cropState, int growth, int health)
    {
        
    }

    //Load over the static farmData onto the Instance's landData
    public void ImportLandData(List<LandSaveState> landDatasetToLoad)
    {
        ...
    }

    //Load over the static farmData onto the Instance's cropData
    public void ImportCropData(List<CropSaveState> cropDatasetToLoad)
    {
        ...       
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

On the other hand, for OnCropStateChange(), we will be using FindIndex() to find and update the correct cropIndex with the seedToGrow.

Note that we removed SeedData from the parameter because once we have the cropIndex, we can get seedToGrow from the cropData. This is more efficient as we do not have to continuously pass the same SeedData through the different functions. Thus, make these changes to LandManager:

LandManager.cs

    //Update the corresponding Crop Data on ever change to the Land's state
    public void OnCropStateChange(int landID, SeedData seedToGrow, CropBehaviour.CropState cropState, int growth, int health)
    {
        //Find its index in the list from the landID
        int cropIndex = cropData.FindIndex(x => x.landID == landID);

        string seedToGrow = cropData[cropIndex].seedToGrow;
        cropData[cropIndex] = new CropSaveState(landID, seedToGrow, cropState, growth, health);
    }

d. Organise LandManager

Before moving on, let’s organize the code by adding in the regions for “state changes”, “Registering and Deregistering” and “Loading Data” to LandManager.

LandManager.cs

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

public class LandManager : MonoBehaviour
{
    public static LandManager Instance { get; private set; }

    public static Tuple<List<LandSaveState>, List<CropSaveState>> farmData = null; 

    List<Land> landPlots = new List<Land>();

    //The save states of our land and crops
    List<LandSaveState> landData = new List<LandSaveState>();
    List<CropSaveState> cropData = new List<CropSaveState>(); 

    private void Awake()
    {
        ...
    }


    void OnEnable()
    {
        ...
    }

    IEnumerator LoadFarmData()
    {
        ...
    }

    private void OnDestroy()
    {
        ...
    }

    #region Registering and Deregistering
    //Get all the Land Objects in the scene and cache it
    void RegisterLandPlots()
    {
        ...
    }

    //Registers the crop onto the Instance
    public void RegisterCrop(int landID, SeedData seedToGrow, CropBehaviour.CropState cropState, int growth, int health)
    {
        ...
    }

    public void DeregisterCrop(int landID)
    {
        ...
    }
    #endregion

    #region State Changes
    //Update the corresponding Land Data on ever change to the Land's state
    public void OnLandStateChange(int id, Land.LandStatus landStatus, GameTimestamp lastWatered)
    {
        ...
    }

    //Update the corresponding Crop Data on ever change to the Land's state
    public void OnCropStateChange(int landID, CropBehaviour.CropState cropState, int growth, int health)
    {
        ...
    }
    #endregion

    #region Loading Data
    //Load over the static farmData onto the Instance's landData
    public void ImportLandData(List<LandSaveState> landDatasetToLoad)
    {
        ... 
    }

    //Load over the static farmData onto the Instance's cropData
    public void ImportCropData(List<CropSaveState> cropDatasetToLoad)
    {
        ...
    }
    #endregion

    // Update is called once per frame
    void Update()
    {
        
    }
}

e. Implement Crop functions

Moving on to CropBehaviour, the first thing we need to do is to register our crops whenever we plant them. Thus, under Plant(), add the following code:

CropBehaviour.cs

    //Initialisation for the crop GameObject
    //Called when the player plants a seed
    public void Plant(int landID,SeedData seedToGrow)
    {
        this.landID = landID;
        //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);

        //Set the growth and health accordingly
        this.growth = growth;
        this.health = health; 

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

        LandManager.Instance.RegisterCrop(landID, seedToGrow, cropState, growth, health); 

    }

As we had added the id parameter, we also need to make the following changes to the Land script.

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.GetEquippedSlotItem(InventorySlot.InventoryType.Tool);

        //If there's nothing equipped, return
        if (!InventoryManager.Instance.SlotEquipped(InventorySlot.InventoryType.Tool))
        {
            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:
                    //The land must be tilled first
                    if (landStatus != LandStatus.Soil)
                    {
                        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.1f, 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(id, seedTool);

            //Consume the item
            InventoryManager.Instance.ConsumeItem(InventoryManager.Instance.GetEquippedSlot(InventorySlot.InventoryType.Tool));

        }
    }

Next, we want to instantiate and load the crop onto the land. Most of the code to do that is already in the Plant() function, except we do not want to re-register the crop. Thus, what we can do instead is to create a new function called LoadCrop() with mostly the same parameters as CropSaveState [you can copy the parameters from there and amend them]. Then, shift everything from Plant() to the new function [except for RegisterCrop()].

CropBehaviour.cs

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

public class CropBehaviour : MonoBehaviour
{
    //The ID of the land the crop belongs to
    int landID;

    //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(int landID ,SeedData seedToGrow)
    {
        this.landID = landID;
        //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);

        //Set the growth and health accordingly
        this.growth = growth;
        this.health = health; 

        //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);
        LandManager.Instance.RegisterCrop(landID, seedToGrow, cropState, growth, health); 

    }

    public void LoadCrop(int landID, SeedData seedToGrow, CropState cropState, int growth, int health)
    {
        this.landID = landID;
        //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);

    }

    //The crop will grow when watered
    public void Grow()
    {
        ...
    }

    //The crop will progressively wither when the soil is dry 
    public void Wither()
    {
        ...
    }

    //Function to handle the state changes 
    void SwitchState(CropState stateToSwitch)
    {
        ...
    }

    //Called when the player harvests a regrowable crop. Resets the state to seedling 
    public void Regrow()
    {
        ... 
    }

}

We also need to add the growth and health of the crop.


Article continues after the advertisement:


CropBehaviour.cs

    public void LoadCrop(int landID, SeedData seedToGrow, CropState cropState, int growth, int health)
    {
        this.landID = landID;
        //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);

        //Set the growth and health accordingly
        this.growth = growth;
        this.health = health; 

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

    }

Back in Plant(), we will use LoadCrop() instead. Growth and health are 0 as they are not required when it is first planted since the health will only start when it reaches the Seedling stage.

CropBehaviour.cs

    public void Plant(int landID ,SeedData seedToGrow)
    {
        LoadCrop(landID, seedToGrow, CropState.Seed, 0, 0);
        LandManager.Instance.RegisterCrop(landID, seedToGrow, cropState, growth, health); 

    }

Next, we need to call the update function wherever the crop state changes, which only happens when it grows or withers. This means we only need to call LandManager.Instance.OnCropStateChange() in Wither() and Grow(). Add the line to the 2 functions:

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

        //Inform LandManager on the changes
        LandManager.Instance.OnCropStateChange(landID, cropState, growth, health);
    }

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

        //Inform LandManager on the changes
        LandManager.Instance.OnCropStateChange(landID, cropState, growth, health);
    }

As for the Deregister() function, call it when the crop is destroyed.

CropBehaviour.cs

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

public class CropBehaviour : MonoBehaviour
{
    //The ID of the land the crop belongs to
    int landID;

    //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(int landID ,SeedData seedToGrow)
    {
       ...
    }

    public void LoadCrop(int landID, SeedData seedToGrow, CropState cropState, int growth, int health)
    {
        ...
    }

    //The crop will grow when watered
    public void Grow()
    {
        ...
    }

    //The crop will progressively wither when the soil is dry 
    public void Wither()
    {
        ...
    }

    //Function to handle the state changes 
    void SwitchState(CropState stateToSwitch)
    {
        ...
    }

    //Called when the player harvests a regrowable crop. Resets the state to seedling 
    public void Regrow()
    {
        ...
    }

    public void OnDestroy()
    {
        LandManager.Instance.DeregisterCrop(landID);
    }

}

When you test it, you should be able to see the growth and the health of the plant.

crop health and growth

At the same time, when you harvest the crop, it should be removed.

remove crop

f. Loading crop data

The last thing we need to do is loading cropData. However, there is a problem: we had stored our seedToGrow as a string instead of a ScriptableObject.

To overcome this issue, we will convert the string back to a ScriptableObject using a new script called “ItemIndex”. Create this script in the Inventory folder [Assets/Scripts/Inventory].

This script will be a ScriptableObject that contains a list that holds all the items and a function to get the ScriptableObject by using its name (in the form of a string).

ItemIndex.cs

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

[CreateAssetMenu(menuName = "Items/Item Index")]
public class ItemIndex : MonoBehaviour ScriptableObject
{
    public List<ItemData> items; 

    public ItemData GetItemFromString(string name)
    {
        return items.Find(i => i.name == name); 
    }
}

Back in unity, create a new ItemIndex in the Data folder [Assets/Data]. Name it “Item List” and add every single item from the Tools, Seeds and Item folders into it.

item list items

Then, go to InventoryManager and add a reference to the Item List.

InventoryManager.cs

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

public class InventoryManager : MonoBehaviour
{
    public static InventoryManager Instance { get; private set; }

    private void Awake()
    {
        ...
    }

    //The full list of items 
    public ItemIndex itemIndex; 

    [Header("Tools")]
    //Tool Slots
    [SerializeField]
    private ItemSlotData[] toolSlots = new ItemSlotData[8];
    //Tool in the player's hand
    [SerializeField]
    private ItemSlotData equippedToolSlot = null; 

    [Header("Items")]
    //Item Slots
    [SerializeField]
    private ItemSlotData[] itemSlots = new ItemSlotData[8];
    //Item in the player's hand
    [SerializeField]
    private ItemSlotData equippedItemSlot = null;

    //The transform for the player to hold items in the scene
    public Transform handPoint; 

    //Equipping

    //Handles movement of item from Inventory to Hand
    public void InventoryToHand(int slotIndex, InventorySlot.InventoryType inventoryType)
    {
        ...
    }

    //Handles movement of item from Hand to Inventory
    public void HandToInventory(InventorySlot.InventoryType inventoryType)
    {
        ...
    }

    //Iterate through each of the items in the inventory to see if it can be stacked
    //Will perform the operation if found, returns false if unsuccessful
    public bool StackItemToInventory(ItemSlotData itemSlot, ItemSlotData[] inventoryArray)
    {
       ... 
    }

    //Render the player's equipped item in the scene
    public void RenderHand()
    {
        ...        
    }

    //Inventory Slot Data 
    #region Gets and Checks
    ...
    #endregion

    //Equip the hand slot with an ItemData (Will overwrite the slot)
    public void EquipHandSlot(ItemData item)
    {
        ...
    }

    //Equip the hand slot with an ItemSlotData (Will overwrite the slot)
    public void EquipHandSlot(ItemSlotData itemSlot)
    {
        ...
    }

    public void ConsumeItem(ItemSlotData itemSlot)
    {
        ... 
    }


    #region Inventory Slot Validation
    ...
    #endregion

    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

Add the ItemIndex to the Manager GameObject.

add ItemIndex to Manager

Going back to LandManager, we need to do 3 things to import the the cropData:

  1. Access the land
  2. Spawn the crop
  3. Load the crop

The first point is simple to complete. We just need to get the landPlot using the id that the crop has.

LandManager.cs

    //Load over the static farmData onto the Instance's cropData
    public void ImportCropData(List<CropSaveState> cropDatasetToLoad)
    {
        cropData = cropDatasetToLoad;
        foreach (CropSaveState cropSave in cropDatasetToLoad)
        {
            //Access the land
            Land landToPlant = landPlots[cropSave.landID];
            //Spawn the crop
            //Load in the data
        }
        
    }

Article continues after the advertisement:


Then over in our Land script, we want to split our spawn crop code into a function, so that it can be used by both Land and LandManager. Currently, the method we are using to spawn the seed is by instantiating the object before setting its transform and behaviour, so we need to return a CropBehaviour for our new function to be able to set the behaviour. Thus, make the following changes to Land:

Land.cs

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

public class Land : MonoBehaviour, ITimeTracker
{
    public int id; 
    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()
    {
        ...
    }

    public void LoadLandData(LandStatus statusToSwitch, GameTimestamp lastWatered)
    {
        ...
    }

    public void SwitchLandStatus(LandStatus statusToSwitch)
    {
        ...
    }

    public void Select(bool 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.GetEquippedSlotItem(InventorySlot.InventoryType.Tool);

        //If there's nothing equipped, return
        if (!InventoryManager.Instance.SlotEquipped(InventorySlot.InventoryType.Tool))
        {
            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:
                    //The land must be tilled first
                    if (landStatus != LandStatus.Soil)
                    {
                        SwitchLandStatus(LandStatus.Watered);
                    }
                    
                    break;

                case EquipmentData.ToolType.Shovel:

                    //Remove the crop from the land
                    if(cropPlanted != null)
                    {
                        cropPlanted.RemoveCrop();
                    }
                    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.1f, transform.position.z);

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

            //Consume the item
            InventoryManager.Instance.ConsumeItem(InventoryManager.Instance.GetEquippedSlot(InventorySlot.InventoryType.Tool));

        }
    }

    public CropBehaviour SpawnCrop()
    {
        //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.1f, transform.position.z);

        //Access the CropBehaviour of the crop we're going to plant
        cropPlanted = cropObject.GetComponent<CropBehaviour>();
        return cropPlanted; 
    }

    public void ClockUpdate(GameTimestamp timestamp)
    {
        ...
    }
}

Back again in LandManager, we can use the function we just created to plant the crop. The CropBehaviour returned can then be used for point 3, which is loading the crop using LoadCrop().

LandManager.cs

    //Load over the static farmData onto the Instance's cropData
    public void ImportCropData(List<CropSaveState> cropDatasetToLoad)
    {
        cropData = cropDatasetToLoad;
        foreach (CropSaveState cropSave in cropDatasetToLoad)
        {
            //Access the land
            Land landToPlant = landPlots[cropSave.landID];
            //Spawn the crop
            CropBehaviour cropToPlant = landToPlant.SpawnCrop();
            Debug.Log(cropToPlant.gameObject); 
            //Load in the data
            SeedData seedToGrow = (SeedData) InventoryManager.Instance.itemIndex.GetItemFromString(cropSave.seedToGrow);
            cropToPlant.LoadCrop(cropSave.landID, seedToGrow, cropSave.cropState, cropSave.growth, cropSave.health); 
        }
        
    }

g. Fix crop loading bug

Currently, if you test the code, you would see that landData has loaded properly but cropData did not. This is due to the crops getting deregistered everytime the player leaves the scene because we had put the Deregister() function in OnDestroy()

To fix this, we need to find the situations where the crop is deregistered and place it there instead. They are:

  1. When we harvest the crop
  2. When we destroy the crop using a shovel

Since there are 2 situations, we are going to create another function to deregister the crop and destroy the GameObject instead. Thus, add the following code to CropBehaviour:

CropBehaviour.cs

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

public class CropBehaviour : MonoBehaviour
{
    //The ID of the land the crop belongs to
    int landID;

    //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(int landID ,SeedData seedToGrow)
    {
        LoadCrop(landID, seedToGrow, CropState.Seed, 0, 0);
        LandManager.Instance.RegisterCrop(landID, seedToGrow, cropState, growth, health); 

    }

    public void LoadCrop(int landID, SeedData seedToGrow, CropState cropState, int growth, int health)
    {
        this.landID = landID;
        //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);

        //Set the growth and health accordingly
        this.growth = growth;
        this.health = health; 

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

    }

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

        //Inform LandManager on the changes
        LandManager.Instance.OnCropStateChange(landID, cropState, growth, health);
    }

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

        //Inform LandManager on the changes
        LandManager.Instance.OnCropStateChange(landID, cropState, growth, health);
    }

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

    //Destroys and Deregisters the Crop
    public void RemoveCrop()
    {
        LandManager.Instance.DeregisterCrop(landID);
        Destroy(gameObject);
    }

    //Called when the player harvests a regrowable crop. Resets the state to seedling 
    public void Regrow()
    {
       ... 
    }

    public void OnDestroy()
    {
        LandManager.Instance.DeregisterCrop(landID);
    }
}

Apply this function in the Land script for the shovel too.

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.GetEquippedSlotItem(InventorySlot.InventoryType.Tool);

        //If there's nothing equipped, return
        if (!InventoryManager.Instance.SlotEquipped(InventorySlot.InventoryType.Tool))
        {
            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:
                    //The land must be tilled first
                    if (landStatus != LandStatus.Soil)
                    {
                        SwitchLandStatus(LandStatus.Watered);
                    }
                    
                    break;

                case EquipmentData.ToolType.Shovel:

                    //Remove the crop from the land
                    if(cropPlanted != null)
                    {
                        Destroy(cropPlanted.gameObject);
                        cropPlanted.RemoveCrop();
                    }
                    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)
        {
            SpawnCrop();
            //Plant it with the seed's information
            cropPlanted.Plant(id, seedTool);

            //Consume the item
            InventoryManager.Instance.ConsumeItem(InventoryManager.Instance.GetEquippedSlot(InventorySlot.InventoryType.Tool));

        }
    }

Article continues after the advertisement:


Conclusion

Today, we have made our farm data persistant by saving and loading its data.

If you are a Patreon supporter, you can download the project files for what we have 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:

CropBehaviour.cs

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

public class CropBehaviour : MonoBehaviour
{
    //The ID of the land the crop belongs to
    int landID;

    //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(int landID ,SeedData seedToGrow)
    {
        LoadCrop(landID, seedToGrow, CropState.Seed, 0, 0);
        LandManager.Instance.RegisterCrop(landID, seedToGrow, cropState, growth, health); 

    }

    public void LoadCrop(int landID, SeedData seedToGrow, CropState cropState, int growth, int health)
    {
        this.landID = landID;
        //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);

        //Set the growth and health accordingly
        this.growth = growth;
        this.health = health; 

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

    }

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

        //Inform LandManager on the changes
        LandManager.Instance.OnCropStateChange(landID, cropState, growth, health);
    }

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

        //Inform LandManager on the changes
        LandManager.Instance.OnCropStateChange(landID, cropState, growth, health);
    }

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

    //Destroys and Deregisters the Crop
    public void RemoveCrop()
    {
        LandManager.Instance.DeregisterCrop(landID);
        Destroy(gameObject);
    }

    //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 int id; 
    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 LoadLandData(LandStatus statusToSwitch, GameTimestamp lastWatered)
    {
        //Set land status accordingly
        landStatus = statusToSwitch;
        timeWatered = lastWatered;

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

        }

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

    }

    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;

        LandManager.Instance.OnLandStateChange(id, landStatus, timeWatered);
    }

    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.GetEquippedSlotItem(InventorySlot.InventoryType.Tool);

        //If there's nothing equipped, return
        if (!InventoryManager.Instance.SlotEquipped(InventorySlot.InventoryType.Tool))
        {
            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:
                    //The land must be tilled first
                    if (landStatus != LandStatus.Soil)
                    {
                        SwitchLandStatus(LandStatus.Watered);
                    }
                    
                    break;

                case EquipmentData.ToolType.Shovel:

                    //Remove the crop from the land
                    if(cropPlanted != null)
                    {
                        cropPlanted.RemoveCrop();
                    }
                    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)
        {
            SpawnCrop();
            //Plant it with the seed's information
            cropPlanted.Plant(id, seedTool);

            //Consume the item
            InventoryManager.Instance.ConsumeItem(InventoryManager.Instance.GetEquippedSlot(InventorySlot.InventoryType.Tool));

        }
    }

    public CropBehaviour SpawnCrop()
    {
        //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.1f, transform.position.z);

        //Access the CropBehaviour of the crop we're going to plant
        cropPlanted = cropObject.GetComponent<CropBehaviour>();
        return cropPlanted; 
    }

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

LandManager.cs

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

public class LandManager : MonoBehaviour
{
    public static LandManager Instance { get; private set; }

    public static Tuple<List<LandSaveState>, List<CropSaveState>> farmData = null; 

    List<Land> landPlots = new List<Land>();

    //The save states of our land and crops
    List<LandSaveState> landData = new List<LandSaveState>();
    List<CropSaveState> cropData = new List<CropSaveState>(); 

    private void Awake()
    {
        //If there is more than one instance, destroy the extra
        if (Instance != null && Instance != this)
        {
            Destroy(this);
        }
        else
        {
            //Set the static instance to this instance
            Instance = this;
        }
    }


    void OnEnable()
    {
        RegisterLandPlots();
        StartCoroutine(LoadFarmData());
        
    }

    IEnumerator LoadFarmData()
    {
        yield return new WaitForEndOfFrame();
        //Load farm data if any
        if (farmData != null)
        {
            //Load in any saved data 
            ImportLandData(farmData.Item1);
            ImportCropData(farmData.Item2);
            

        }
    }

    private void OnDestroy()
    {
        //Save the Instance variables over to the static variable
        farmData = new Tuple<List<LandSaveState>, List<CropSaveState>>(landData, cropData);
        cropData.ForEach((CropSaveState crop) => {
            Debug.Log(crop.seedToGrow);
        });
    }

    #region Registering and Deregistering
    //Get all the Land Objects in the scene and cache it
    void RegisterLandPlots()
    {
        foreach(Transform landTransform in transform)
        {
            Land land = landTransform.GetComponent<Land>();
            landPlots.Add(land);

            //Create a corresponding LandSaveState
            landData.Add(new LandSaveState()); 

            //Assign it an id based on its index
            land.id = landPlots.Count - 1; 
        }
    }

    //Registers the crop onto the Instance
    public void RegisterCrop(int landID, SeedData seedToGrow, CropBehaviour.CropState cropState, int growth, int health)
    {
        cropData.Add(new CropSaveState(landID, seedToGrow.name, cropState, growth, health));
    }

    public void DeregisterCrop(int landID)
    {
        //Find its index in the list from the landID and remove it
        cropData.RemoveAll(x => x.landID == landID); 
    }
    #endregion

    #region State Changes
    //Update the corresponding Land Data on ever change to the Land's state
    public void OnLandStateChange(int id, Land.LandStatus landStatus, GameTimestamp lastWatered)
    {
        landData[id] = new LandSaveState(landStatus, lastWatered);
    }

    //Update the corresponding Crop Data on ever change to the Land's state
    public void OnCropStateChange(int landID, CropBehaviour.CropState cropState, int growth, int health)
    {
        //Find its index in the list from the landID
        int cropIndex = cropData.FindIndex(x => x.landID == landID);

        string seedToGrow = cropData[cropIndex].seedToGrow;
        cropData[cropIndex] = new CropSaveState(landID, seedToGrow, cropState, growth, health);
    }
    #endregion

    #region Loading Data
    //Load over the static farmData onto the Instance's landData
    public void ImportLandData(List<LandSaveState> landDatasetToLoad)
    {
        for(int i =0; i < landDatasetToLoad.Count; i++)
        {
            //Get the individual land save state
            LandSaveState landDataToLoad = landDatasetToLoad[i];
            //Load it up onto the Land instance
            landPlots[i].LoadLandData(landDataToLoad.landStatus, landDataToLoad.lastWatered);
            
        }

        landData = landDatasetToLoad; 
    }

    //Load over the static farmData onto the Instance's cropData
    public void ImportCropData(List<CropSaveState> cropDatasetToLoad)
    {
        cropData = cropDatasetToLoad;
        foreach (CropSaveState cropSave in cropDatasetToLoad)
        {
            //Access the land
            Land landToPlant = landPlots[cropSave.landID];
            //Spawn the crop
            CropBehaviour cropToPlant = landToPlant.SpawnCrop();
            Debug.Log(cropToPlant.gameObject); 
            //Load in the data
            SeedData seedToGrow = (SeedData) InventoryManager.Instance.itemIndex.GetItemFromString(cropSave.seedToGrow);
            cropToPlant.LoadCrop(cropSave.landID, seedToGrow, cropSave.cropState, cropSave.growth, cropSave.health);
        }
        
    }
    #endregion

    // Update is called once per frame
    void Update()
    {
        
    }
}

CropSaveState.cs

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

[System.Serializable]
public struct CropSaveState
{
    //The index of the land the crop is planted on 
    public int landID;

    public string seedToGrow;
    public CropBehaviour.CropState cropState;
    public int growth;
    public int health;

    public CropSaveState(int landID, string seedToGrow, CropBehaviour.CropState cropState, int growth, int health)
    {
        this.landID = landID;
        this.seedToGrow = seedToGrow;
        this.cropState = cropState;
        this.growth = growth;
        this.health = health;
    }
}

LandSaveState.cs

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

[System.Serializable]
public struct LandSaveState 
{
    public Land.LandStatus landStatus;
    public GameTimestamp lastWatered;

    public LandSaveState(Land.LandStatus landStatus, GameTimestamp lastWatered)
    {
        this.landStatus = landStatus;
        this.lastWatered = lastWatered;
    }
}

ItemIndex.cs

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

[CreateAssetMenu(menuName = "Items/Item Index")]
public class ItemIndex : ScriptableObject
{
    public List<ItemData> items; 

    public ItemData GetItemFromString(string name)
    {
        return items.Find(i => i.name == name); 
    }
}

InventoryManager.cs

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

public class InventoryManager : MonoBehaviour
{
    public static InventoryManager Instance { get; private set; }

    private void Awake()
    {
        //If there is more than one instance, destroy the extra
        if(Instance != null && Instance != this)
        {
            Destroy(this);
        }
        else
        {
            //Set the static instance to this instance
            Instance = this; 
        }
    }

    //The full list of items 
    public ItemIndex itemIndex; 

    [Header("Tools")]
    //Tool Slots
    [SerializeField]
    private ItemSlotData[] toolSlots = new ItemSlotData[8];
    //Tool in the player's hand
    [SerializeField]
    private ItemSlotData equippedToolSlot = null; 

    [Header("Items")]
    //Item Slots
    [SerializeField]
    private ItemSlotData[] itemSlots = new ItemSlotData[8];
    //Item in the player's hand
    [SerializeField]
    private ItemSlotData equippedItemSlot = null;

    //The transform for the player to hold items in the scene
    public Transform handPoint; 

    //Equipping

    //Handles movement of item from Inventory to Hand
    public void InventoryToHand(int slotIndex, InventorySlot.InventoryType inventoryType)
    {
        //The slot to equip (Tool by default)
        ItemSlotData handToEquip = equippedToolSlot;
        //The array to change
        ItemSlotData[] inventoryToAlter = toolSlots; 
        
        if(inventoryType == InventorySlot.InventoryType.Item)
        {
            //Change the slot to item
            handToEquip = equippedItemSlot;
            inventoryToAlter = itemSlots;
        }

        //Check if stackable
        if (handToEquip.Stackable(inventoryToAlter[slotIndex]))
        {
            ItemSlotData slotToAlter = inventoryToAlter[slotIndex];

            //Add to the hand slot
            handToEquip.AddQuantity(slotToAlter.quantity);

            //Empty the inventory slot
            slotToAlter.Empty();


        } else
        {
            //Not stackable
            //Cache the Inventory ItemSlotData
            ItemSlotData slotToEquip = new ItemSlotData(inventoryToAlter[slotIndex]);

            //Change the inventory slot to the hands
            inventoryToAlter[slotIndex] = new ItemSlotData(handToEquip);

            EquipHandSlot(slotToEquip); 
        }

        //Update the changes in the scene
        if (inventoryType == InventorySlot.InventoryType.Item)
        {
            RenderHand();
        }

        //Update the changes to the UI
        UIManager.Instance.RenderInventory();

    }

    //Handles movement of item from Hand to Inventory
    public void HandToInventory(InventorySlot.InventoryType inventoryType)
    {
        //The slot to move from (Tool by default)
        ItemSlotData handSlot = equippedToolSlot;
        //The array to change
        ItemSlotData[] inventoryToAlter = toolSlots;

        if (inventoryType == InventorySlot.InventoryType.Item)
        {
            handSlot = equippedItemSlot;
            inventoryToAlter = itemSlots;
        }

        //Try stacking the hand slot. 
        //Check if the operation failed
        if (!StackItemToInventory(handSlot, inventoryToAlter))
        {
            //Find an empty slot to put the item in 
            //Iterate through each inventory slot and find an empty slot
            for (int i = 0; i < inventoryToAlter.Length; i++)
            {
                if (inventoryToAlter[i].IsEmpty())
                {
                    //Send the equipped item over to its new slot
                    inventoryToAlter[i] = new ItemSlotData(handSlot);
                    //Remove the item from the hand
                    handSlot.Empty();
                    break;
                }
            }

        }

        //Update the changes in the scene
        if (inventoryType == InventorySlot.InventoryType.Item)
        {
            RenderHand();
        }

        //Update the changes to the UI
        UIManager.Instance.RenderInventory();

       
    }

    //Iterate through each of the items in the inventory to see if it can be stacked
    //Will perform the operation if found, returns false if unsuccessful
    public bool StackItemToInventory(ItemSlotData itemSlot, ItemSlotData[] inventoryArray)
    {
        
        for (int i = 0; i < inventoryArray.Length; i++)
        {
            if (inventoryArray[i].Stackable(itemSlot))
            {
                //Add to the inventory slot's stack
                inventoryArray[i].AddQuantity(itemSlot.quantity);
                //Empty the item slot
                itemSlot.Empty();
                return true; 
            }
        }

        //Can't find any slot that can be stacked
        return false; 
    }

    //Render the player's equipped item in the scene
    public void RenderHand()
    {
        //Reset objects on the hand
        if(handPoint.childCount > 0)
        {
            Destroy(handPoint.GetChild(0).gameObject);
        }

        //Check if the player has anything equipped
        if(SlotEquipped(InventorySlot.InventoryType.Item))
        {
            //Instantiate the game model on the player's hand and put it on the scene
            Instantiate(GetEquippedSlotItem(InventorySlot.InventoryType.Item).gameModel, handPoint);
        }
        
    }

    //Inventory Slot Data 
    #region Gets and Checks
    //Get the slot item (ItemData) 
    public ItemData GetEquippedSlotItem(InventorySlot.InventoryType inventoryType)
    {
        if(inventoryType == InventorySlot.InventoryType.Item)
        {
            return equippedItemSlot.itemData;
        }
        return equippedToolSlot.itemData; 
    }

    //Get function for the slots (ItemSlotData)
    public ItemSlotData GetEquippedSlot(InventorySlot.InventoryType inventoryType)
    {
        if (inventoryType == InventorySlot.InventoryType.Item)
        {
            return equippedItemSlot;
        }
        return equippedToolSlot;
    }

    //Get function for the inventory slots
    public ItemSlotData[] GetInventorySlots(InventorySlot.InventoryType inventoryType)
    {
        if (inventoryType == InventorySlot.InventoryType.Item)
        {
            return itemSlots;
        }
        return toolSlots;
    }

    //Check if a hand slot has an item
    public bool SlotEquipped(InventorySlot.InventoryType inventoryType)
    {
        if (inventoryType == InventorySlot.InventoryType.Item)
        {
            return !equippedItemSlot.IsEmpty();
        }
        return !equippedToolSlot.IsEmpty();
    }

    //Check if the item is a tool
    public bool IsTool(ItemData item)
    {
        //Is it equipment? 
        //Try to cast it as equipment first
        EquipmentData equipment = item as EquipmentData;
        if(equipment != null)
        {
            return true; 
        }

        //Is it a seed?
        //Try to cast it as a seed
        SeedData seed = item as SeedData;
        //If the seed is not null it is a seed 
        return seed != null; 

    }

    #endregion

    //Equip the hand slot with an ItemData (Will overwrite the slot)
    public void EquipHandSlot(ItemData item)
    {
        if (IsTool(item))
        {
            equippedToolSlot = new ItemSlotData(item); 
        } else
        {
            equippedItemSlot = new ItemSlotData(item); 
        }

    }

    //Equip the hand slot with an ItemSlotData (Will overwrite the slot)
    public void EquipHandSlot(ItemSlotData itemSlot)
    {
        //Get the item data from the slot 
        ItemData item = itemSlot.itemData;
        
        if (IsTool(item))
        {
            equippedToolSlot = new ItemSlotData(itemSlot);
        }
        else
        {
            equippedItemSlot = new ItemSlotData(itemSlot);
        }
    }

    public void ConsumeItem(ItemSlotData itemSlot)
    {
        if (itemSlot.IsEmpty())
        {
            Debug.LogError("There is nothing to consume!");
            return; 
        }

        //Use up one of the item slots
        itemSlot.Remove();
        //Refresh inventory
        RenderHand();
        UIManager.Instance.RenderInventory(); 
    }


    #region Inventory Slot Validation
    private void OnValidate()
    {
        //Validate the hand slots
        ValidateInventorySlot(equippedToolSlot);
        ValidateInventorySlot(equippedItemSlot);

        //Validate the slots in the inventoryy
        ValidateInventorySlots(itemSlots);
        ValidateInventorySlots(toolSlots);

    }
    
    //When giving the itemData value in the inspector, automatically set the quantity to 1 
    void ValidateInventorySlot(ItemSlotData slot)
    {
        if(slot.itemData != null && slot.quantity == 0)
        {
            slot.quantity = 1;
        }
    }

    //Validate arrays
    void ValidateInventorySlots(ItemSlotData[] array)
    {
        foreach (ItemSlotData slot in array)
        {
            ValidateInventorySlot(slot);
        }
    }
    #endregion

    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

Article continues after the advertisement:


There are 2 comments:

  1. Hi there, after i completed this tutorial i got some error pops up “ArgumentOutOfRangeException, parameter name index”, and two error lines are hightlighted in CropBehavior LandManager.Instance.OnCropStateChange(landID, cropState, growth, health).
    I even overwrote all the codes by copy and paste yours, still no lucks.

    I think the problem is when CropBehavior try to inform LandManager the changes(4 parameters), and in the LandManager cropData[cropIndex] = new CropSaveState(landID, seedToGrow, cropState, growth, health), it passed 5 parameters to CropSaveState.

    i really appreciate your help in this matter. Thank you.

    1. Hi Andreas,
      It is likely that the problem lies with the landID value being set incorrectly at some point.

      Try running the game, enable debug mode in the inspector, and check the cropData List to see if the landID values are registered properly.

Leave a Reply

Your email address will not be published.