To ensure I stay fresh with game engines like Unity, sometimes I set myself a challenge. This time I wanted to see if I could create an Interact system in a day. Also see below for updates on my UE5 game!
By making an Interact Interface, it would allow any class that implements it to define their own behaviours for when an interaction happens, and what conditions must be met. There are Pickups, that the player can only hold one at a time. There are also buttons that shoot fireworks, that can still be activated even while holding an object. I also added an interactable component for shared functionality, including the outline that all interactable objects have.
The interactor keeps a list of all the interactable objects and will add and remove them based on the OnTriggerEnter and OnTriggerExit events. This ensures that once an object is thrown, any other objects we are overlapping with can immediately become eligible for interaction again.
This was a fun challenge! Since I work on a custom engine at work, and am making my own game in UE5, it's good to be able to transfer my skills back to Unity, which I used to make a lot of my projects in.
using UnityEngine;
public interface IInteractInterface
{
bool CanInteract(GameObject interactor) { return true; }
void Interact(GameObject interactor) { }
}
using UnityEngine;
public class Pickup : MonoBehaviour, IInteractInterface
{
public bool CanInteract(GameObject interactor)
{
PickUpThrow pickUpThrow = interactor.GetComponentInChildren();
return pickUpThrow != null && pickUpThrow.ActivePickup == null;
}
public void Interact(GameObject interactor)
{
PickUpThrow pickUpThrow = interactor.GetComponentInChildren();
if (pickUpThrow != null)
{
pickUpThrow.Pickup(gameObject);
}
}
}
using NUnit.Framework;
using StarterAssets;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;
public class Interactor : MonoBehaviour
{
[SerializeField]
private float lookingAtInteractableMaxAngle = 30f;
private List overlappedInteractables = new List();
private Interactable currentPotentialInteractable;
public void OnEnable()
{
StarterAssetsInputs.OnInteractEvent += OnInteractEvent;
}
public void OnDisable()
{
StarterAssetsInputs.OnInteractEvent -= OnInteractEvent;
currentPotentialInteractable = null;
}
public void Update()
{
if (overlappedInteractables.Count == 0)
{
return;
}
Vector3 interactorPos = Camera.main.transform.position;
float bestDistance = -1f;
Interactable bestInteractable = null;
foreach (Interactable interactable in overlappedInteractables)
{
Vector3 interactablePos = interactable.transform.position;
float interactableDistance = Vector3.Distance(interactorPos, interactable.transform.position);
IInteractInterface interactInterface = interactable.GetComponent();
bool lookingAtInteractable = Vector3.Angle(Camera.main.transform.forward, interactablePos - interactorPos) <= lookingAtInteractableMaxAngle;
if (lookingAtInteractable && interactInterface.CanInteract(gameObject) && (bestDistance == -1f || interactableDistance < bestDistance))
{
bestDistance = interactableDistance;
bestInteractable = interactable;
}
}
if (bestInteractable && bestInteractable != currentPotentialInteractable)
{
if (currentPotentialInteractable)
{
currentPotentialInteractable.SetPotentialInteractionEnabled(false);
}
currentPotentialInteractable = bestInteractable;
bestInteractable.SetPotentialInteractionEnabled(true);
}
}
private void OnInteractEvent()
{
if (currentPotentialInteractable == null)
{
return;
}
currentPotentialInteractable.GetComponent().Interact(gameObject);
currentPotentialInteractable.SetPotentialInteractionEnabled(false);
currentPotentialInteractable = null;
}
public void OnTriggerEnter(Collider other)
{
if (!other.CompareTag("Interact"))
{
return;
}
GameObject otherGameobject = other.gameObject;
IInteractInterface otherInteractInterface = otherGameobject.GetComponentInParent();
Interactable interactable = otherGameobject.GetComponentInParent();
if (otherInteractInterface == null || interactable == null || !otherInteractInterface.CanInteract(gameObject))
{
Assert.IsNotNull(otherInteractInterface, "Interactable parent %s does not implement the Interact Interface!", otherGameobject.name);
return;
}
if (!overlappedInteractables.Contains(interactable))
{
overlappedInteractables.Add(interactable);
}
}
public void OnTriggerExit(Collider other)
{
if (!other.CompareTag("Interact"))
{
return;
}
Interactable interactable = other.gameObject.GetComponentInParent();
if (interactable == null)
{
return;
}
if (interactable == currentPotentialInteractable)
{
interactable.SetPotentialInteractionEnabled(false);
currentPotentialInteractable = null;
}
if (overlappedInteractables.Contains(interactable))
{
overlappedInteractables.Remove(interactable);
}
}
}
Back to UE5 and my track placing adventure game! Recently I've been focusing on NPCs and their behaviours. You're able to start dialogue with them and make choices that will lead to them either following you, or asking them to move to a location on Nav Mesh.
The cool part is that if you tell them where to go, they will build tracks in order to get there! My aim is to create companions with different abilities that will aide you on your journey, aswell as dialogue options that are locked behind prerequisites.
However there are other options... If you blow up tracks with a bomb, the other track pieces will fall and connect to make a bridge! I like the idea of having multiple options to solve a challenge. It's another way of adding emergent gameplay for fun and unexpected results.
This is the result of a refactor I've done to the tracks to make them modular. Each piece is now one actor as opposed to one actor comprised of multiple spline points. It was an interesting challenge to apply the fix to existing tracks in my levels!