This will be a fairly lengthy explanation with a practical example. Hopefully this will clear up some confusion regarding Events and Interfaces.
I’m coming at this from the C++ side so I’ll mainly be talking from a Delegate background. But Events are pretty much a BP representation of Delegates anyway, so the logic should be the same.
C++ → BP
Signature (same for both. Used to enforce the “structure” of Delegates/Events, meaning they must have the same types/number of input parameters and return types)
Delegates → Event Dispatchers
Functions → Events (C++ binds normal functions to Delegates instead of having separate “Events”)
The third actor in your case would be a persistent Actor (think GameMode, PlayerController, etc.) that should never be destroyed, and holds all of the Event Dispatchers. Let’s make ours from an Actor BP and call it “EventManager”. For simplicity’s sake we’ll keep it at one manager, but you can have multiple managers for organization.
For the purposes of this explanation, TownGuard will be referencing the Chickens instead of the other way around. Here’s an example of how the references would look like without Events:
TownGuard has a Scare() function that goes through all the referenced Chickens and makes them Flee(), or move away from the current location of TownGuard.
Seems simple enough. You can have an Array of Chicken references and loop through to call Flee(). But what if there are 100 Chickens? What if there are 1,000 Chickens? Every time a Chicken is spawned, you’ll have to go to TownGuard and add a Chicken reference to the Array.
If a Chicken is destroyed you’ll have to go into the Array, find that specific Chicken, and remove it. Not doing so would either cause a crash when you call Flee() from a non-existent Chicken, or if you do a validity check before calling Flee(), you’ll still have a massively bloated Array of ghost Chickens. None of this is desirable.
Scalability is a huge issue too. If you constantly spawn and kill chickens then keeping track of all the references would be a nightmare. And if you add another 2 TownGuards, they would also have to have their own Arrays referencing the 1,000 Chickens.
What’s the correct way to implement this then? Use Events!
This is a good usage of hard references as EventManager will ALWAYS be loaded into memory, so it is both cheap and will never cause null reference crashes. If you delete a TownGuard or Chicken, you don’t have to remove any references since they reference EventManager and not the other way around.
So how do you get the Chickens to Flee()?
- On spawn, all TownGuards and Chickens create a reference to EventManager (easy since there’s only 1 persistent Actor to find and reference).
- All Chickens bind FleeEvent() to the FleeEventDispatcher on spawn.
- Scare() now calls FleeEventDispatcher, passing in the TownGuard’s location as a parameter.
- Calling FleeEventDispatcher will run ALL Events bound to it. Since every Chicken is coded to automatically bind FleeEvent() on spawn, you don’t have to iterate through any Arrays.
Think of an EventDispatcher as a remote control for “wireless” function/Event calls.
In the original example where you hard reference each Chicken, you create one-way “wires” (references) from the TownGuard to each Chicken. If you want all the Chickens to Flee(), Scare() will have to go to each “wire” and send a signal to each Chicken to run Flee(). If there isn’t a Chicken at the end of the “wire”, then the “wire” explodes and your game crashes. So every time a Chicken is removed from the game, you need to manually clean up the “wire”.
With EventDispatchers, you instead have one-way “wires” from TownGuards and Chickens to the EventDispatcher. When Scare() is called, it sends a signal through the “wire” to EventManager, but importantly FleeEventDispatcher sends a “WIRELESS” signal out to into the world telling all Events bound to it to run.
It doesn’t care if there’s a Chicken to pick it up or not. All Chickens that have FleeEvent() bound and do pick up the signal will all run FleeEvent() at the same time. Since EventManager doesn’t have any one-way wires to Chickens, even if all the Chickens are dead when the “wireless” signal is sent, then the signal simply won’t get picked up by anything and your game doesn’t crash.
For safety you should add logic to remove FleeEvent() bindings to FleeEventDispatcher when a Chicken is destroyed (through OnDestroy node). Even though sending a signal to a non-existent Chicken won’t crash the game, loose bindings will still bloat your FleeEventDispatcher until garbage collection cleans it up, and may possibly cause weird runtime issues.
To sum it up, the benefits of Events and Event Dispatchers are as follows:
-
You don’t have to manually manage a truck load of hard references. Managing references between 10 TownGuards and 1,000 Chickens would be a full-time job. With an Event Dispatcher, every TownGuard and Chicken can create a reference to EventManager on spawn and not have to worry about it.
-
Destroying a TownGuard or Chicken will automatically clean up the reference to EventManager, since the reference originates from them TO the EventManager. EventManager is a blind manager that never has to see anything that references it. TownGuards and Chickens never have to directly see each other either, only the almighty EventManager. This is called “decoupling” logic between classes, meaning you can have one class affect another without them even knowing about each other.
-
Calling EventDispatchers sends “wireless” signals that won’t crash your game if not picked up. To be more technical, they create [weak pointers] to the Objects that bind their Events to it, so if the Objects no longer exists the Dispatcher just shrugs and continues about its business.
-
Events are built for scalability. 1 Chicken? 1,000 Chickens? 1,000,000 Chickens? Just call FleeEventDispatcher once. Adding more TownGuards? Have them also reference the EventManager and have their Scare() call the FleeEventDispatcher, passing in their location. Or maybe you can add a completely different Wolf Actor that can also cause Chickens to flee by calling FleeEventDispatcher. Want the Chickens to affect the TownGuards too? Create another EventDispatcher in EventManager and reverse the logic flow.
Imagine if you had 1,000 TownGuards and 1,000 Chickens. That would be 1,000,000 hard references you have to manage. Gross. Using Events you’ll have 2,000 hard references, all of them to EventManager, meaning they’ll get cleaned up automatically when the originating Actor is destroyed.
The big mistake a lot of beginners make when using Events is that they create the Event and Event Dispatcher in the same Actor. Instead of having one FleeEventDispatcher in an EventManager, they create separate FleeEventDispatchers in every Chicken that their FleeEvent() binds to. This means the TownGuard needs to hard reference each Chicken to even get to their FleeEventDispatcher, which will send a “wireless” signal to THE SAME OWNING ACTOR (the Chicken) to call FleeEvent() (???).
This is functionally useless. If you have already have a hard reference to a Chicken, just call Flee() directly.
Note that Events are VERY different from Interfaces. Interfaces are used for different classes to implement the same abstract function, which may or may not be overridden per class.
Interfaces are for one-to-one functionality, where you’re not sure what the latter “one” is. As in, if a player character press E on an interactable Actor, the Actor will run its implementation of an Interface function (let’s call it RecieveInteraction()). A door’s RecieveInteraction() would cause it to open, a light switch’s RecieveInteraction() would cause it to turn on/off, etc.
The player says, “Hey, run RecieveInteraction(),” without caring about what it does specifically, and the target Actor will run its own version of RecieveInteraction().
Events are mainly for managing logic between one-to-many, many-to-one, and many-to-many Actors. They’re also used to bind logic to triggers and inputs, such as creating Events to bind to OnClick.
Events and Interfaces do different things. They CAN be used together and should NEVER be used to replace each other.