DEV Community

Kwesikwaa Hayford
Kwesikwaa Hayford

Posted on

Practical approach to interface, delegate and event in Unity

The explanation of these three key concepts has always been a bit vague to me, often using technical terms that are either confusing or impractical for truly understanding what they’re meant for.

interface
A player interacts with many objects in a game. These objects could be projectiles, lives, enemies, bullets, portals, doors etc. At any point when a player collides with the collider of these other objects, something is expected to happen. In plain language, what is expected to happen depends on which object a player interacts/collides with. However, how a developer chooses to implement it, is what we need to talk about.

if condition/switch is often the first option for handling collisions/triggers as has been demonstrated in the code below.

void OnTriggerEnter(Collider other)
{
    switch (other.gameObject.tag)
    {
        case "Enemy":
            // handle enemy trigger
            Debug.Log("enemy entered trigger");
            break;
        case "Life":
            // handle life trigger
            Debug.Log("gained a life");
            break;
        case "Bullet":
            // handle bullet trigger
            Debug.Log("got shot");
            break;
        default:
            // handle unknown trigger
            Debug.Log("unknown object entered trigger");
            break;
    }
}
Enter fullscreen mode Exit fullscreen mode

This clearly does the job but the question is, is it efficient enough?, especially when several other entities are added; how many checks would have to be done in that case, is it decoupled, the questions are endless.

This is where interfaces come in handy to cleanup the situation. An interface is basically like a class but with methods and other members that are not yet implemented and the word "interface" in place of "class". The cool thing is, any class can inherit as many interfaces as they want and would then be responsible for the implementation of the methods the way its wants. For example, an interface called IBird can has Fly(), Sing(), Eat() as methods to be implemented by Eagle, Parrot and Crow classes. How these classes decide to define these methods depend on how they fly, sing or eat. "I" is mostly the first letter of the interface's name.

public interface ISomething
{
    // c# rule, it is wrong to add public here, the implementing class must add public
    void DoThis();
    void DoThat();
}
Enter fullscreen mode Exit fullscreen mode

It is now the responsibility of any class that chooses to inherit this interface to define what the DoThis() and DoThat() methods should do. that is all !!! that simple !

now let's choose to do this for Enemy, Life and Bullet classes

public class Enemy: MonoBehaviour, ISomething
{
    public void DoThis()
    {
        // define what Enemy's DoThis() should do
    }
    public void DoThat()
    {
        // define what Enemy's DoThat() should do
    }
}
public class Life: MonoBehaviour, ISomething
{
    public void DoThis()
    {
        // define what Life's DoThis() should do
    }
    public void DoThat()
    {
        // define what Life's DoThat() should do
    }
}
public class Bullet: MonoBehaviour, ISomething
{
    public void DoThis()
    {
        // define what Bullet's DoThis() should do
    }
    public void DoThat()
    {
        // define what Bullet's DoThat() should do
    }
}
Enter fullscreen mode Exit fullscreen mode

Each class handles responses its own way decoupled from the player.
The OnTriggerEnter function now looks much cleaner and scalable:

void OnTriggerEnter(Collider other)
{
    ISomething entity = other.GetComponent<ISomething>();
    entity?.DoThis(); // take notice the null-conditional operator(?) :)
}
Enter fullscreen mode Exit fullscreen mode

The code is simpler, decoupled and easy to maintain. Also note that interface names typically describe what they do. For example IInteractable, IKillable, or IDamageable. With this, I believe you can now revisit the topic with a different perspective. Good luck!

delegate and event
There is a GameManager or SoundManager that runs an action or actions when something happens, for example when a player dies. Delegates and events are ideal for this. They allow classes to subscribe to events without being tightly coupled.
A delegate defines a method signature (what a method returns and what parameters it takes), and can reference or point to any method that matches this signature. Event acting as a broadcaster, notifiers those methods that have subscribed to the delegates to take action when an action happens in the order they were added. In practical terms, five methods somewhere are called when a player dies.

public class Player
{
  public delegate void PlayerDied();
  public static event PlayerDied OnPlayerDied;

  // this is static because it is accessing a static event; c# rules :)
  public static void PlayerDiesHere()
  {
      OnPlayerDied?.Invoke();
  }  
}

// subscribers, GameManager and SoundManager
public class GameManager : MonoBehaviour
{
    void OnEnable()
    {
        // the FunctionForPlayerDied() has the same signature as the PlayerDied delegate
        Player.OnPlayerDied += FunctionForPlayerDied;
    }
    void FunctionForPlayerDied()
    {
        // whatever must be done here
    }
    void OnDisable()
    {
        // it is important to unsubscribe
        Player.OnPlayerDied -= FunctionForPlayerDied;
    }

}
public class SoundManager : MonoBehaviour
{
    void OnEnable()
    {
        Player.OnPlayerDied += SoundForPlayerDied;
    }
    void SoundForPlayerDied()
    {
        // do as you please :)
    }
    void OnDisable()
    {
        // never forget to unsubscribe :)
    }
}
Enter fullscreen mode Exit fullscreen mode

It is important to unsubscribe in either OnDisable() or OnDestroy to avoid unexpected behaviour.

In the example above, i defined delegate separately from the event. While that is fine, there is a simpler approach using the built-in Action type.

This:

public class Player
{
  public delegate void PlayerDied();
  public static event PlayerDied OnPlayerDied;
Enter fullscreen mode Exit fullscreen mode

can be replaced with:

public class Player
{
  public static event Action OnPlayerDIed;

  // and if the function should have parameters, you write it this way
  public static event Action<string> OnPlayerDIed; //one parameter
  public static event Action<string, int> OnPlayerDIed; //two parameters
  // ie Action<T1,T2> in that order
Enter fullscreen mode Exit fullscreen mode

I hope this gives a practical perspective to these three concepts.
Have fun building cool stuff! cheers!!

Top comments (1)

Collapse
 
adabru profile image
Adam Brunnmeier

It is important to unsubscribe in either OnDisable() or OnDestroy to avoid unexpected behaviour.

Nice mention :).