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.
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
farmData
whenever theLandState
changes - 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() { ... } voidStartOnEnable() { 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.