DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

Cover image for Implementing key objects in Unity by a newbie. Part 2.
Ale SΓ‘nchez
Ale SΓ‘nchez

Posted on • Updated on • Originally published at blog.alesanchez.es

Implementing key objects in Unity by a newbie. Part 2.

In the previous part we built the foundations for implementing the key objects.

In this post we are going to finish that task developing the actionable by key specific object and testing everything we developed until now.

In the first part the last step was the 3, so here we will start directly in the 4, for preserving the tags in the repo :)

Step 4: Actionable by key

Let's implement the key (sorry for that) part of the post! Our ActionableByKey class!

As we saw in the diagram, the ActionableByKey object used a feature called events! Events in C# are a way to notify that something happened. We are going to use that to send a message to the universe when an item has been picked. Start by defining the event we'll need.

Create a new folder called "Events" inside the "Scripts" one and put inside a file called "ItemPick":

public delegate void NotifyItemPick(ActionableObject pickedItem);

public static class ItemPickEvents {
    public static event NotifyItemPick OnItemPick;

    public static void NotifyItemPick(ActionableObject pickedItem) {
        OnItemPick?.Invoke(pickedItem);
    }
}
Enter fullscreen mode Exit fullscreen mode

First we define a deletage called NotifyItemPick that will be called with an ActionableObject referencing the picked item.

The class itself will contain the OnItemPick event as static, so anyone can subscribe to it and a static method for notifying an item pick.

We have talked a lot about picking an item and objects that will be interacted when an item is picked, but haven't defined the item itself yet!

In order to represent our item, we need a KeyItem script that will define an object that has to be picked to activate another. Picking an object is some sort of interaction so... we can take advantage of our generic actionable object! Create a new folder called "Items" inside "Scripts" and, inside, create a "KeyItem" file:

public class KeyItem : ActionableObject
{
    protected override void InnerStart(){}

    protected override void InnerInteract() {
        ItemPickEvents.NotifyItemPick(this);
        Destroy(gameObject);
    }
}
Enter fullscreen mode Exit fullscreen mode

As you can see, the inner start does nothing and the interaction just notify that itself has been picked, followed by the descruction of the picked item. Easy!

Now that we have the events as well as the key item, the next step is to create the actionable by key one. Create a file side by side with StandardActionableObject and call it ActionableByKey:

public class ActionableByKey : StandardActionableObject
{
    [Tooltip("This item will activate the actuator of this object when picked")]
    [SerializeField] KeyItem actionedBy;

    protected override void InnerStart()
    {
        base.InnerStart();
        SetIsInteractive(false);
    }

    private void OnEnable() {
        ItemPickEvents.OnItemPick += OnItemPick;
    }

    private void OnDisable() {
        ItemPickEvents.OnItemPick -= OnItemPick;
    }

    private void OnItemPick(ActionableObject pickedItem)
    {
        if(pickedItem is KeyItem && pickedItem.Equals(actionedBy)) {
            SetIsInteractive(true);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

The first attribute is a reference to the key item that need to be picked to activate this object.

The InnerStart deactivates the interactions with this object.

We will subscribe to the OnItemPick event in the OnEnable and unsusbcribe in the OnDisable. The function bound to the event is called also OnItemPick.

In the OnItemPick method we check if the picked item is a KeyItem as well as the one that activates the interactions with the actionable object. If both conditions are met, activate the interactions :D

We are almost there! Just one more thing to code and we can jump back to the Unity editor!

Access the code for this step here

Step 5: How to interact with objects

Ok, we have set everything up so that when we interact with an object it works like a charm but... How can we interact with an object?

That is what we are going to solve here. In general terms, the approach is to throw Raycast from the player eyes (more or less), a unit of distance and a half away and check if it collides with a collider belonging to an IActionableObject GameObject (that's why we set a collider up in our door). If we find an actionable object, we will store a reference to it and show the interaction text. When the key for interacting is pressed, we will call the Interact function of the stored actionable object.

We will create a file called ObjectInteractuator inside the Scripts folder:

public class ObjectInteractuator : MonoBehaviour
{
    [SerializeField] InteractionText interactText;

    [SerializeField] LayerMask interactionMask;

    GameActions actions;
    Camera mainCamera;

    IActionableObject currInteractiveObject;

    private void Awake() {
        actions = new GameActions();
        actions.Player.Interact.performed += OnInteractPerformed;
    }

    private void OnEnable() {
        actions.Player.Enable();
    }

    private void OnDisable() {
        actions.Player.Disable();
    }

    private void OnDestroy() {
        actions.Player.Interact.performed -= OnInteractPerformed;
    }

    private void Start() {
        mainCamera = Camera.main;
    }

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

    private void CheckInteractions()
    {
        Ray cameraRay = mainCamera.ViewportPointToRay(Vector3.one / 2f);

        RaycastHit hit;
        IActionableObject objectHit;

        // We are looking towards an interactive object an in range
        if (Physics.Raycast(cameraRay, out hit, 1.5f, interactionMask, QueryTriggerInteraction.Collide) 
            && hit.transform.TryGetComponent<IActionableObject>(out objectHit) 
            && objectHit.IsInteracterActive()
        ) {
            interactText.SetInteractionText(objectHit.GetInteractionText());
            currInteractiveObject = objectHit;

            interactText.Enable();
        }
        else if (interactText.enabled)
        {
            interactText.Disable();
            currInteractiveObject = null;
        }
    }

    private void OnInteractPerformed(InputAction.CallbackContext obj)
    {
        currInteractiveObject?.Interact();
    }

}
Enter fullscreen mode Exit fullscreen mode

The first attribute is a reference to the InteractionText of the scene (we will create it later). The second is the interaction mask the raycast has to check for performing a hit (you can create a custom layer if you want, but in this example we will use the default one).

The GameActions are needed to listen for event sent when the interaction key is pressed. Finally, we need the Camera to cast the ray from it.

The last attribute is to store a reference to the current interactive object (the one we are looking at right now, if any).

In the Awake we will bind the OnInteractPerformed function to the Interact action, and will unbind it in the OnDestroy function. The only purpose of that function is to call the Interact method of the interactive object we are looking at right now (checking first if there is any).

The OnEnable and OnDisable functions are used to enable and disable the game actions.

In the Start we are storing the reference to the main camera and in the Update we are going to check for interactions.

The CheckInteractions is where the magic happens, let's look into it step by step.

First we construct a Ray that starts at the middle point of the viewport (that's why we use Vector.One / 2f).

Then we create two variables to store the hit of the raycast as well as the actionable object.

Then we have an if with three conditions. The first one:

Physics.Raycast(cameraRay, out hit, 1.5f, interactionMask, QueryTriggerInteraction.Collide)
Enter fullscreen mode Exit fullscreen mode

This casts the previously created ray 1.5 units of distance away, storing the possible hit in the hit variable, colliding only with interactionMask and reporting with trigger colliders thanks to the QueryTriggerInteraction.Collide attribute. If that condition is met (an object has been hit), then it goes for the second one:

hit.transform.TryGetComponent<IActionableObject>(out objectHit)
Enter fullscreen mode Exit fullscreen mode

That one asks if the collided object has a component implementing the interface IActionableObject. If so, it stores the found component in the objectHit variable and goes for the last check:

objectHit.IsInteracterActive()
Enter fullscreen mode Exit fullscreen mode

This one just asks if the interactions are enabled for the collided object.

If all of that is met, we set the interaction text to the one returned by the collided object, store a reference to the collided object and enable the interact text component.

If any of the conditions fail it means that we are not looking to an interactive object, so we disable the interaction text and clear the reference.

And with that, we have all set for interacting with objects!! Let's move again to the editor for the final touches.

Access the code for this step here

Step 6: Attaching all needed scripts

Now it's time to start adding scripts to all of the GameObjects in the scene.

As we said earlier, we must create the elements needed for showing the interaction text. We are going to use an overlay canvas with a text. That text will be white so that it makes more contrast with the background.

After creating the canvas and switching the EventSystem to the new input system, we must attach the InteractionText script to the Text component in order to reference it in the ObjectInteractuator script.

To set up the interactor, we need to attach the ObjectInteractuator script to the FPSController. Then reference the text component we created in the previous step and set the interaction mask to Default.

The last step before we can begin to interact with our door is to make it a StandardActionableObject (for the moment).

Before that, we must fix a thing we forgot when creating the animation in Part 1. We need to make sure that the animation is not played on loop. For that, go to the animation file and uncheck the "Loop time" attribute. Then we just need to attach the StandardActionableObject script to it and fill the animation state name, which is "DoorOpen".

Now we are able to play around with our door opening and closing it. Our very last step is to make it actionable by a key!!

Access the code developed in this step here

NOTE: I changed the colliders of the doors so the player can walk across it. I created one trigger collider around the whole door for the raycast and one collider in the door itself to prevent the player from crossing it when closed.

Step 7: Making the door actionable by cube key

The last thing we need to finish our heroic journey is making the door interactive only when a specific item is picked. In a real game, that item would be a key, in our amazing AAA game, it will be the most cubic cube you'll ever see.

The first step is to place our cube and make it a KeyItem. Since it will be picked up and destroyed, we don't need any reverse interaction text.

And the last thing... Replace the script attached to the door with an ActionableByKey script and set the reference to our key. That's all! The door won't be interactive until we pick up the key!

Access the code of this step here

Conclussions... or something like that

So... yes... it's finished, it's working, it's awesome... I'm not crying, you're crying.

Now seriously, I hope you enjoyed the journey reading and following the steps as much as I enjoyed writing the posts and recording the videos.

As in the first part, feel free to make any suggestions, improvements, doubts, requests or whatever you want to say!

See you in the next post!

Top comments (0)

🌚 Life is too short to browse without dark mode