Mastering Unity Interfaces: A Practical Guide to Teleportation

Why would you want to use an interface in Unity? In this series of articles, we are discussing intermediate Unity concepts. In this article, we will talk about portals, teleportation, and what this has to do with interfaces.

The Challenge: A Simple Portal with Complex Characters

Roughly speaking, the way a portal works is very simple. Both portals have a collider and a portal script. Whenever a collision is detected, the colliding object is teleported to the other portal. Conceptually, this is straightforward, and a first idea to implement this might be something like using the prominent OnTriggerEnter2D method on the portal script to teleport whatever collides with it.

The Problem with a Naive Approach

Can we do that? Well, if teleporting were as simple as just setting the transform to a new position, then sure, that would be no problem. But more often than not, you won't be teleporting inanimate objects and just spitting them out on the other side. Instead, you'll likely teleport game characters who have specific behaviors, and that behavior must be taken into account when teleporting.

In many games, you might have several different species of characters. For instance, a main player character and multiple types of monsters acting as antagonists. Without going too much into the details, they often have totally different behaviors.

  • Player Character: Movement might be based on direct input, checking for walkable neighbor cells.
  • AI Monsters: Movement could be controlled by an AI navigation system that calculates intermediate targets to a final destination.

This means that the act of teleporting is different for the player character and for the monsters. For the sake of argument, we can easily imagine a third type of object that needs to be teleported, like a fireball, which would also have its own unique teleportation logic.

If teleportation means something different for all of those objects, we cannot handle it in a single, generic block of code. We would have to start creating an ugly distinction in the teleport script, which first identifies the type of object and only then implements the specific teleportation logic.

Here's a conceptual example of what that bad practice might look like:

// Inside the Portal Script - AVOID THIS PATTERN
void OnTriggerEnter2D(Collider2D other)
{
    if (other.GetComponent<PlayerController>() != null)
    {
        // Player-specific teleport logic...
    }
    else if (other.GetComponent<MonsterAI>() != null)
    {
        // Monster-specific teleport logic...
    }
    else if (other.GetComponent<Fireball>() != null)
    {
        // Fireball-specific teleport logic...
    }
}

What's even worse is that because teleporting is specific to the object type, we would have to provide a reference from the teleported object to the teleport script, tightly coupling them together. This is a perfect example of creating avoidable coupling, and the solution to this kind of problem can be an interface.

The Solution: Introducing Interfaces

If you remember what was mentioned at the beginning, the first approach was very simple: whenever we detect a collision with an object we want to teleport, just teleport it. This seems naive at first glance, but it's actually the correct line of thinking because it highlights a very important concept at the core of numerous programming issues: How can we deal with things that are the same but different?

What does that mean? For simplicity, let's say that all of our various entities have controllers, and all these controllers have totally different functionality. Clearly, in that sense, our entities are different. But for the teleportation logic, we want to take the perspective that they are actually the same. They can all be teleported—in other words, they are teleportable.

How Interfaces Solve the Problem

An interface is exactly that: a contract. All an interface does is signal to the world that an object can perform a certain action. We want to signal that our objects are teleportable, so we will call the interface ITeleportable.

And what does it mean to be teleportable? You have to be able to do a single, crucial thing: teleport yourself.

Here is the interface definition: csharp public interface ITeleportable { void Teleport(UnityEngine.Vector3 position); } Notice that we are not saying anything about how the individual objects will teleport. All we are saying is that they must provide some way to teleport.

By doing this, we achieve our goal. In our teleport script, we can now use this fact and do what we wanted to in the first place: tell the colliding object to teleport.

The portal's logic becomes clean and decoupled: csharp // Inside the Portal Script - THE RIGHT WAY void OnTriggerEnter2D(UnityEngine.Collider2D other) { var teleportable = other.GetComponent<ITeleportable>(); if (teleportable != null) { // The portal tells the object WHERE to go, // but the object itself knows HOW to get there. // (destinationPortal is a reference to the other portal) teleportable.Teleport(destinationPortal.transform.position); } }

Implementing the ITeleportable Interface

Now, all that's left is to decide which objects we want to be teleportable by implementing the interface and providing their specific teleportation logic.

For the player, it might be a simple position change: csharp public class PlayerController : UnityEngine.MonoBehaviour, ITeleportable { public void Teleport(UnityEngine.Vector3 position) { transform.position = position; } // ... other player logic }

For a monster, it might involve resetting its AI pathfinding: ```csharp public class MonsterAI : UnityEngine.MonoBehaviour, ITeleportable { private UnityEngine.AI.NavMeshAgent agent;

public void Teleport(UnityEngine.Vector3 position)
{
    // Disabling and re-enabling the agent is a common way
    // to teleport NavMesh agents without breaking pathfinding.
    agent.enabled = false;
    transform.position = position;
    agent.enabled = true;
}
// ... other AI logic

} ``` And that is it.

Conclusion: The Power of Perspective

Interfaces allow you to adopt a perspective where objects are treated as the same, even when they are different. This powerful concept works for any kind of functionality. Perhaps you need some objects to be IInteractable or IResetable. In numerous such cases, you will likely want to use an interface.