Skip to main content
added 548 characters in body
Source Link
DMGregory
  • 136.3k
  • 22
  • 247
  • 373

This is a particularly big win when you're making UI widgets. Take UnityEngine.UI.Button - you'll often use this dozens if not hundreds of times when making the interface for a game. But because the developers set it up to work with a UnityEvent, you won't need to edit its code or subclass it every time you want it to do something different (like enter play mode, switch to the next tab, close a dialog, purchase an item). You can just tell each instance what you want it to do as Inspector data, while keeping the code simple and shared.

This is an example of something called The Observer Pattern, in case you'd like to read up more about how this is used (or how it works under the hood).

This is an example of something called The Observer Pattern, in case you'd like to read up more about how this is used (or how it works under the hood).

This is a particularly big win when you're making UI widgets. Take UnityEngine.UI.Button - you'll often use this dozens if not hundreds of times when making the interface for a game. But because the developers set it up to work with a UnityEvent, you won't need to edit its code or subclass it every time you want it to do something different (like enter play mode, switch to the next tab, close a dialog, purchase an item). You can just tell each instance what you want it to do as Inspector data, while keeping the code simple and shared.

This is an example of something called The Observer Pattern, in case you'd like to read up more about how this is used (or how it works under the hood).

Source Link
DMGregory
  • 136.3k
  • 22
  • 247
  • 373

What UnityEvents give us is decoupling. I'll demonstrate that with an example...

Let's say we have a pressure switch in our game that's supposed to open a door. The pressure switch has a trigger collider and a script maybe a bit like this:

public class PressureSwitch : MonoBehaviour {

    public Door doorToOpen;

    int _objectsInContact;


    void OnTriggerEnter(Collider other) {
        _objectsInContact++;
        if (_objectsInContact == 1) Activate();
    }

    void OnTriggerExit(Collider other) {
        _objectsInContact--;
        if (_objectsInContact  == 0) Deactivate();
    }

    void Activate() {
        doorToOpen.Open();
    }

    void Deactivate() {
        doorToOpen.Close();
    }
}

So far so good. But then in playtests, sometimes players don't notice the door opening, so we want to add a camera animation to show them. So we modify the Activate method:

void Activate() {
    cameraAnimation.Play();
    doorToOpen.Open();
}

Then in level 2, we have another puzzle, but instead of opening a door, we want to extend a bridge. We don't want to make a new class that just copies all the trigger code, so we just add a bridge variable, and then check which of those variables has been set so we can do the right thing for level 1 or level 2:

void Activate() {
    if (bridge != null) bridge.Extend();
    if (cameraAnimation != null) cameraAnimation.Play();
    if (doorToOpen != null) doorToOpen.Open();
}

(Plus corresponding changes for the Deactivate method)

...okay, this is starting to get cumbersome. You can imagine if we keep scaling this to more levels where pressure switches turn on lights or drain water or turn off force fields or activate conveyor belts... this approach just doesn't scale.

Somehow we want to separate the logic of what activates the switch from what happens as a result. The switch should be responsible for just its own activation state, and shouldn't need to know anything about what happens downstream.

Enter the UnityEvent:

public class PressureSwitch : MonoBehaviour {

    public UnityEvent OnActivate;
    public UnityEvent OnDeactivate;

    int _objectsInContact;

    void OnTriggerEnter(Collider other) {
        _objectsInContact++;
        if (_objectsInContact == 1 && OnActivate != null) OnActivate.Invoke();
    }

    void OnTriggerExit(Collider other) {
        _objectsInContact--;
        if (_objectsInContact  == 0 && OnDeactivate != null) OnDeactivate.Invoke();
    }
}

Now we'll have two fields in the Unity Inspector where we can wire up any number of functions on any number of different objects to respond to this switch getting activated or deactivated.

We can use this to trigger sounds, animations, toggle gameobject active states, pretty much anything you can do with a public function (that takes no arguments or a single argument you can set in the inspector). We can even attach and detach new functions to react to this switch at runtime, depending on the player's actions. And our PressureSwitch class doesn't need detailed knowledge about any of that to do its work.

We've taken the logic of figuring out whether the switch should turn on or off, and "decoupled it" from the logic of what should happen next.

In the process, we've made this class orders of magnitude more powerful, while also making the code shorter and simpler! We don't get that pairing very often, so savour it when it happens. 😉

This is also a big win for level designers on your team, who might not be programmers. Now they can use this PressureSwitch in hundreds of different configurations just by setting it up in the Inspector. They don't need to come ask you or another programmer on your team to make a code change every time they need a switch to interact with a new type of object in a puzzle. Or even if you're working solo, now you don't need to recompile your code to make this kind of change.

This is an example of something called The Observer Pattern, in case you'd like to read up more about how this is used (or how it works under the hood).