What UnityEvent
s 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).