Ever wanted to create a game like Harvest Moon in Unity? Check out Part 29 of our guide here, where we work on player animations and a stamina system. You can also find Part 28 of our guide here, where we went through Part 3 of our NPC events.
1. Revamping our UI Assets
Before we start on the main topics in this part, we will replace some of our user interface assets, and give them a fresher look
a. Preparing our new asset pack
After we import our new asset pack for the UI, we need to make some adjustments to the sprites before integrating it into our game.
While selecting all the UI assets in this pack, we need to:
- Set the texture type to 2D and UI
- Compression to None
- Sprite Mode to Single
- Filter Mode to Bilinear
After these are set, apply the changes
b. Adjusting the scaling of UI assets
When wanting to adjust the height or width of a UI sprite without maintaining its aspect ratio, it might lead to the sprite having a squashed look. Using the sprite editor, we can easily fix that by setting the border of the sprite.
In the bottom right panel of the sprite editor, adjust the values of your border to indicate what parts of the sprite should maintain its aspect ratio, or use the green dots that are around the sprite to scale them. Remember to apply these changes before closing the tab.
After that just assign the sprite image to the respective UI game object, and we rinse and repeat for all the UI in the game.
For the tool and food icons, it follows the same process, the only difference is that the sprites are added in the ScriptableObject of each respective tool.
2. Adding Player Tool Animations
Now we start adding the animations for the player using farming tools on the plot of land. For this tutorial, we will be adding a Watering and Plowing animation for when the player uses the watering can or hoe
a. Setting up our animation files
After getting our animations from mixamo, we need to adjust some settings of each file to make it work smoothly with our character
- Rename each animation file as Watering and Plowing
- In the rig panel, set the Animation type as Humanoid and the Avatar definition to Copy from the character model's Avatar
- In the animation panel, tick the box to Bake the transform rotation
We will add both animation states into the player's animator controller:
- Duplicate the animation clip of each imported animation and rename them Watering and Plowing
- Drag and drop them into the animator controller and connect them to the Idle animation state with back and forth transition arrows
- For the transition arrows pointing away from idle, set to Has No Exit Time and Transition Duration to 0
Add the respective triggers to each transition as well
b. Integrating the tools into the animations
Now it is time to add the tool models into the character animation. In the hierachy, create a gameobject with the tool model as the child of the player's HandPoint, then adjust it's transform to as close to the player's hand as possible.
Set the tool using animation as the default state (Example: Make the plowing animation the default state when adjusting the hoe) then enter Play Mode, pause play mode at a point in the animation and change the transform of the tool game object to fit in the player's hand.
After adjusting the transform the way you like it, copy the component transform and rotation coordinates by selecting Copy > Component, then exit Play Mode and pasting the component values into the tool's transform. Test out the animation, and it will look like the player's holding the tool. Do the same for the Watering Can.
To integrate the animation triggers into the script:
- In the PlayerInteraction script, create a variable for Animator and EquipmentData
- Reference the animator component of the player on Start
- In the Interact function, reference the equipped tool and make switch cases for each tool, while also adding animation triggers to each case
- We will also call to interact with the selected land such that the land state will change once we use a certain tool
PlayerInteraction.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; public class PlayerInteraction : MonoBehaviour { PlayerController playerController; Animator animator; EquipmentData equipmentTool; //The land the player is currently selecting Land selectedLand = null; //The interactable object the player is currently selecting InteractableObject selectedInteractable = null; // Start is called before the first frame update void Start() { //Get access to our PlayerController component playerController = transform.parent.GetComponent<PlayerController>(); animator = GameObject.FindGameObjectWithTag("Player").GetComponent<Animator>(); } // Update is called once per frame void Update() { RaycastHit hit; if(Physics.Raycast(transform.position, Vector3.down,out hit, 1)) { OnInteractableHit(hit); } } //Handles what happens when the interaction raycast hits something interactable void OnInteractableHit(RaycastHit hit) { Collider other = hit.collider; //Check if the player is going to interact with land if(other.tag == "Land") { //Get the land component Land land = other.GetComponent<Land>(); SelectLand(land); return; } //Check if the player is going to interact with an Item if(other.tag == "Item") { //Set the interactable to the currently selected interactable selectedInteractable = other.GetComponent<InteractableObject>(); selectedInteractable.OnHover(); return; } //Deselect the interactable if the player is not standing on anything at the moment if(selectedInteractable != null) { selectedInteractable.OnMoveAway(); selectedInteractable = null; } //Deselect the land if the player is not standing on any land at the moment if(selectedLand != null) { selectedLand.Select(false); selectedLand = null; } } //Handles the selection process of the land void SelectLand(Land land) { //Set the previously selected land to false (If any) if (selectedLand != null) { selectedLand.Select(false); } //Set the new selected land to the land we're selecting now. selectedLand = land; land.Select(true); } //Triggered when the player presses the tool button public void Interact() { //Detecting what is the current tool that the player is holding ItemData toolSlot = InventoryManager.Instance.GetEquippedSlotItem(InventorySlot.InventoryType.Tool); EquipmentData equipmentTool = toolSlot as EquipmentData; //The player shouldn't be able to use his tool when he has his hands full with an item if(InventoryManager.Instance.SlotEquipped(InventorySlot.InventoryType.Item)) { return; } //Check if the player is selecting any land if(selectedLand != null) { EquipmentData.ToolType toolType = equipmentTool.toolType; switch (toolType) { case EquipmentData.ToolType.WateringCan: animator.SetTrigger("Watering"); selectedLand.Interact(); return; case EquipmentData.ToolType.Hoe: animator.SetTrigger("Plowing"); selectedLand.Interact(); return; } return; } Debug.Log("Not on any land!"); } //Triggered when the player presses the item interact button public void ItemInteract() { //If the player isn't holding anything, pick up an item //Check if there is an interactable selected if (selectedInteractable != null) { //Pick it up selectedInteractable.Pickup(); } } public void ItemKeep() { //If the player is holding something, keep it in his inventory if (InventoryManager.Instance.SlotEquipped(InventorySlot.InventoryType.Item)) { InventoryManager.Instance.HandToInventory(InventorySlot.InventoryType.Item); return; } } }
Save the script then set the Idle animation state as the default.
c. Spawning and Despawning of tools
When we want each tool to only appear when their respective animations are being played, we need to make use of the animation timeline.
Now its time to create functions for spawning and despawning the tools. Make a new script called AnimEventsTools in the Player game object, which will contain the functions to spawn and despawn specific tools.
- For each tool, create 2 types of game object variables for each model, one for the original model, and the other for an instantiated clone. Create a transform variable for the handPoint as well.
- Then create public functions to spawn and despawn each tool
AnimEventsTools.cs
using UnityEngine; public class AnimEventsTools : MonoBehaviour { // this script contains the anim events for the spawning of tools when the animation is played // this is the original objects of the tools public GameObject wateringCan; public GameObject Hoe; private GameObject newWateringCan; private GameObject newHoe; public Transform handPoint; public void SpawnWaterCan() { newWateringCan = Instantiate(wateringCan, handPoint); } public void DespawnWaterCan() { Destroy(newWateringCan); } public void SpawnHoe() { newHoe = Instantiate(Hoe, handPoint); } public void DespawnHoe() { Destroy(newHoe); } }
Save the script and close it. Drag the Hoe and Watering Can game objects into the project files to create prefab variants, then delete the original game objects.
Assign the prefab variants and the Hand Point into the inspector of AnimEventTools
Open the animation timeline of the player's animation controller, in the each animation, we will create 2 animation events, one at the start and the other nearly at the end. Now call the function in AnimEventTools to spawn the tool at the start and despawn the tool at the end. Proceed to save and test the animations.
3. Adding the Stamina System
a. Creating the Stamina HUD
We will start off by adding a visual stamina UI to the HUD, import the stamina UI images (can be found in this drive), and add the stamina UI base as a UI image and a sprite emoticon as a child image.
Now we will start adding the stamina system within the script
In PlayerStats, add stamina as a static integer, and add it into SaveStats. Create 2 new functions UseStamina and RestoreStamina, which we will use later on to manage how much stamina the player will have.
PlayerStats.cs
public class PlayerStats { public static int Money { get; private set; } public static int Stamina; public const string CURRENCY = "G"; public static void Spend(int cost) { //Check if the player has enough to spend if(cost > Money) { Debug.LogError("Player does not have enough money"); return; } Money -= cost; UIManager.Instance.RenderPlayerStats(); } public static void Earn(int income) { Money += income; UIManager.Instance.RenderPlayerStats(); } public static void LoadStats(int money, int stamina) { Money = money; Stamina = stamina; UIManager.Instance.RenderPlayerStats(); } public static void UseStamina(int staminaLost) { Stamina -= staminaLost; UIManager.Instance.RenderPlayerStats(); } public static void RestoreStamina() { Stamina = 150; UIManager.Instance.RenderPlayerStats(); } }
Now in the UIManager script, create a sprite array for the sprite emoticons, an image variable in the staminaUI emoticon in the game, and lastly a stamina count integer. Call the RestoreStamina() function on start to test out the stamina function.
Create a new function called ChangeStaminaUI and call it in RenderPlayerStats. ChangeStaminaUI will switch the the images of the sprite array when the stamina reaches below a certain value.
UIManager.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using TMPro; public class UIManager : MonoBehaviour, ITimeTracker { public static UIManager Instance { get; private set; } [Header("Screen Management")] public GameObject menuScreen; public enum Tab { Inventory, Relationships, Animals } //The current selected tab public Tab selectedTab; [Header("Status Bar")] //Tool equip slot on the status bar public Image toolEquipSlot; //Tool Quantity text on the status bar public TextMeshProUGUI toolQuantityText; //Time UI public TextMeshProUGUI timeText; public TextMeshProUGUI dateText; [Header("Inventory System")] //The inventory panel public GameObject inventoryPanel; //The tool equip slot UI on the Inventory panel public HandInventorySlot toolHandSlot; //The tool slot UIs public InventorySlot[] toolSlots; //The item equip slot UI on the Inventory panel public HandInventorySlot itemHandSlot; //The item slot UIs public InventorySlot[] itemSlots; [Header("Item info box")] public GameObject itemInfoBox; public TextMeshProUGUI itemNameText; public TextMeshProUGUI itemDescriptionText; [Header("Screen Transitions")] public GameObject fadeIn; public GameObject fadeOut; [Header("Prompts")] public YesNoPrompt yesNoPrompt; public NamingPrompt namingPrompt; [SerializeField] InteractBubble interactBubble; [Header("Player Stats")] public TextMeshProUGUI moneyText; [Header("Shop")] public ShopListingManager shopListingManager; [Header("Relationships")] public RelationshipListingManager relationshipListingManager; public AnimalListingManager animalRelationshipListingManager; [Header("Calendar")] public CalendarUIListing calendar; [Header("Stamina")] public Sprite[] staminaUI; public Image StaminaUIImage; public int staminaCount; 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; } } private void Start() { PlayerStats.RestoreStamina(); RenderInventory(); AssignSlotIndexes(); RenderPlayerStats(); DisplayItemInfo(null); //Add UIManager to the list of objects TimeManager will notify when the time updates TimeManager.Instance.RegisterTracker(this); } #region Prompts public void TriggerNamingPrompt(string message, System.Action<string> onConfirmCallback) { //Check if another prompt is already in progress if (namingPrompt.gameObject.activeSelf) { //Queue the prompt namingPrompt.QueuePromptAction(() => TriggerNamingPrompt(message, onConfirmCallback)); return; } //Open the panel namingPrompt.gameObject.SetActive(true); namingPrompt.CreatePrompt(message, onConfirmCallback); } public void TriggerYesNoPrompt(string message, System.Action onYesCallback) { //Set active the gameobject of the Yes No Prompt yesNoPrompt.gameObject.SetActive(true); yesNoPrompt.CreatePrompt(message, onYesCallback); } #endregion #region Tab Management public void ToggleMenuPanel() { menuScreen.SetActive(!menuScreen.activeSelf); OpenWindow(selectedTab); TabBehaviour.onTabStateChange?.Invoke(); } //Manage the opening of windows associated with the tab public void OpenWindow(Tab windowToOpen) { //Disable all windows relationshipListingManager.gameObject.SetActive(false); inventoryPanel.SetActive(false); animalRelationshipListingManager.gameObject.SetActive(false); //Open the corresponding window and render it switch (windowToOpen) { case Tab.Inventory: inventoryPanel.SetActive(true); RenderInventory(); break; case Tab.Relationships: relationshipListingManager.gameObject.SetActive(true); relationshipListingManager.Render(RelationshipStats.relationships); break; case Tab.Animals: animalRelationshipListingManager.gameObject.SetActive(true); animalRelationshipListingManager.Render(AnimalStats.animalRelationships); break; } //Set the selected tab selectedTab = windowToOpen; } #endregion #region Fadein Fadeout Transitions public void FadeOutScreen() { fadeOut.SetActive(true); } public void FadeInScreen() { fadeIn.SetActive(true); } public void OnFadeInComplete() { //Disable Fade in Screen when animation is completed fadeIn.SetActive(false); } //Reset the fadein fadeout screens to their default positions public void ResetFadeDefaults() { fadeOut.SetActive(false); fadeIn.SetActive(true); } #endregion #region Inventory //Iterate through the slot UI elements and assign it its reference slot index public void AssignSlotIndexes() { for (int i =0; i<toolSlots.Length; i++) { toolSlots[i].AssignIndex(i); itemSlots[i].AssignIndex(i); } } //Render the inventory screen to reflect the Player's Inventory. public void RenderInventory() { //Get the respective slots to process ItemSlotData[] inventoryToolSlots = InventoryManager.Instance.GetInventorySlots(InventorySlot.InventoryType.Tool); ItemSlotData[] inventoryItemSlots = InventoryManager.Instance.GetInventorySlots(InventorySlot.InventoryType.Item); //Render the Tool section RenderInventoryPanel(inventoryToolSlots, toolSlots); //Render the Item section RenderInventoryPanel(inventoryItemSlots, itemSlots); //Render the equipped slots toolHandSlot.Display(InventoryManager.Instance.GetEquippedSlot(InventorySlot.InventoryType.Tool)); itemHandSlot.Display(InventoryManager.Instance.GetEquippedSlot(InventorySlot.InventoryType.Item)); //Get Tool Equip from InventoryManager ItemData equippedTool = InventoryManager.Instance.GetEquippedSlotItem(InventorySlot.InventoryType.Tool); //Text should be empty by default toolQuantityText.text = ""; //Check if there is an item to display if (equippedTool != null) { //Switch the thumbnail over toolEquipSlot.sprite = equippedTool.thumbnail; toolEquipSlot.gameObject.SetActive(true); //Get quantity int quantity = InventoryManager.Instance.GetEquippedSlot(InventorySlot.InventoryType.Tool).quantity; if (quantity > 1) { toolQuantityText.text = quantity.ToString(); } return; } toolEquipSlot.gameObject.SetActive(false); } //Iterate through a slot in a section and display them in the UI void RenderInventoryPanel(ItemSlotData[] slots, InventorySlot[] uiSlots) { for (int i = 0; i < uiSlots.Length; i++) { //Display them accordingly uiSlots[i].Display(slots[i]); } } public void ToggleInventoryPanel() { //If the panel is hidden, show it and vice versa inventoryPanel.SetActive(!inventoryPanel.activeSelf); RenderInventory(); } //Display Item info on the Item infobox public void DisplayItemInfo(ItemData data) { //If data is null, reset if(data == null) { itemNameText.text = ""; itemDescriptionText.text = ""; itemInfoBox.SetActive(false); return; } itemInfoBox.SetActive(true); itemNameText.text = data.name; itemDescriptionText.text = data.description; } #endregion #region Time //Callback to handle the UI for time public void ClockUpdate(GameTimestamp timestamp) { //Handle the time //Get the hours and minutes int hours = timestamp.hour; int minutes = timestamp.minute; //AM or PM string prefix = "AM "; //Convert hours to 12 hour clock if (hours >= 12) { //Time becomes PM prefix = "PM "; //12 PM and later hours = hours - 12; Debug.Log(hours); } //Special case for 12am/pm to display it as 12 instead of 0 hours = hours == 0 ? 12 : hours; //Format it for the time text display timeText.text = prefix + hours + ":" + minutes.ToString("00"); //Handle the Date int day = timestamp.day; string season = timestamp.season.ToString(); string dayOfTheWeek = timestamp.GetDayOfTheWeek().ToString(); //Format it for the date text display dateText.text = season + " " + day + " (" + dayOfTheWeek +")"; } #endregion //Render the UI of the player stats in the HUD public void RenderPlayerStats() { moneyText.text = PlayerStats.Money + PlayerStats.CURRENCY; staminaCount = PlayerStats.Stamina; ChangeStaminaUI(); } //Open the shop window with the shop items listed public void OpenShop(List<ItemData> shopItems) { //Set active the shop window shopListingManager.gameObject.SetActive(true); shopListingManager.Render(shopItems); } public void ToggleRelationshipPanel() { GameObject panel = relationshipListingManager.gameObject; panel.SetActive(!panel.activeSelf); //If open, render the screen if (panel.activeSelf) { relationshipListingManager.Render(RelationshipStats.relationships); } } public void InteractPrompt(Transform item, string message, float offset) { interactBubble.gameObject.SetActive(true); interactBubble.transform.position = item.transform.position + new Vector3(0, offset, 0); interactBubble.Display(message); } public void DeactivateInteractPrompt() { interactBubble.gameObject.SetActive(false); } public void ChangeStaminaUI() { if (staminaCount <= 45) StaminaUIImage.sprite = staminaUI[3]; // exhausted else if (staminaCount <= 80) StaminaUIImage.sprite = staminaUI[2]; // tired else if (staminaCount <= 115) StaminaUIImage.sprite = staminaUI[1]; // active else if (staminaCount <= 150) StaminaUIImage.sprite = staminaUI[0]; // energised } }
b. Adding the stamina usage into the script
Returning back to the PlayerInteraction script, add the UseStamina function into the switch cases.
PlayerInteraction.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; public class PlayerInteraction : MonoBehaviour { PlayerController playerController; Animator animator; EquipmentData equipmentTool; //The land the player is currently selecting Land selectedLand = null; //The interactable object the player is currently selecting InteractableObject selectedInteractable = null; // Start is called before the first frame update void Start() { //Get access to our PlayerController component playerController = transform.parent.GetComponent<PlayerController>(); animator = GameObject.FindGameObjectWithTag("Player").GetComponent<Animator>(); } // Update is called once per frame void Update() { RaycastHit hit; if(Physics.Raycast(transform.position, Vector3.down,out hit, 1)) { OnInteractableHit(hit); } } //Handles what happens when the interaction raycast hits something interactable void OnInteractableHit(RaycastHit hit) { Collider other = hit.collider; //Check if the player is going to interact with land if(other.tag == "Land") { //Get the land component Land land = other.GetComponent<Land>(); SelectLand(land); return; } //Check if the player is going to interact with an Item if(other.tag == "Item") { //Set the interactable to the currently selected interactable selectedInteractable = other.GetComponent<InteractableObject>(); selectedInteractable.OnHover(); return; } //Deselect the interactable if the player is not standing on anything at the moment if(selectedInteractable != null) { selectedInteractable.OnMoveAway(); selectedInteractable = null; } //Deselect the land if the player is not standing on any land at the moment if(selectedLand != null) { selectedLand.Select(false); selectedLand = null; } } //Handles the selection process of the land void SelectLand(Land land) { //Set the previously selected land to false (If any) if (selectedLand != null) { selectedLand.Select(false); } //Set the new selected land to the land we're selecting now. selectedLand = land; land.Select(true); } //Triggered when the player presses the tool button public void Interact() { //Detecting what is the current tool that the player is holding ItemData toolSlot = InventoryManager.Instance.GetEquippedSlotItem(InventorySlot.InventoryType.Tool); EquipmentData equipmentTool = toolSlot as EquipmentData; //The player shouldn't be able to use his tool when he has his hands full with an item if(InventoryManager.Instance.SlotEquipped(InventorySlot.InventoryType.Item)) { return; } //Check if the player is selecting any land if(selectedLand != null) { EquipmentData.ToolType toolType = equipmentTool.toolType; switch (toolType) { case EquipmentData.ToolType.WateringCan: PlayerStats.UseStamina(2); animator.SetTrigger("Watering"); selectedLand.Interact(); return; case EquipmentData.ToolType.Hoe: PlayerStats.UseStamina(2); animator.SetTrigger("Plowing"); selectedLand.Interact(); return; } return; } Debug.Log("Not on any land!"); } //Triggered when the player presses the item interact button public void ItemInteract() { //If the player isn't holding anything, pick up an item //Check if there is an interactable selected if (selectedInteractable != null) { //Pick it up selectedInteractable.Pickup(); } } public void ItemKeep() { //If the player is holding something, keep it in his inventory if (InventoryManager.Instance.SlotEquipped(InventorySlot.InventoryType.Item)) { InventoryManager.Instance.HandToInventory(InventorySlot.InventoryType.Item); return; } } }
Save all these scripts and assign the Stamina UI emoticons to the UIManager in the inspector in the order to energised to exhausted. Then test the game, if the stamina UI changes after using the tools for a while, our stamina system is working.
c. Saving our stamina
We need to add our stamina variables into our save system, to do that we can start adding it into the PlayerSaveState script
PlayerSaveState.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; [System.Serializable] public class PlayerSaveState { public int money; public int stamina; public PlayerSaveState(int money, int stamina) { this.money = money; this.stamina = stamina; } public void LoadData() { PlayerStats.LoadStats(money, stamina); } public static PlayerSaveState Export() { return new PlayerSaveState(PlayerStats.Money, PlayerStats.Stamina); } }
Lastly for our current stamina system, we will make it such that when the player sleeps , they will replenish stamina completely.
In our GameStateManager script, we will call RestoreStamina in the Sleep function
GameStateManager.cs
using System; using System.Collections; using System.Collections.Generic; using System.Runtime.InteropServices.WindowsRuntime; using UnityEngine; using UnityEngine.Events; public class GameStateManager : MonoBehaviour, ITimeTracker { public static GameStateManager Instance { get; private set; } //Check if the screen has finished fading out bool screenFadedOut; //To track interval updates private int minutesElapsed = 0; //Event triggered every 15 minutes public UnityEvent onIntervalUpdate; [SerializeField] GameBlackboard blackboard = new GameBlackboard(); //This blackboard will not be saved GameBlackboard sceneItemsBoard = new GameBlackboard(); const string TIMESTAMP = "Timestamp"; public GameBlackboard GetBlackboard() { return blackboard; } 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; } } // Start is called before the first frame update void Start() { //Add this to TimeManager's Listener list TimeManager.Instance.RegisterTracker(this); SceneTransitionManager.Instance.onLocationLoad.AddListener(RenderPersistentObjects); } public void ClockUpdate(GameTimestamp timestamp) { UpdateShippingState(timestamp); UpdateFarmState(timestamp); IncubationManager.UpdateEggs(); blackboard.SetValue(TIMESTAMP, timestamp); if (timestamp.hour == 0 && timestamp.minute == 0) { OnDayReset(); } if(minutesElapsed >= 15) { minutesElapsed = 0; onIntervalUpdate?.Invoke(); } else { minutesElapsed++; } } //Called when the day has been reset void OnDayReset() { Debug.Log("Day has been reset"); foreach(NPCRelationshipState npc in RelationshipStats.relationships) { npc.hasTalkedToday = false; npc.giftGivenToday = false; } AnimalFeedManager.ResetFeedboxes(); AnimalStats.OnDayReset(); } void UpdateShippingState(GameTimestamp timestamp) { //Check if the hour is here (Exactly 1800 hours) if(timestamp.hour == ShippingBin.hourToShip && timestamp.minute == 0) { ShippingBin.ShipItems(); } } void UpdateFarmState(GameTimestamp timestamp) { //Update the Land and Crop Save states as long as the player is outside of the Farm scene if (SceneTransitionManager.Instance.currentLocation != SceneTransitionManager.Location.Farm) { //If there is nothing to update to begin with, stop if (LandManager.farmData == null) return; //Retrieve the Land and Farm data from the static variable List<LandSaveState> landData = LandManager.farmData.Item1; List<CropSaveState> cropData = LandManager.farmData.Item2; //If there are no crops planted, we don't need to worry about updating anything if (cropData.Count == 0) return; for (int i = 0; i < cropData.Count; i++) { //Get the crop and corresponding land data CropSaveState crop = cropData[i]; LandSaveState land = landData[crop.landID]; //Check if the crop is already wilted if (crop.cropState == CropBehaviour.CropState.Wilted) continue; //Update the Land's state land.ClockUpdate(timestamp); //Update the crop's state based on the land state if (land.landStatus == Land.LandStatus.Watered) { crop.Grow(); } else if (crop.cropState != CropBehaviour.CropState.Seed) { crop.Wither(); } //Update the element in the array cropData[i] = crop; landData[crop.landID] = land; } } } //Instantiate the GameObject such that it stays in the scene even after the player leaves public GameObject PersistentInstantiate(GameObject gameObject, Vector3 position, Quaternion rotation) { GameObject item = Instantiate(gameObject, position, rotation); //Save it to the blackboard (GameObject, Vector3) itemInformation = (gameObject, position); List<(GameObject, Vector3)> items = sceneItemsBoard.GetOrInitList<(GameObject, Vector3)>(SceneTransitionManager.Instance.currentLocation.ToString()); items.Add(itemInformation); sceneItemsBoard.Debug(); return item; } public void PersistentDestroy(GameObject item) { if (sceneItemsBoard.TryGetValue(SceneTransitionManager.Instance.currentLocation.ToString(), out List<(GameObject, Vector3)> items)) { int index = items.FindIndex(i => i.Item2 == item.transform.position); items.RemoveAt(index); } Destroy(item); } void RenderPersistentObjects() { Debug.Log("Rendering Persistent Objects"); if (sceneItemsBoard.TryGetValue(SceneTransitionManager.Instance.currentLocation.ToString(), out List<(GameObject, Vector3)> items)) { foreach((GameObject, Vector3) item in items) { Instantiate(item.Item1, item.Item2, Quaternion.identity); } } } public void Sleep() { //Call a fadeout UIManager.Instance.FadeOutScreen(); screenFadedOut = false; StartCoroutine(TransitionTime()); //Restore Stamina fully when sleeping PlayerStats.RestoreStamina(); } IEnumerator TransitionTime() { //Calculate how many ticks we need to advance the time to 6am //Get the time stamp of 6am the next day GameTimestamp timestampOfNextDay = TimeManager.Instance.GetGameTimestamp(); timestampOfNextDay.day += 1; timestampOfNextDay.hour = 6; timestampOfNextDay.minute = 0; Debug.Log(timestampOfNextDay.day + " " + timestampOfNextDay.hour + ":" + timestampOfNextDay.minute); //Wait for the scene to finish fading out before loading the next scene while (!screenFadedOut) { yield return new WaitForSeconds(1f); } TimeManager.Instance.SkipTime(timestampOfNextDay); //Save SaveManager.Save(ExportSaveState()); //Reset the boolean screenFadedOut = false; UIManager.Instance.ResetFadeDefaults(); } //Called when the screen has faded out public void OnFadeOutComplete() { screenFadedOut = true; } public GameSaveState ExportSaveState() { //Retrieve Farm Data FarmSaveState farmSaveState = FarmSaveState.Export(); //Retrieve inventory data InventorySaveState inventorySaveState = InventorySaveState.Export(); PlayerSaveState playerSaveState = PlayerSaveState.Export(); //Time GameTimestamp timestamp = TimeManager.Instance.GetGameTimestamp(); return new GameSaveState(blackboard, farmSaveState, inventorySaveState, timestamp, playerSaveState); } public void LoadSave() { //Retrieve the loaded save GameSaveState save = SaveManager.Load(); //Load up the parts blackboard = save.blackboard; blackboard.Debug(); //Time TimeManager.Instance.LoadTime(save.timestamp); //Inventory save.inventorySaveState.LoadData(); //Farming data save.farmSaveState.LoadData(); //Player Stats save.playerSaveState.LoadData(); //Relationship stats AnimalStats.LoadStats(); RelationshipStats.LoadStats(); } }
Summary
In this article, we added tool animations to our player and a stamina system to our game.
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).
Here is the final code for all the scripts we have worked on today:
PlayerInteraction.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; public class PlayerInteraction : MonoBehaviour { PlayerController playerController; Animator animator; EquipmentData equipmentTool; //The land the player is currently selecting Land selectedLand = null; //The interactable object the player is currently selecting InteractableObject selectedInteractable = null; // Start is called before the first frame update void Start() { //Get access to our PlayerController component playerController = transform.parent.GetComponent<PlayerController>(); animator = GameObject.FindGameObjectWithTag("Player").GetComponent<Animator>(); } // Update is called once per frame void Update() { RaycastHit hit; if(Physics.Raycast(transform.position, Vector3.down,out hit, 1)) { OnInteractableHit(hit); } } //Handles what happens when the interaction raycast hits something interactable void OnInteractableHit(RaycastHit hit) { Collider other = hit.collider; //Check if the player is going to interact with land if(other.tag == "Land") { //Get the land component Land land = other.GetComponent<Land>(); SelectLand(land); return; } //Check if the player is going to interact with an Item if(other.tag == "Item") { //Set the interactable to the currently selected interactable selectedInteractable = other.GetComponent<InteractableObject>(); selectedInteractable.OnHover(); return; } //Deselect the interactable if the player is not standing on anything at the moment if(selectedInteractable != null) { selectedInteractable.OnMoveAway(); selectedInteractable = null; } //Deselect the land if the player is not standing on any land at the moment if(selectedLand != null) { selectedLand.Select(false); selectedLand = null; } } //Handles the selection process of the land void SelectLand(Land land) { //Set the previously selected land to false (If any) if (selectedLand != null) { selectedLand.Select(false); } //Set the new selected land to the land we're selecting now. selectedLand = land; land.Select(true); } //Triggered when the player presses the tool button public void Interact() { //Detecting what is the current tool that the player is holding ItemData toolSlot = InventoryManager.Instance.GetEquippedSlotItem(InventorySlot.InventoryType.Tool); EquipmentData equipmentTool = toolSlot as EquipmentData; //The player shouldn't be able to use his tool when he has his hands full with an item if(InventoryManager.Instance.SlotEquipped(InventorySlot.InventoryType.Item)) { return; } //Check if the player is selecting any land if(selectedLand != null) { EquipmentData.ToolType toolType = equipmentTool.toolType; switch (toolType) { case EquipmentData.ToolType.WateringCan: PlayerStats.UseStamina(2); animator.SetTrigger("Watering"); selectedLand.Interact(); return; case EquipmentData.ToolType.Hoe: PlayerStats.UseStamina(2); animator.SetTrigger("Plowing"); selectedLand.Interact(); return; } return; } Debug.Log("Not on any land!"); } //Triggered when the player presses the item interact button public void ItemInteract() { //If the player isn't holding anything, pick up an item //Check if there is an interactable selected if (selectedInteractable != null) { //Pick it up selectedInteractable.Pickup(); } } public void ItemKeep() { //If the player is holding something, keep it in his inventory if (InventoryManager.Instance.SlotEquipped(InventorySlot.InventoryType.Item)) { InventoryManager.Instance.HandToInventory(InventorySlot.InventoryType.Item); return; } } }
UIManager.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using TMPro; public class UIManager : MonoBehaviour, ITimeTracker { public static UIManager Instance { get; private set; } [Header("Screen Management")] public GameObject menuScreen; public enum Tab { Inventory, Relationships, Animals } //The current selected tab public Tab selectedTab; [Header("Status Bar")] //Tool equip slot on the status bar public Image toolEquipSlot; //Tool Quantity text on the status bar public TextMeshProUGUI toolQuantityText; //Time UI public TextMeshProUGUI timeText; public TextMeshProUGUI dateText; [Header("Inventory System")] //The inventory panel public GameObject inventoryPanel; //The tool equip slot UI on the Inventory panel public HandInventorySlot toolHandSlot; //The tool slot UIs public InventorySlot[] toolSlots; //The item equip slot UI on the Inventory panel public HandInventorySlot itemHandSlot; //The item slot UIs public InventorySlot[] itemSlots; [Header("Item info box")] public GameObject itemInfoBox; public TextMeshProUGUI itemNameText; public TextMeshProUGUI itemDescriptionText; [Header("Screen Transitions")] public GameObject fadeIn; public GameObject fadeOut; [Header("Prompts")] public YesNoPrompt yesNoPrompt; public NamingPrompt namingPrompt; [SerializeField] InteractBubble interactBubble; [Header("Player Stats")] public TextMeshProUGUI moneyText; [Header("Shop")] public ShopListingManager shopListingManager; [Header("Relationships")] public RelationshipListingManager relationshipListingManager; public AnimalListingManager animalRelationshipListingManager; [Header("Calendar")] public CalendarUIListing calendar; [Header("Stamina")] public Sprite[] staminaUI; public Image StaminaUIImage; public int staminaCount; 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; } } private void Start() { PlayerStats.RestoreStamina(); RenderInventory(); AssignSlotIndexes(); RenderPlayerStats(); DisplayItemInfo(null); //Add UIManager to the list of objects TimeManager will notify when the time updates TimeManager.Instance.RegisterTracker(this); } #region Prompts public void TriggerNamingPrompt(string message, System.Action<string> onConfirmCallback) { //Check if another prompt is already in progress if (namingPrompt.gameObject.activeSelf) { //Queue the prompt namingPrompt.QueuePromptAction(() => TriggerNamingPrompt(message, onConfirmCallback)); return; } //Open the panel namingPrompt.gameObject.SetActive(true); namingPrompt.CreatePrompt(message, onConfirmCallback); } public void TriggerYesNoPrompt(string message, System.Action onYesCallback) { //Set active the gameobject of the Yes No Prompt yesNoPrompt.gameObject.SetActive(true); yesNoPrompt.CreatePrompt(message, onYesCallback); } #endregion #region Tab Management public void ToggleMenuPanel() { menuScreen.SetActive(!menuScreen.activeSelf); OpenWindow(selectedTab); TabBehaviour.onTabStateChange?.Invoke(); } //Manage the opening of windows associated with the tab public void OpenWindow(Tab windowToOpen) { //Disable all windows relationshipListingManager.gameObject.SetActive(false); inventoryPanel.SetActive(false); animalRelationshipListingManager.gameObject.SetActive(false); //Open the corresponding window and render it switch (windowToOpen) { case Tab.Inventory: inventoryPanel.SetActive(true); RenderInventory(); break; case Tab.Relationships: relationshipListingManager.gameObject.SetActive(true); relationshipListingManager.Render(RelationshipStats.relationships); break; case Tab.Animals: animalRelationshipListingManager.gameObject.SetActive(true); animalRelationshipListingManager.Render(AnimalStats.animalRelationships); break; } //Set the selected tab selectedTab = windowToOpen; } #endregion #region Fadein Fadeout Transitions public void FadeOutScreen() { fadeOut.SetActive(true); } public void FadeInScreen() { fadeIn.SetActive(true); } public void OnFadeInComplete() { //Disable Fade in Screen when animation is completed fadeIn.SetActive(false); } //Reset the fadein fadeout screens to their default positions public void ResetFadeDefaults() { fadeOut.SetActive(false); fadeIn.SetActive(true); } #endregion #region Inventory //Iterate through the slot UI elements and assign it its reference slot index public void AssignSlotIndexes() { for (int i =0; i<toolSlots.Length; i++) { toolSlots[i].AssignIndex(i); itemSlots[i].AssignIndex(i); } } //Render the inventory screen to reflect the Player's Inventory. public void RenderInventory() { //Get the respective slots to process ItemSlotData[] inventoryToolSlots = InventoryManager.Instance.GetInventorySlots(InventorySlot.InventoryType.Tool); ItemSlotData[] inventoryItemSlots = InventoryManager.Instance.GetInventorySlots(InventorySlot.InventoryType.Item); //Render the Tool section RenderInventoryPanel(inventoryToolSlots, toolSlots); //Render the Item section RenderInventoryPanel(inventoryItemSlots, itemSlots); //Render the equipped slots toolHandSlot.Display(InventoryManager.Instance.GetEquippedSlot(InventorySlot.InventoryType.Tool)); itemHandSlot.Display(InventoryManager.Instance.GetEquippedSlot(InventorySlot.InventoryType.Item)); //Get Tool Equip from InventoryManager ItemData equippedTool = InventoryManager.Instance.GetEquippedSlotItem(InventorySlot.InventoryType.Tool); //Text should be empty by default toolQuantityText.text = ""; //Check if there is an item to display if (equippedTool != null) { //Switch the thumbnail over toolEquipSlot.sprite = equippedTool.thumbnail; toolEquipSlot.gameObject.SetActive(true); //Get quantity int quantity = InventoryManager.Instance.GetEquippedSlot(InventorySlot.InventoryType.Tool).quantity; if (quantity > 1) { toolQuantityText.text = quantity.ToString(); } return; } toolEquipSlot.gameObject.SetActive(false); } //Iterate through a slot in a section and display them in the UI void RenderInventoryPanel(ItemSlotData[] slots, InventorySlot[] uiSlots) { for (int i = 0; i < uiSlots.Length; i++) { //Display them accordingly uiSlots[i].Display(slots[i]); } } public void ToggleInventoryPanel() { //If the panel is hidden, show it and vice versa inventoryPanel.SetActive(!inventoryPanel.activeSelf); RenderInventory(); } //Display Item info on the Item infobox public void DisplayItemInfo(ItemData data) { //If data is null, reset if(data == null) { itemNameText.text = ""; itemDescriptionText.text = ""; itemInfoBox.SetActive(false); return; } itemInfoBox.SetActive(true); itemNameText.text = data.name; itemDescriptionText.text = data.description; } #endregion #region Time //Callback to handle the UI for time public void ClockUpdate(GameTimestamp timestamp) { //Handle the time //Get the hours and minutes int hours = timestamp.hour; int minutes = timestamp.minute; //AM or PM string prefix = "AM "; //Convert hours to 12 hour clock if (hours >= 12) { //Time becomes PM prefix = "PM "; //12 PM and later hours = hours - 12; Debug.Log(hours); } //Special case for 12am/pm to display it as 12 instead of 0 hours = hours == 0 ? 12 : hours; //Format it for the time text display timeText.text = prefix + hours + ":" + minutes.ToString("00"); //Handle the Date int day = timestamp.day; string season = timestamp.season.ToString(); string dayOfTheWeek = timestamp.GetDayOfTheWeek().ToString(); //Format it for the date text display dateText.text = season + " " + day + " (" + dayOfTheWeek +")"; } #endregion //Render the UI of the player stats in the HUD public void RenderPlayerStats() { moneyText.text = PlayerStats.Money + PlayerStats.CURRENCY; staminaCount = PlayerStats.Stamina; ChangeStaminaUI(); } //Open the shop window with the shop items listed public void OpenShop(List<ItemData> shopItems) { //Set active the shop window shopListingManager.gameObject.SetActive(true); shopListingManager.Render(shopItems); } public void ToggleRelationshipPanel() { GameObject panel = relationshipListingManager.gameObject; panel.SetActive(!panel.activeSelf); //If open, render the screen if (panel.activeSelf) { relationshipListingManager.Render(RelationshipStats.relationships); } } public void InteractPrompt(Transform item, string message, float offset) { interactBubble.gameObject.SetActive(true); interactBubble.transform.position = item.transform.position + new Vector3(0, offset, 0); interactBubble.Display(message); } public void DeactivateInteractPrompt() { interactBubble.gameObject.SetActive(false); } public void ChangeStaminaUI() { if (staminaCount <= 45) StaminaUIImage.sprite = staminaUI[3]; // exhausted else if (staminaCount <= 80) StaminaUIImage.sprite = staminaUI[2]; // tired else if (staminaCount <= 115) StaminaUIImage.sprite = staminaUI[1]; // active else if (staminaCount <= 150) StaminaUIImage.sprite = staminaUI[0]; // energised } }
PlayerSaveState.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; [System.Serializable] public class PlayerSaveState { //In future we will collect information like achievements, shipping history, building assets/upgrades and the like here. public int money; public int stamina; public PlayerSaveState(int money, int stamina) { this.money = money; this.stamina = stamina; this.stamina = stamina; } public void LoadData() { PlayerStats.LoadStats(money, stamina); } public static PlayerSaveState Export() { return new PlayerSaveState(PlayerStats.Money, PlayerStats.Stamina); } }
GameStateManager.cs
using System; using System.Collections; using System.Collections.Generic; using System.Runtime.InteropServices.WindowsRuntime; using UnityEngine; using UnityEngine.Events; public class GameStateManager : MonoBehaviour, ITimeTracker { public static GameStateManager Instance { get; private set; } //Check if the screen has finished fading out bool screenFadedOut; //To track interval updates private int minutesElapsed = 0; //Event triggered every 15 minutes public UnityEvent onIntervalUpdate; [SerializeField] GameBlackboard blackboard = new GameBlackboard(); //This blackboard will not be saved GameBlackboard sceneItemsBoard = new GameBlackboard(); const string TIMESTAMP = "Timestamp"; public GameBlackboard GetBlackboard() { return blackboard; } 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; } } // Start is called before the first frame update void Start() { //Add this to TimeManager's Listener list TimeManager.Instance.RegisterTracker(this); SceneTransitionManager.Instance.onLocationLoad.AddListener(RenderPersistentObjects); } public void ClockUpdate(GameTimestamp timestamp) { UpdateShippingState(timestamp); UpdateFarmState(timestamp); IncubationManager.UpdateEggs(); blackboard.SetValue(TIMESTAMP, timestamp); if (timestamp.hour == 0 && timestamp.minute == 0) { OnDayReset(); } if(minutesElapsed >= 15) { minutesElapsed = 0; onIntervalUpdate?.Invoke(); } else { minutesElapsed++; } } //Called when the day has been reset void OnDayReset() { Debug.Log("Day has been reset"); foreach(NPCRelationshipState npc in RelationshipStats.relationships) { npc.hasTalkedToday = false; npc.giftGivenToday = false; } AnimalFeedManager.ResetFeedboxes(); AnimalStats.OnDayReset(); } void UpdateShippingState(GameTimestamp timestamp) { //Check if the hour is here (Exactly 1800 hours) if(timestamp.hour == ShippingBin.hourToShip && timestamp.minute == 0) { ShippingBin.ShipItems(); } } void UpdateFarmState(GameTimestamp timestamp) { //Update the Land and Crop Save states as long as the player is outside of the Farm scene if (SceneTransitionManager.Instance.currentLocation != SceneTransitionManager.Location.Farm) { //If there is nothing to update to begin with, stop if (LandManager.farmData == null) return; //Retrieve the Land and Farm data from the static variable List<LandSaveState> landData = LandManager.farmData.Item1; List<CropSaveState> cropData = LandManager.farmData.Item2; //If there are no crops planted, we don't need to worry about updating anything if (cropData.Count == 0) return; for (int i = 0; i < cropData.Count; i++) { //Get the crop and corresponding land data CropSaveState crop = cropData[i]; LandSaveState land = landData[crop.landID]; //Check if the crop is already wilted if (crop.cropState == CropBehaviour.CropState.Wilted) continue; //Update the Land's state land.ClockUpdate(timestamp); //Update the crop's state based on the land state if (land.landStatus == Land.LandStatus.Watered) { crop.Grow(); } else if (crop.cropState != CropBehaviour.CropState.Seed) { crop.Wither(); } //Update the element in the array cropData[i] = crop; landData[crop.landID] = land; } /*LandManager.farmData.Item2.ForEach((CropSaveState crop) => { Debug.Log(crop.seedToGrow + "\n Health: " + crop.health + "\n Growth: " + crop.growth + "\n State: " + crop.cropState.ToString()); });*/ } } //Instantiate the GameObject such that it stays in the scene even after the player leaves public GameObject PersistentInstantiate(GameObject gameObject, Vector3 position, Quaternion rotation) { GameObject item = Instantiate(gameObject, position, rotation); //Save it to the blackboard (GameObject, Vector3) itemInformation = (gameObject, position); List<(GameObject, Vector3)> items = sceneItemsBoard.GetOrInitList<(GameObject, Vector3)>(SceneTransitionManager.Instance.currentLocation.ToString()); items.Add(itemInformation); sceneItemsBoard.Debug(); return item; } public void PersistentDestroy(GameObject item) { if (sceneItemsBoard.TryGetValue(SceneTransitionManager.Instance.currentLocation.ToString(), out List<(GameObject, Vector3)> items)) { int index = items.FindIndex(i => i.Item2 == item.transform.position); items.RemoveAt(index); } Destroy(item); } void RenderPersistentObjects() { Debug.Log("Rendering Persistent Objects"); if (sceneItemsBoard.TryGetValue(SceneTransitionManager.Instance.currentLocation.ToString(), out List<(GameObject, Vector3)> items)) { foreach((GameObject, Vector3) item in items) { Instantiate(item.Item1, item.Item2, Quaternion.identity); } } } public void Sleep() { //Call a fadeout UIManager.Instance.FadeOutScreen(); screenFadedOut = false; StartCoroutine(TransitionTime()); //Restore Stamina fully when sleeping PlayerStats.RestoreStamina(); } IEnumerator TransitionTime() { //Calculate how many ticks we need to advance the time to 6am //Get the time stamp of 6am the next day GameTimestamp timestampOfNextDay = TimeManager.Instance.GetGameTimestamp(); timestampOfNextDay.day += 1; timestampOfNextDay.hour = 6; timestampOfNextDay.minute = 0; Debug.Log(timestampOfNextDay.day + " " + timestampOfNextDay.hour + ":" + timestampOfNextDay.minute); //Wait for the scene to finish fading out before loading the next scene while (!screenFadedOut) { yield return new WaitForSeconds(1f); } TimeManager.Instance.SkipTime(timestampOfNextDay); //Save SaveManager.Save(ExportSaveState()); //Reset the boolean screenFadedOut = false; UIManager.Instance.ResetFadeDefaults(); } //Called when the screen has faded out public void OnFadeOutComplete() { screenFadedOut = true; } public GameSaveState ExportSaveState() { //Retrieve Farm Data FarmSaveState farmSaveState = FarmSaveState.Export(); //Retrieve inventory data InventorySaveState inventorySaveState = InventorySaveState.Export(); PlayerSaveState playerSaveState = PlayerSaveState.Export(); //Time GameTimestamp timestamp = TimeManager.Instance.GetGameTimestamp(); //Relationships //RelationshipSaveState relationshipSaveState = RelationshipSaveState.Export(); return new GameSaveState(blackboard, farmSaveState, inventorySaveState, timestamp, playerSaveState); } public void LoadSave() { //Retrieve the loaded save GameSaveState save = SaveManager.Load(); //Load up the parts blackboard = save.blackboard; blackboard.Debug(); //Time TimeManager.Instance.LoadTime(save.timestamp); //Inventory save.inventorySaveState.LoadData(); //Farming data save.farmSaveState.LoadData(); //Player Stats save.playerSaveState.LoadData(); //Relationship stats AnimalStats.LoadStats(); RelationshipStats.LoadStats(); //save.relationshipSaveState.LoadData(); } }
PlayerStats.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; public class PlayerStats { public static int Money { get; private set; } public static int Stamina; public const string CURRENCY = "G"; public static void Spend(int cost) { //Check if the player has enough to spend if(cost > Money) { Debug.LogError("Player does not have enough money"); return; } Money -= cost; UIManager.Instance.RenderPlayerStats(); } public static void Earn(int income) { Money += income; UIManager.Instance.RenderPlayerStats(); } public static void LoadStats(int money, int stamina) { Money = money; Stamina = stamina; UIManager.Instance.RenderPlayerStats(); } public static void UseStamina(int staminaLost) { Stamina -= staminaLost; UIManager.Instance.RenderPlayerStats(); } public static void RestoreStamina() { Stamina = 150; UIManager.Instance.RenderPlayerStats(); } }
AnimEventsTools.cs
using UnityEngine; public class AnimEventsTools : MonoBehaviour { // this script contains the anim events for the spawing of tools when the animation is played // this is the original objects of the tools public GameObject wateringCan; public GameObject Hoe; private GameObject newWateringCan; private GameObject newHoe; public Transform handPoint; public void SpawnWaterCan() { newWateringCan = Instantiate(wateringCan, handPoint); } public void DespawnWaterCan() { Destroy(newWateringCan); } public void SpawnHoe() { newHoe = Instantiate(Hoe, handPoint); } public void DespawnHoe() { Destroy(newHoe); } }