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