Creating a farming RPG in Unity - Part 3: Farmland Interaction

Creating a Farming RPG (like Harvest Moon) in Unity — Part 3: Farmland Interaction

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

Correction: In the video, we made a reference to the PlayerController component in the PlayerInteraction class. However, we later found that we didn’t make use of it at least in this part, so you can choose to skip that bit in the video for now, as it is redundant. They are highlighted in red in the finalised codes below.

Ever wanted to create a game like Harvest Moon in Unity? This is Part 3 of our guide, where we go through how to set up farmland elements that our player character will interact with. You can also find Part 2 of our guide here, where we went through how to set up our player camera.


Update (5 September 2021): The narrated audio has been amplified, so it should be much easier to hear now.

1. Finding the textures

Our plots of interactable land will have 3 states:

  1. Soil: The default state of the soil.
  2. Farmland: Land that is plowed from a hoe.
  3. Watered Farmland: Farmland that has been watered with a watering can.

We need a texture for each of the 3 states. To find suitable free textures, we can search Polyhaven for them.

Finding Textures on Poly Haven
All assets on Polyhaven are under the CC0 license, which means they are copyright-free.

When you have found the textures you want, extract and import them into your project. In this tutorial, we place them in the Imported Asset > Farmland folder. We also create a separate folder for each set of textures (i.e. the Watered Farmland textures are under Imported Asset > Farmland > Watered Land).

Textures
How we recommend organising your textures.

In your Scene, create a cube and drag a texture into it. This will generate a Material asset that is automatically assigned to the cube. On the generated material, assign the texture set’s normal texture to its normal map, and the texture with the _disp prefix (e.g. aerial_ground_rock_disp_1k.png) to the height map, as shown in the image below:

Assigning Materials
You can adjust the value beside both the normal and height map textures to emphasise or de-emphasise their effect on your material.

Do the same thing to the other 2 sets of textures. You should end up with 3 materials. Set the cube to use the Soil Material, and make it into a Prefab called Land.

In the materials we made above, we are making use of Albedo, Normal and Height maps to add different kinds of details. To find out more about what kinds of details these maps add, click on the links we have provided.

2. Handling the material changes

Create a new script, Land.cs, and attach it to the prefab. We will have to represent each of the materials we’ve just set up with a corresponding variable, so add the highlighted line below into your script:

Land.cs

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

public class Land : MonoBehaviour
{

    public Material soilMat, farmlandMat, wateredMat;
 


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


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

}

Article continues after the advertisement:


a. Defining constants

In our Land.cs script, we will need to represent the 3 states that a piece of land can have. This can be done by creating a new enumeration type (i.e. enum type), which is used to define a set of constants. In this case, our 3 possible enumerations for our land type — SoilFarmland, and Watered — will be members of an enumerated type called LandStatus.

Land.cs

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

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

    public Material soilMat, farmlandMat, wateredMat;
 


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


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

}

On the Land prefab, assign our materials to their corresponding material variables on the script.

assigning materials to variable

Drag the prefab into the scene so we can test it later.

Land prefab on the Scene

We don’t need the Update() function, so it can be removed. Also, represent the current state of the Land prefab with a variable called landStatus:

Land.cs

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

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

    public LandStatus landStatus;

    public Material soilMat, farmlandMat, wateredMat;
 


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


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

}

b. Processing state changes

When the Land prefab changes its state, the following needs to happen:

  1. The variable landStatus, needs to be updated to reflect the changes
  2. The material of the prefab needs to be updated to reflect the current state.

We can create a function to handle these. In addition to that, we need to access the Renderer component of our GameObject to handle the material changes.

Hence, make the following changes:

Land.cs

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

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

    public LandStatus landStatus;

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

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

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

        }

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


}

When the player first encounters the land, its default state should be Soil. So let’s set that in Start():

Land.cs

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

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

    public LandStatus landStatus;

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

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


    }

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

        }

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


}

3. Setting up the interaction point

Now that we’ve set up the Land script for state changes, we can move on to create a way for the player to interact with it. In the Player prefab, create an empty GameObject and place it slightly in front of the player model. Call it Interactor.

Player interactor GameObject
You can adjust the GameObject’s distance from the player to determine the point of interaction.

Create a new script called PlayerInteraction, and attach it to the Interactor GameObject. By the end of this article, this class should work with other classes to handle player interaction, as summarised in this table:

ClassRole in Interaction
PlayerController.csProcesses keyboard/mouse inputs from the player and directs it to PlayerInteraction
PlayerInteraction.csFigures out which Land instance to interact with (selection).
Land.csHandles state changes based on input from PlayerInteraction.cs
Table of relationships between our classes.

Article continues after the advertisement:


4. Selection

The selection system essentially works by sending a Raycast down the y-axis of the Interactor GameObject.

a. Checking with Raycast

As we will need to check if the player is standing on Land at every frame, we will put this in Update(). Add the following to PlayerInteraction.cs:

PlayerInteraction.cs

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

public class PlayerInteraction : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
       
    }

    // 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)
    {
        Debug.Log(hit);
    }
}

For now, it just debugs the RaycastHit object when the player walks over anything.

raycasthit debug
Not very informative.

b. Distinguishing interactables from non-interactables

We need the function to discriminate Land from other objects. Before that, let’s add a Quad to the scene for testing purposes.

land and not-land
Non-interactable ‘land’ and Interactable Land.

We can set the interactable Land apart from the Quad with the use of Tags. On the Land prefab, go to the Tag dropdown and select Add Tag…

Adding a Tag

Add a new Tag called Land:

Creating a new Tag called 'Land'
Once you name a Tag, it cannot be renamed later.

Go back to the Land prefab and set the tag to the newly created ‘Land’.

Setting Tag to Land

To check the tag of the RaycastHit object, we have to:

  1. Retrieve its Collider component
  2. From there, get its tag property

To this end, we’ll make the following changes:

PlayerInteraction.cs

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

public class PlayerInteraction : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
       
    }

    // 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)
    {
        Debug.Log(hit);
        Collider other = hit.collider;
        
        //Check if the player is going to interact with land
        if(other.tag == "Land")
        {
            Debug.Log("I am standing by farmable land");
        }
    }
}

Now if you were to test it, the console should output “I am standing by farmable land” only when your player is facing the Land prefab.

Debugging the selection of Land

Article continues after the advertisement:


c. Adding feedback

This debug message is not going to cut it, since the game’s console will not be visible to the player. Hence, we need to add some UI elements. On the Land prefab, add a Cube GameObject and set it up like the picture below:

Select GameObject on the Land Prefab

Rename it to ‘Select’ and set it to inactive.

set select to inactive

How it works is simple: It pops up when the player is selecting this Instance of Land. Otherwise, it will be out of sight.

Just to make debugging easier, we’re going to duplicate the Land prefabs like this:

Duping Land Prefabs

Set up a function that handles the selection and deselection of the plot of land:

Land.cs

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

public class Land : MonoBehaviour
{
    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; 

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

    }

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

        }

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

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

}

Assign the Select GameObject to the Select variable in the Inspector.

Assigning gameobject to select variable

Replace the Debug message in PlayerInteraction with a call to use the newly-declared Select function from the Land component:

PlayerInteraction.cs

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

public class PlayerInteraction : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
       
    }

    // 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")
        {
            Debug.Log("I am standing by farmable land");
            //Get the land component
            Land land = other.GetComponent<Land>();
            land.Select(true);
        }
    }
}

Article continues after the advertisement:


d. Selecting and Deselecting

The player is now able to select instances of Land, but as of now, there is no way to deselect it.

A trail of selected Land.

We will need to make the following changes to the selection logic:

  1. The player should only be selecting a maximum of one instance of Land at any point of time.
  2. This should be tracked by a variable.
  3. When the player is selecting a new instance of Land, the previously selected land must be deselected.
  4. The tracking variable should then be set to the newly selected instance of Land.

Since it has gotten more complex, we should move the selection process to a new function, SelectLand():

PlayerInteraction.cs

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

public class PlayerInteraction : MonoBehaviour
{

    //The land the player is currently selecting
    Land selectedLand = null; 

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

    // 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")
        {
            Debug.Log("I am standing by farmable land");
            //Get the land component
            Land land = other.GetComponent<Land>();
            land.Select(true);
            SelectLand(land);
        }
    }

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

}

Now the basic selection mechanism works for the most part. However, a glitch remains:

not selecting anything glitch
When the Player has walked off and is not selecting any new instances of Land, the last selected instance remains selected.

We just need to deselect the previously selected Land instance when the Raycast is not hitting any Land instances:

PlayerInteraction.cs

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

public class PlayerInteraction : MonoBehaviour
{
  
    //The land the player is currently selecting
    Land selectedLand = null; 

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

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

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


}

Note: The return statement is necessary to terminate execution of the function, as we don’t want this newly-implemented deselection logic to override the one from SelectLand!

Don’t forget to remove the BoxCollider component from the Select GameObject or it will conflict with the selection logic.

removing boxcollider from select
We don’t want the Raycast to register the Select UI as something else.

e. Polishing up the UI

Having a white block as our selection UI is fine, but it can look better. Open up your image editing software, and create a square texture with a white border and transparent filling. Alternatively, you can download and use the PNG below.

You can use Photoshop or any image editing software that supports transparency. In our video, we use Paint.NET, a free and lightweight photo editing software for quick and easy edits.

select texture
Save yourself the trouble with a Right-click > Save image as… to download this image.

Import it into the Project, and save it under a new folder Imported Asset/UI.

imported select texture

When you first drag the texture over to the Select Cube, it is white with a black fill:

select texture unconfigured

Just set the material’s Rendering Mode to Transparent, and change the colour of the Albedo to a colour of your choice.

select texture configured
In this tutorial, it’s a red box.

Article continues after the advertisement:


To save yourself the hassle of having to re-enable and disable the Select GameObject to preview your changes, you can just leave the GameObject and rely on code to do the deselection for you:

Land.cs

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

public class Land : MonoBehaviour
{
    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; 

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

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

        }

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

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

}
improved selection feedback
Opinion: A red border looks a lot better than a white one.

5. Finishing up

Now that we have all the classes we need set up with a selection system, here’s a refresher on how the interaction system is going to work:

ClassRole in Interaction
PlayerController.csProcesses keyboard/mouse inputs from the player and directs it to PlayerInteraction
PlayerInteraction.csFigures out which Land instance to interact with (selection).
Land.csHandles state changes based on input from PlayerInteraction.cs
The same table from above.

To keep things tidy, we will place all the interaction-related actions in an Interact() method. Here’s a summary of how it will work when we’re done:

interaction summary
Interact() on the 3 classes.

a. PlayerController

First, we need to pass a reference to PlayerInteraction.cs:

PlayerInteraction.cs

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

public class PlayerController : MonoBehaviour
{
    //Movement Components
    private CharacterController controller; 
    private Animator animator;

    private float moveSpeed = 4f;

    [Header("Movement System")]
    public float walkSpeed = 4f;
    public float runSpeed = 8f;


    //Interaction components
    PlayerInteraction playerInteraction; 

    // Start is called before the first frame update
    void Start()
    {
        //Get movement components
        controller = GetComponent<CharacterController>();
        animator = GetComponent<Animator>();

        //Get interaction component
        playerInteraction = GetComponentInChildren<PlayerInteraction>(); 

    }

    // Update is called once per frame
    void Update()
    {
        //Runs the function that handles all movement
        Move();

    }

    
    public void Move()
    {
        //Get the horizontal and vertical inputs as a number
        float horizontal = Input.GetAxisRaw("Horizontal");
        float vertical = Input.GetAxisRaw("Vertical");

        //Direction in a normalised vector
        Vector3 dir = new Vector3(horizontal, 0f, vertical).normalized;
        Vector3 velocity = moveSpeed * Time.deltaTime * dir;

        //Is the sprint key pressed down?
        if (Input.GetButton("Sprint"))
        {
            //Set the animation to run and increase our movespeed
            moveSpeed = runSpeed;
            animator.SetBool("Running", true);
        } else
        {
            //Set the animation to walk and decrease our movespeed
            moveSpeed = walkSpeed;
            animator.SetBool("Running", false);
        }


        //Check if there is movement
        if (dir.magnitude >= 0.1f)
        {
            //Look towards that direction
            transform.rotation = Quaternion.LookRotation(dir);

            //Move
            controller.Move(velocity); 
            
        }

        //Animation speed parameter
        animator.SetFloat("Speed", velocity.magnitude); 



    }
}

Like what we did for movement, we’ll add a function to handle all interaction inputs in Update(). This will also handle item interactions as well; though for now, we will just have it direct the interaction logic to PlayerInteraction.cs on a Left-click:

PlayerInteraction.cs

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

public class PlayerController : MonoBehaviour
{
    //Movement Components
    private CharacterController controller; 
    private Animator animator;

    private float moveSpeed = 4f;

    [Header("Movement System")]
    public float walkSpeed = 4f;
    public float runSpeed = 8f;


    //Interaction components
    PlayerInteraction playerInteraction; 

    // Start is called before the first frame update
    void Start()
    {
        //Get movement components
        controller = GetComponent<CharacterController>();
        animator = GetComponent<Animator>();

        //Get interaction component
        playerInteraction = GetComponentInChildren<PlayerInteraction>(); 

    }

    // Update is called once per frame
    void Update()
    {
        //Runs the function that handles all movement
        Move();

        //Runs the function that handles all interaction
        Interact();
    }

    public void Interact()
    {
        //Tool interaction
        if (Input.GetButtonDown("Fire1"))
        {
            //Interact
            playerInteraction.Interact(); 
        }

        //TODO: Set up item interaction
    }

    
    public void Move()
    {
        //Get the horizontal and vertical inputs as a number
        float horizontal = Input.GetAxisRaw("Horizontal");
        float vertical = Input.GetAxisRaw("Vertical");

        //Direction in a normalised vector
        Vector3 dir = new Vector3(horizontal, 0f, vertical).normalized;
        Vector3 velocity = moveSpeed * Time.deltaTime * dir;

        //Is the sprint key pressed down?
        if (Input.GetButton("Sprint"))
        {
            //Set the animation to run and increase our movespeed
            moveSpeed = runSpeed;
            animator.SetBool("Running", true);
        } else
        {
            //Set the animation to walk and decrease our movespeed
            moveSpeed = walkSpeed;
            animator.SetBool("Running", false);
        }


        //Check if there is movement
        if (dir.magnitude >= 0.1f)
        {
            //Look towards that direction
            transform.rotation = Quaternion.LookRotation(dir);

            //Move
            controller.Move(velocity); 
            
        }

        //Animation speed parameter
        animator.SetFloat("Speed", velocity.magnitude); 



    }
}

Article continues after the advertisement:


b. PlayerInteraction

There are cases where the player will not be selecting any Land, so we have to check if there is any selected land before directing interaction to it:

PlayerInteraction.cs

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

public class PlayerInteraction : MonoBehaviour
{
    //The land the player is currently selecting
    Land selectedLand = null; 

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

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

        //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()
    {
        //Check if the player is selecting any land
        if(selectedLand != null)
        {
            selectedLand.Interact();
            return; 
        }

        Debug.Log("Not on any land!");
    }
}

c. Land

In the final product, the Player should be able to till, water, and harvest from the Land. However, we have not set up our item and inventory system yet, so for this part, we’ll just make it switch states to Farmland:

Land.cs

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

public class Land : MonoBehaviour
{
    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; 

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

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

        }

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

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

    //When the player presses the interact button while selecting this land
    public void Interact()
    {
        //Interaction 
        SwitchLandStatus(LandStatus.Farmland);
    }
}

Article continues after the advertisement:


Conclusion

final product for part 3
The selected Land changes from Soil to Farmland on a Left-click.

With that, we have a basic interaction system. Here is the final code for all the scripts we have worked with today:

Note: The parts highlighted in red are redundant parts from the video that are omitted in this article.

PlayerInteraction.cs

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

public class PlayerInteraction : MonoBehaviour
{
    PlayerController playerController;
    //The land the player is currently selecting
    Land selectedLand = null; 

    // Start is called before the first frame update
    void Start()
    {
        //Get access to our PlayerController component
        playerController = transform.parent.GetComponent<PlayerController>();
    }

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

        //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()
    {
        //Check if the player is selecting any land
        if(selectedLand != null)
        {
            selectedLand.Interact();
            return; 
        }

        Debug.Log("Not on any land!");
    }
}

Land.cs

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

public class Land : MonoBehaviour
{
    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; 

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

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

        }

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

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

    //When the player presses the interact button while selecting this land
    public void Interact()
    {
        //Interaction 
        SwitchLandStatus(LandStatus.Farmland);
    }
}

PlayerController.cs

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

public class PlayerController : MonoBehaviour
{
    //Movement Components
    private CharacterController controller; 
    private Animator animator;

    private float moveSpeed = 4f;

    [Header("Movement System")]
    public float walkSpeed = 4f;
    public float runSpeed = 8f;


    //Interaction components
    PlayerInteraction playerInteraction; 

    // Start is called before the first frame update
    void Start()
    {
        //Get movement components
        controller = GetComponent<CharacterController>();
        animator = GetComponent<Animator>();

        //Get interaction component
        playerInteraction = GetComponentInChildren<PlayerInteraction>(); 

    }

    // Update is called once per frame
    void Update()
    {
        //Runs the function that handles all movement
        Move();

        //Runs the function that handles all interaction
        Interact();
    }

    public void Interact()
    {
        //Tool interaction
        if (Input.GetButtonDown("Fire1"))
        {
            //Interact
            playerInteraction.Interact(); 
        }

        //TODO: Set up item interaction
    }

    
    public void Move()
    {
        //Get the horizontal and vertical inputs as a number
        float horizontal = Input.GetAxisRaw("Horizontal");
        float vertical = Input.GetAxisRaw("Vertical");

        //Direction in a normalised vector
        Vector3 dir = new Vector3(horizontal, 0f, vertical).normalized;
        Vector3 velocity = moveSpeed * Time.deltaTime * dir;

        //Is the sprint key pressed down?
        if (Input.GetButton("Sprint"))
        {
            //Set the animation to run and increase our movespeed
            moveSpeed = runSpeed;
            animator.SetBool("Running", true);
        } else
        {
            //Set the animation to walk and decrease our movespeed
            moveSpeed = walkSpeed;
            animator.SetBool("Running", false);
        }


        //Check if there is movement
        if (dir.magnitude >= 0.1f)
        {
            //Look towards that direction
            transform.rotation = Quaternion.LookRotation(dir);

            //Move
            controller.Move(velocity); 
            
        }

        //Animation speed parameter
        animator.SetFloat("Speed", velocity.magnitude); 



    }
}

Article continues after the advertisement:


Leave a Reply

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

Note: You can use Markdown to format your comments.

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