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