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.
1. Bug fixes
Before starting on our agenda in this article, we need to first fix some bugs.
- Make tilling the land a requirement before watering it instead of being able to directly change the soil to watered with the watering can
- Make our seeds visible after planting them.

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,00.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.

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.

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.

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.
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.

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] publicclassstruct 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:
landID(to know which land the crop is planted on)- the seed that it is growing on the land
- current growth of the crop
- stage of growth (
CropState) - 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] publicclassstruct 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.
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.

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:
- Save the changes immediately to
farmDatawhenever theLandStatechanges - 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.
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.
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.
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.

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

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 :MonoBehaviourScriptableObject { 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.

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.

Going back to LandManager, we need to do 3 things to import the the cropData:
- Access the land
- Spawn the crop
- 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 } }
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:
- When we harvest the crop
- 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)); } }
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()
{
}
}

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.
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.