Unity3D Tutorial: Events

An energetic hello to all new readers, and a gentle welcome back to old ones!
Today’s tutorial is about Events.

Remember that those tutorials expects you to have some kind of programming experience, and they reflect my experience and may not agree with everyone.

What is an Event?

According to Microsoft C# Programming Guide:

Events enable a class or object to notify other classes or objects when something of interest occurs. The class that sends (or raises) the event is called the publisher and the classes that receive (or handle) the event are called subscribers.

What is it good for?

Absolutely noth- I mean, Events are a nice way to notify all interested parties that something happened, without the caller having to known everyone.
For example, you might have an event that is called when your character takes damage, then Items, Buffs, Debuffs, Enemies, anything may subscribe to that event and the character that took the damage don’t need to hold a reference or known the type of every single subscriber.

Teach me how to do it!

For this tutorial, I made a simple example project where you can connect power stations to lamps, then when you activate a station all lamps will change to the station’s color:

I’ve also included a bonus example where you can get every single individual event return value, because under normal circumstances if you register an event with a return value, you are only able to get the last value returned by all the method calls.

In this case, all stations on the right will stop lighting up the lamps as soon as one lamp malfunctions, breaking the chain.

How to code it?

The following code will contain a lot of stuff that is only necessary for the visual of the example, and might confuse new programmers, please try to understand.

The example project works like this:

  • The player controls a character (WASD or Arrows).
  • If that character touches a station, that station will be selected.
  • The player can use the Space key to deselect a station.
  • If the player touches a lamp while a station is selected, that lamp will subscribe to the station’s event (I’ve limited it so a lamp can only subscribe once, but there is no limit programming-wise).
  • The player can press the mouse 1 button or left ctrl to use the selected station (The character don’t need to be near it).
  • A station will raise their event whenever used.
  • All stations on the left will light up all lamps connected to it.
  • Stations on the right will stop trying to light up lamps if one lamp returns false.

I will start with the Station code since it’s the one with less dependencies (delegates will be explained right after this code):

public class Station : MonoBehaviour {

    //A delegate is just a method signature, any method that returns a bool and takes an int is a FlipSwitch.
    public delegate bool FlipSwitch(int color);

    [Header("Sprites")]
    public Sprite[] sprites;

    public bool bStopOnFaultyLamp;

    private SpriteRenderer _spriteRenderer;

    event FlipSwitch myEvent;
    //This would work too for example.
    event System.Func<int, bool> myEvent2;

    SmartEvent<FlipSwitch> mySmartEvent = new SmartEvent<FlipSwitch>();

    void Awake() {
        _spriteRenderer = GetComponent<SpriteRenderer>();
    }

    public void ToggleSelection(bool bToggle) {
        transform.GetChild(0).gameObject.SetActive(bToggle);
    }

    public void Use() {
        if (!bStopOnFaultyLamp) {
            myEvent(GetNextColor());
        } else {
            bool bFaulty = false;
            foreach (FlipSwitch flip in mySmartEvent.Actions) {
                if (bFaulty) {
                    flip(0);
                } else if (!flip(GetNextColor())) {
                    bFaulty = true;
                }
            }
        }

        _spriteRenderer.sprite = sprites[GetNextColor()];
    }

    private int GetNextColor() {
        int i;

        for (i = 0; i < sprites.Length; i++) {
            if (_spriteRenderer.sprite == sprites[i]) {
                i++;
                if (i >= sprites.Length) {
                    i = 0;
                }
                break;
            }
        }

        return i;
    }

    public void Connect(FlipSwitch nMethod) {
        if (!bStopOnFaultyLamp) {
            myEvent += nMethod;
        } else {
            mySmartEvent += nMethod;
        }
    }
}

First important stuff is the FlipSwitch delegate. What are delegates you ask? A delegate is a type that defines a method signature, that means that any method that returns a bool and have an int as a parameter is a FlipSwitch.

More on Delegates

I’ve decided to elaborate a little more on that since it seems to be a difficult topic for some people.

Delegates are a way to reference to a type of method, for example when dealing with classes how would you create a reference to a class called Bread?
Bread loaf; right?

And what if you wanted to hold a reference to a method, let’s say a method that returns void and takes a bool and an int as a parameter, how would you do it?
It would be like this:
Action<bool, int> methodRef;

So methodRef can now hold a reference to any method that returns a void, and have a bool and int as parameters.

For example:

using System;
using System.Collections.Generic;

class Program {

    Action<bool, int> methodRef;
    
    static void Main() {
        methodRef = Print;
       
        //At this point, you can now use methodRef to call the Print method, for example
        methodRef(true, 22); //This will print "true : 22" to the console.
    }

    private void Print(bool b1, int i2) {
        Console.WriteLine(b1 + " : " + i2);
    }
}

Wait a second, you don’t use System.Action in the station code!

True, instead of System.Action<> I am using System.Func<> to hold a method reference, why is that?

Because while both Action and Func are used for method references, Action is a reference to a method that don’t return anything (void) and Func is a reference to a method that can actually return something (in our station code, it returns a bool).

And where does delegates enter in this?

Because maybe you think it’s ugly, or maybe you use it in a lot of other places and want to write less, or because you want to give a meaningful name to it so it’s easier to read, you can use a delegate for your method references!

delegate void MyMethodType(bool, int);
MyMethodType methodRef;

So now you don’t need to write Action<bool, int> every time you want to tell the code to expect a method that takes a bool and an int, you can just use the delegate you declared!

using System;
using System.Collections.Generic;

class Program {

    delegate void MyMethodType(bool, int);
    MyMethodType methodRef;

    static void Main() {
        methodRef = Print;

        //At this point, you can now use methodRef to call the Print method, for example
        methodRef(true, 22); //This will print "true : 22" to the console.
    }

    private void Print(bool b1, int i2) {
        Console.WriteLine(b1 + " : " + i2);
    }
}

Going back to the station code:

Next on that is “event FlipSwitch myEvent;” and “event System.Func<int, bool> myEvent2;”, those two are basically the same thing, a declaration of a field that is an event and requires a method that returns a bool and have an int as a parameter, I’ve just written it twice so you can see how declaring a delegate makes for a more pleasant to read code.

Now you may have noticed the SmartEvent thing, that is a custom class that I have included to allow us to deal with event returns on a per-event basis. I will expand on it later.

Use: A method called when the player uses the station, it will raise the event.

GetNextColor: Just a method to get what the next color for the station should be.

Connect: This is called for anything that wishes to register to our event.

By this point you might have noticed that an event is akin to a list of all methods registered to it. You use += to add a method to it and -= to remove a method, don’t worry as -= won’t throw an error if that method wasn’t subscribed to being with.
What happens if you use = you ask? The compiler will throw an error and won’t let you, ha!

Now to the Lamp code:

public class Lamp : MonoBehaviour {

    [Header("Sprites")]
    public Sprite[] sprites;

    private SpriteRenderer _spriteRenderer;

    private bool bConnected;

    private void Awake() {
        _spriteRenderer = GetComponent<SpriteRenderer>();
    }

    private bool TurnLight(int color) {
        bool result = Random.Range(0, 10) > 1;

        _spriteRenderer.sprite = sprites[color];

        return result;
    }

    public void ConnectToStation(Station station) {
        if (bConnected) {
            return;
        }
        station.Connect(TurnLight);
        bConnected = true;
    }
}

A very simple class, the only two important stuff here are:

TurnLight: This method changes this lamp color and returns a boolean, it will return false at a 10% chance.

ConnectToStation: This method calls the Connect method of the given station while giving the TurnLight method as a parameter, effectively subscribing to the station’s event.

Now for the biggest class, the PlayerController:

public class PlayerController : MonoBehaviour {

    public Sprite nothingSprite;
    public Sprite cableSprite;

    public float movementSpeed = 5f;

    private Rigidbody2D _rigidbody2D;
    private SpriteRenderer _spriteRenderer;

    public Station LinkedStation {
        get; private set;
    }

    private void Awake() {
        _rigidbody2D = GetComponent<Rigidbody2D>();
        _spriteRenderer = GetComponent<SpriteRenderer>();
    }

    // Update is called once per frame
    void Update() {
        float x, y;

        x = Input.GetAxis("Horizontal");
        y = Input.GetAxis("Vertical");

        _rigidbody2D.MovePosition(_rigidbody2D.position + (new Vector2(x, y) * Time.deltaTime * movementSpeed));

        if (Input.GetButtonDown("Fire1")) {
            UseStation();
        }

        if (Input.GetButtonDown("Jump")) {
            DropStation();
        }
    }

    private void OnTriggerEnter2D(Collider2D collision) {
        Station station = collision.gameObject.GetComponent<Station>();

        if (station != null) {
            if (LinkedStation != null) {
                LinkedStation.ToggleSelection(false);
            }

            LinkedStation = station;
            _spriteRenderer.sprite = cableSprite;

            LinkedStation.ToggleSelection(true);

            return;
        }

        Lamp lamp = collision.gameObject.GetComponent<Lamp>();

        if (lamp != null && LinkedStation != null) {
            lamp.ConnectToStation(LinkedStation);
        }
    }

    private void DropStation() {
        if (LinkedStation == null) {
            return;
        }

        LinkedStation.ToggleSelection(false);
        _spriteRenderer.sprite = nothingSprite;
        LinkedStation = null;
    }

    private void UseStation() {
        if (LinkedStation == null) {
            return;
        }

        LinkedStation.Use();
    }
}

Most stuff here is to control the character, so here we go:

OnTriggerEnter2D: If you have entered a station’s trigger area, it will select that station. If it was a lamp, it will subscribe that lamp to the current selected station’s event if it exists.

DropStation: Deselect the station.

UseStation: Uses the selected station.

And lastly but more importantly, the SmartEvent class!

public class SmartEvent<T> {

    private readonly LinkedList<T> _actions = new LinkedList<T>();

    public LinkedList<T> Actions { get { return _actions; } }

    public static SmartEvent<T> operator +(SmartEvent<T> smartEvent, T action) {
        smartEvent._actions.AddLast(action);
        return smartEvent;
    }

    public static SmartEvent<T> operator -(SmartEvent<T> smartEvent, T action) {
        smartEvent._actions.Remove(action);
        return smartEvent;
    }

}

This class basically fakes what an event does by just having a collection of methods registered to it, I’m only using it to be able to individually get all method’s return value and there are a huge number of different ways you could accomplish that.

If you are interested, this also shows you can override how operators work with your class, like the + and – operators.

What would I even use events for?

As a practical example, in my game TinyAttack there are a lot of different stuff that could mitigate the damage you take, like items and buffs, so when a player takes damage it will raise an event with the damage information and everything that subscribed to that event have the opportunity to change the damage it took. You could even for example have a debuff that increases the fire damage you take!

Full Project Download:

You can download the project I used to test here.

Thank you for reading my tutorial!

I hope this tutorial was of help to you, and if possible a donation of 1$ may be of great help on aiding the development of future tutorials:

 

https://www.patreon.com/TinyBirdGames

Thank you very much!

Liked it? Take a second to support TinyBird Games on Patreon!

Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.