The item interaction. Log #17 (Unity, C#)
18.03.2018

    Weapon is created, but to interact with it, meaning to pick up, add into inventory or drop it, I will need script this behaviour. Also physics is necessary, to interact with surroundings. So I am going to create prefab out of the 3D model and set all this behaviour through components. To load the item into game I will use this prefab, which is going to be saved in ScriptableObject as reference.

    First step is to create ScriptableObject of my Axe. In folder Editor in Assets I create script called WeaponAsset, which creates menu item Weapon in context menu. So when I right click in folder ObjectsData/Items/Weapons I got new weapon asset under Create > Item > Weapon. I rename it to Axe and fill the fields like Item Name, Description, et cetera, and to field Item Icon I drag my icon created in previous log, fill the rest of the attributes, but I leave Item Prefab empty for now. Here is my asset class.

 

using UnityEditor;

public class WeaponAsset
{
    [MenuItem("Assets/Create/Item/Weapon")]
    public static void CreateAsset()
    {
        ScriptableObjectUtility.CreateAsset<Weapon>();
    }
}

 

    Now back to the prefab, prefab is asset of any object that has set its properties and components, it acts as a template of any object. Exactly what I need for my items. Every item will have physics and script for interaction. I will start with the simplier, physics. So, what do I mean by that? Physics of item will ensure, that it won't fall through ground, or levitate and that it can sit on ground or another object. Only things I need to add are MeshCollider and RigidBody components. Then set Mesh Collider to be convex, so the mesh of the collider would be simplier and faster. RigidBody provides gravity for item and there is no need to set anything in it for now. So when I run the game, my axe will fall on the ground.

    Now for the interaction itself. When the player clicks on the item, character will come to the item first, then pick it up. But on the inside, whole interaction is a little more complex. First it needs to distinguish between NPC, enemy or item and then act. When it can tell the clicked object is item to interact, it has to send the character there first, then pick it up. When the character picks up the item, it has to disappear from scene, but it still needs to have a saved reference to the item. My ScriptableObject will do. So it will store my item data all the time, and when player drops the item, the object will be created again from this reference.

    I will start with creating script Interactable. Then I add two virtual methods, Awake() and Interact(GameObject). Virtual because another class will inherit from this class and I will need to override both methods. In method Awake() I set layermask to 11 (in my case) by gameObject.layer = 11. Interact(GameObject) method I leave empty. Layer masks are created in object inspector.

 

using UnityEngine;

public class Interactable : MonoBehaviour
{
    protected virtual void Awake()
    {
        gameObject.layer = 11;
    }

    public virtual void Interact(GameObject character){}
}

 

    Next step is to create script ItemInteraction, which will deal with interaction with concrete item and determines how will the item interact. In case of this item, character will just pick it up, destroys it and adds it to the inventory (in next log). But it has to inherit from Interactable. At the top of the class I add public variable Item itemData, that will store my ScriptableObject and its data. Method Awake() with key word new will contatin calling of the superior method base.Awake() and Interact(GameObject) method with override key word invokes Destroy(GameObject), for now.

 

using UnityEngine;

public class ItemInteraction : Interactable {

    public Item itemData;

    protected new void Awake()
    {
        base.Awake();
    }

    public override void Interact(GameObject character)
    {
        Destroy(gameObject);
    }
}

 

    Now the complex part. I have to solve how to call the character first and interact with it after. For this I use events and delegates, which tells me when the character reached the destination. The instace of the delegate of my event I will add only when player clicks on the interactable object. If player clicks somewhere else it has to remove the instance of the delegate from delegates list. This prevents the case when the player clicks on interactable item but then clicks on the ground, so the event won't start when character reaches its destination. To know that character reached the destination I add the delegate and the event in PlayerMotor script. Above the class I add public delegate void DestinationReached(), this declares new delegate, and then in class I add event with my delegate type public event DestinationReached Reached. Now I need to say when (where) the event should be invoked. It is going to be in if statement for ending the path pending.

 

using UnityEngine;
using UnityEngine.AI;
using UnityEngine.EventSystems;

public delegate void DestinationReached();

[RequireComponent(typeof(NavMeshAgent))]
[RequireComponent(typeof(PlayerController))]
public class PlayerMotor : MonoBehaviour
{
    public event DestinationReached Reached;

    NavMeshAgent agent;
    bool pending;
    Vector3 target;

    void Start()
    {
        agent = GetComponent<NavMeshAgent>();
    }

    public void MoveToObject(Vector3 position)
    {
        GetComponent<CharacterAnimation>().StopAllAnimations();
        pending = true;
        agent.SetDestination(position);
    }

    void Update()
    {
        if (pending && agent.remainingDistance <= agent.stoppingDistance)
        {
            if (Reached != null) Reached();
            pending = false;
        }
    }
}

 

    Now back to the PlayerController class, where I will store the reference of Interactable component of the clicked object into local variable Interactable _object in if statement for my interactionMask. Now I add method Interact() above the Update() method. In this method I call Interact(GameObject) on my clicked item, then I have to remove the delegate instance. Below saving the interactable component reference I add line with adding the instance of delegate of my event, with parameter of my created Interact() method, to the list and send the character to the destination of the object. Eventually I save instance of my clicked object. Now to if statement of my movementMask I need to add removing of my delegate of my event, in case the player would click on the ground after clicking on interactable item. 

 

using UnityEngine;
using UnityEngine.AI;
using UnityEngine.EventSystems;

[RequireComponent(typeof(NavMeshAgent))]
public class PlayerController : MonoBehaviour
{
    public LayerMask movementMask;      // The ground
    public LayerMask interactionMask;   // Everything we can interact with
    public float mouseClickDistance;
    PlayerMotor motor;      // Reference to our motor
    Camera cam;             // Reference to our camera
    RaycastHit hit;

    // Get references
    void Start()
    {
        motor = GetComponent<PlayerMotor>();
        cam = Camera.main;
    }

    void Interact()
    {
        hit.transform.GetComponent<Interactable>().Interact(gameObject);
        GetComponent<PlayerMotor>().Reached -= new DestinationReached(Interact);
    }

    // Update is called once per frame
    void Update()
    {
        // If we press left mouse
        if (Input.GetMouseButtonDown(0))
        {
            // Shoot out a ray
            Ray ray = cam.ScreenPointToRay(Input.mousePosition);
            RaycastHit hit;

            // If we hit walkable layer
            if (Physics.Raycast(ray, out hit, mouseClickDistance, movementMask))
            {
                GetComponent<PlayerMotor>().Reached -= new DestinationReached(Interact);
                motor.MoveToObject(hit.point);
            }

            // If we hit interactable layer
            if (Physics.Raycast(ray, out hit, mouseClickDistance, interactionMask))
            {
                Interactable _object = hit.transform.GetComponent<Interactable>();
                GetComponent<PlayerMotor>().Reached += new DestinationReached(Interact);
                motor.MoveToObject(_object.interactionPoint.position);
                this.hit = hit;
            }
        }
    }
}

 

    The interaction is done. Now I drag ItemInteraction script on my axe, drag the ScriptableObject of my axe to the Item Data field in Item Interaction component, then save my prefab into folder Resources/Objects/Items/Weapons, and finally drag the prefab on Item Prefab in my ScriptableObject of my axe.

 

 

    Player can pick up the weapon now, but it will only disappear from the scene, so I will fix this in next log, and add the item into inventory list.