In this post, we discuss delegates and how they can be used to make an event system. Delegates store methods and events used to broadcast listeners' messages when some event occurs.

Delegates

Delegates are the data type that is used to reference methods. When creating delegates, their parameters and return type should be defined. It can hold any method as long as the signatures match.

Similar to objects, delegates can be passed around and used from anywhere.

Creating Delegates

Let's declare a delegate that requires two floats and returns a void.

public delegate void Operators(float a, float b);
Declaring Delegate

Now, let's create methods with the same signature as the delegate.

// adds given numbers
void Adder(float num1, float num2) {
  Debug.Log($"result is {num1 + num2}");
}

//multiplies given numbers. Both methods have same signature but different functionality
void Multiplier(float num1, float num2) {
  Debug.Log($"result is {num1 * num2}");
}
Creating Methods

Instantiating Delegates

To use delegates and methods, we need to instantiate them first. This can be done in the same way as constructing an object. Instantiated delegates can be called in the same way as calling a method.

Operators operations = Adder; // instantiate delegate as Adder
operations(1, 2);

operations = Multiplier; // reference another method
operations(1, 2);

// ***** output *****
// result is 3
// result is 2
Instantiating and using delegates

Multicast Delegates

A delegate can also reference multiple methods. We can assign multiple delegates by using the += operator. The + and - operators can be used to add and remove methods from delegates. Those operators can also be applied between two delegates.

Adding Multicast

Let's add both Adder and Multiplier in the delegate operations .

Operators operations = Adder; // instantiate delegate as Adder
operations += Multiplier;
operations(1, 2); // this calls both methods at once

// ***** output *****
// result is 3
// result is 2
Assigning Multiple Delegates

C# Events

Events are used to broadcast messages to listeners when something happens. An event system consists of two entities: a listener and an emitter. There can be multiple listeners listening to one event invoked by an emitter.

Events can be useful for cases when we just want to know when something occurs without knowing other details. Listeners are also independent, so a listener should not care about what the other listeners do when an event is triggered.

C# events use delegates. They are used to define the type of event.

Adding Events

Let's create a Health class that will trigger an event OnDamageTaken when the player takes damage. This event will contain the amount of damage taken. When the player health reaches 0, an event will be triggered notifying the player's death. This class will also contain a TakeDamage method to do damage to the player.

public class Health {
  public delegate void DamageTaken(float damage);
  public event DamageTaken OnDamageTaken; // will be triggered on damage

  public delegate void PlayerDead(); // will be triggered when dead.
  public event PlayerDead OnDead;

  private float health = 100;

  // call this to do damage
  public void TakeDamage(float damage) {
    if (health <= 0) return;

    health -= 20;
    if (OnDamageTaken != null) // null check is required because if there is no subscribers it produces error when invoking
    {
      OnDamageTaken.Invoke(damage);
    }

    if (health <= 0) {
      if (OnDead != null) {
        OnDead.Invoke();
      }
    }
  }
}
Event Invoker

Adding Listeners

Let's create two classes that will listen to the events of the Health class.

// only want to know when player is dead
public class DeathUI {
  // subscribe to death event when object is created
  public DeathUI(Health health) {
    // subscribing OnDeath event
    health.OnDead += OnDeath;
  }

  private void OnDeath() {
    Debug.Log("You Died");
  }
}

// should know bot when player is dead and takes damage
public class DamageUI {
  // subscribe to dead and damage taken event when object is created
  public DamageUI(Health health) {
    // subscribe to both event
    health.OnDead += PlayerDied;
    health.OnDamageTaken += DamageTaken;
  }

  private void DamageTaken(float damage) {
    Debug.Log($"Player takes {damage} damage");
  }

  private void PlayerDied() {
    Debug.Log("Show dead effect");
  }
}
Event Listeners

Now to use them, create a HealthController class that constructs the Health and its listeners. It will also be responsible for damaging the player.

For demonstration, 20 damage will be done whenever the Spacebar key is pressed.

public class HealthController: MonoBehaviour {
  private Health health; // this class will invoke event when damage is taken and when dead
  private DamageUI damageUI; // listener of damage and dead event
  private DeathUI deadUI; // listener of dead event

  // create health and listeners at start
  private void Start() {
    health = new Health();
    damageUI = new DamageUI(health);
    deadUI = new DeathUI(health);
  }

  private void Update() {
    // damage player
    if (Input.GetKeyDown(KeyCode.Space)) {
      health.TakeDamage(20);
    }
  }

  // **** result after pressing space key 5 times ****
  // Player takes 20 damage
  // Player takes 20 damage
  // Player takes 20 damage
  // Player takes 20 damage
  // Player takes 20 damage
  // Show dead effect
  // You Died
}
Implementation of Events and Listeners

Notice here that the event invoker doesn't need to know what the listener does when the event is triggered. Also, the listener is unaware of other things except when the event is invoked. This causes separation of concern which makes the system more flexible and abstract.

Events Using Multicast Delegates

Multicast delegates can also be used as events. Making a delegate public allows other objects to add their methods to it. These added methods will get called whenever the delegate gets called. This is similar to using events.

Now, we can convert events of Health into delegates as,

public class Health {
  public delegate void DamageTaken(float damage);
  public DamageTaken OnDamageTaken; // just removing event will convert it to delegate

  public delegate void PlayerDead();
  public PlayerDead OnDead; // remove event

  ...
}
Health with Delegates

The difference between multicast delegates and event is that delegates can be called from other classes that have access to it, but an event can only be invoked from the object in which it exists. Therefore, using events prevents accidental invoking of events from other classes. It is recommended to use an event instead of delegates as events are safer.

Thanks for reading!