Capturing and Forwarding Unity Events
As mentioned in last week’s article on the “pure code” approach to Unity code design, capturing events can be problematic. I gave an example of how this could be overcome, but didn’t flesh it out to cover the sixty events that a MonoBehaviour
can receive. Today’s article includes the source code for a class that does just that. It should prove useful to anyone interested in exploring “pure code” design.
Let’s first recap the problem being solved today. Unity’s way of telling your code that an event has happened is by calling a function on the MonoBehaviour
attached to the applicable GameObject
. This function must have a particular name and not be private. For example, Start
is called on the first frame your GameObject
is active in the scene:
using UnityEngine; public class StartTest : MonoBehaviour { void Start() { Debug.Log("started!"); } }
You don’t need to explicitly register to receive the event and there’s no way to unregister for it. Simply by having that function you will receive the event by having that function called until either your MonoBehaviour
or the GameObject
it’s attached to is destroyed.
In the “pure code” approach to Unity app design, you only have one MonoBehaviour
and it contains and calls all your app code. You still create more GameObject
instances, but don’t attach app logic to them via more MonoBehaviour
instances.
So how do you get events for the game objects your app creates? If you don’t have any MonoBehaviour
on them, you don’t get the events. The trick is to employ the “event forwarding” system mentioned briefly in the previous article.
This system involves the creation of a MonoBehaviour
that doesn’t do any app logic. Instead, it’s sole purpose is to receive events from Unity and dispatch them to anyone interested via a standard C# event
. Here’s a very simple version to illustrate:
using UnityEngine; public class EventForwarder : MonoBehaviour { // Type of function that handles Start events public delegate void EventHandler(); // Event used to forward the Start event public event EventHandler StartEvent = () => {}; // Receiver for the Start event // Unity calls this public void Start() { // Forward the Start event by dispatching the // associated C# event StartEvent(); } }
The app code residing on and called by the main MonoBehaviour
uses this EventForwarder
class like so:
using UnityEngine; public class TestScript : MonoBehaviour { private EventForwarder forwarder; void Start() { // Create a GameObject var otherGameObject = new GameObject("created at runtime"); // Add an EventForwarder to it so we can receive events forwarder = otherGameObject.AddComponent<EventForwarder>(); // Listen for it to start forwarder.StartEvent += HandleStartEvent; } private void HandleStartEvent() { // Stop listening for it to start forwarder.StartEvent -= HandleStartEvent; // Do something in response to the event Debug.Log("Detected other game object's Start()"); } }
There are some advantages to this system. First, it makes the “pure code” approach much more useful as it allows you to receive events from Unity. Second, it allows you to explicitly register and unregister for events. This means you can listen to them only when you want to rather than always listening to them. Third, the C# event
system often feels more natural to those unfamiliar with the Unity events system. Fourth, it helps reduce the number of bugs that crop up when you accidentally name a function incorrectly and then never receive any events.
With that in mind, the following is a complete version of the EventForwarder
class above. It features all 60 events available to a MonoBehaviour
as of Unity 4.6.1.
using UnityEngine; /** * Utility component to add to game objects whose events you want forwarded from Unity's message * system to standard C# events. Handles all events as of Unity 4.6.1. * @author Jackson Dunstan - http://jacksondunstan.com/articles/2922 */ public class EventForwarder : MonoBehaviour { public delegate void EventHandler0(); public delegate void EventHandler1<TParam>(TParam param); public delegate void EventHandler2<TParam1,TParam2>(TParam1 param1, TParam2 param2); public event EventHandler0 AwakeEvent = () => {}; public event EventHandler0 FixedUpdateEvent = () => {}; public event EventHandler0 LateUpdateEvent = () => {}; public event EventHandler1<int> OnAnimatorIKEvent = layerIndex => {}; public event EventHandler0 OnAnimatorMoveEvent = () => {}; public event EventHandler1<bool> OnApplicationFocusEvent = focusStatus => {}; public event EventHandler1<bool> OnApplicationPauseEvent = pauseStatus => {}; public event EventHandler0 OnApplicationQuitEvent = () => {}; public event EventHandler2<float[],int> OnAudioFilterReadEvent = (data,channels) => {}; public event EventHandler0 OnBecameInvisibleEvent = () => {}; public event EventHandler0 OnBecameVisibleEvent = () => {}; public event EventHandler1<Collision> OnCollisionEnterEvent = collision => {}; public event EventHandler1<Collision2D> OnCollisionEnter2DEvent = collision => {}; public event EventHandler1<Collision> OnCollisionExitEvent = collision => {}; public event EventHandler1<Collision2D> OnCollisionExit2DEvent = collision => {}; public event EventHandler1<Collision> OnCollisionStayEvent = collision => {}; public event EventHandler1<Collision2D> OnCollisionStay2DEvent = collision => {}; public event EventHandler0 OnConnectedToServerEvent = () => {}; public event EventHandler1<ControllerColliderHit> OnControllerColliderHitEvent = hit => {}; public event EventHandler0 OnDestroyEvent = () => {}; public event EventHandler0 OnDisableEvent = () => {}; public event EventHandler1<NetworkDisconnection> OnDisconnectedFromServerEvent = info => {}; public event EventHandler0 OnDrawGizmosEvent = () => {}; public event EventHandler0 OnDrawGizmosSelectedEvent = () => {}; public event EventHandler0 OnEnableEvent = () => {}; public event EventHandler1<NetworkConnectionError> OnFailedToConnectEvent = error => {}; public event EventHandler1<NetworkConnectionError> OnFailedToConnectToMasterServerEvent = error => {}; public event EventHandler0 OnGUIEvent = () => {}; public event EventHandler1<float> OnJointBreakEvent = breakForce => {}; public event EventHandler1<int> OnLevelWasLoadedEvent = level => {}; public event EventHandler1<MasterServerEvent> OnMasterServerEventEvent = msEvent => {}; public event EventHandler0 OnMouseDownEvent = () => {}; public event EventHandler0 OnMouseDragEvent = () => {}; public event EventHandler0 OnMouseEnterEvent = () => {}; public event EventHandler0 OnMouseExitEvent = () => {}; public event EventHandler0 OnMouseOverEvent = () => {}; public event EventHandler0 OnMouseUpEvent = () => {}; public event EventHandler0 OnMouseUpAsButtonEvent = () => {}; public event EventHandler1<NetworkMessageInfo> OnNetworkInstantiateEvent = info => {}; public event EventHandler1<GameObject> OnParticleCollisionEvent = other => {}; public event EventHandler1<NetworkPlayer> OnPlayerConnectedEvent = player => {}; public event EventHandler1<NetworkPlayer> OnPlayerDisconnectedEvent = player => {}; public event EventHandler0 OnPostRenderEvent = () => {}; public event EventHandler0 OnPreCullEvent = () => {}; public event EventHandler0 OnPreRenderEvent = () => {}; public event EventHandler2<RenderTexture,RenderTexture> OnRenderImageEvent = (src,dest) => {}; public event EventHandler0 OnRenderObjectEvent = () => {}; public event EventHandler2<BitStream,NetworkMessageInfo> OnSerializeNetworkViewEvent = (stream,info) => {}; public event EventHandler0 OnServerInitializedEvent = () => {}; public event EventHandler1<Collider> OnTriggerEnterEvent = other => {}; public event EventHandler1<Collider2D> OnTriggerEnter2DEvent = other => {}; public event EventHandler1<Collider> OnTriggerExitEvent = other => {}; public event EventHandler1<Collider2D> OnTriggerExit2DEvent = other => {}; public event EventHandler1<Collider> OnTriggerStayEvent = other => {}; public event EventHandler1<Collider2D> OnTriggerStay2DEvent = other => {}; public event EventHandler0 OnValidateEvent = () => {}; public event EventHandler0 OnWillRenderObjectEvent = () => {}; public event EventHandler0 ResetEvent = () => {}; public event EventHandler0 StartEvent = () => {}; public event EventHandler0 UpdateEvent = () => {}; public void Awake() { AwakeEvent(); } public void FixedUpdate() { FixedUpdateEvent(); } public void LateUpdate() { LateUpdateEvent(); } public void OnAnimatorIK(int layerIndex) { OnAnimatorIKEvent(layerIndex); } public void OnAnimatorMove() { OnAnimatorMoveEvent(); } public void OnApplicationFocus(bool focusStatus) { OnApplicationFocusEvent(focusStatus); } public void OnApplicationPause(bool pauseStatus) { OnApplicationPauseEvent(pauseStatus); } public void OnApplicationQuit() { OnApplicationQuitEvent(); } public void OnAudioFilterRead(float[] data, int channels) { OnAudioFilterReadEvent(data, channels); } public void OnBecameInvisible() { OnBecameInvisibleEvent(); } public void OnBecameVisible() { OnBecameVisibleEvent(); } public void OnCollisionEnter(Collision collision) { OnCollisionEnterEvent(collision); } public void OnCollisionEnter2D(Collision2D collision) { OnCollisionEnter2DEvent(collision); } public void OnCollisionExit(Collision collision) { OnCollisionExitEvent(collision); } public void OnCollisionExit2D(Collision2D collision) { OnCollisionExit2DEvent(collision); } public void OnCollisionStay(Collision collision) { OnCollisionStayEvent(collision); } public void OnCollisionStay2D(Collision2D collision) { OnCollisionStay2DEvent(collision); } public void OnConnectedToServer() { OnConnectedToServerEvent(); } public void OnControllerColliderHit(ControllerColliderHit hit) { OnControllerColliderHitEvent(hit); } public void OnDestroy() { OnDestroyEvent(); } public void OnDisable() { OnDisableEvent(); } public void OnDisconnectedFromServer(NetworkDisconnection info) { OnDisconnectedFromServerEvent(info); } public void OnDrawGizmos() { OnDrawGizmosEvent(); } public void OnDrawGizmosSelected() { OnDrawGizmosSelectedEvent(); } public void OnEnable() { OnEnableEvent(); } public void OnFailedToConnect(NetworkConnectionError error) { OnFailedToConnectEvent(error); } public void OnFailedToConnectToMasterServer(NetworkConnectionError error) { OnFailedToConnectToMasterServerEvent(error); } public void OnGUI() { OnGUIEvent(); } public void OnJointBreak(float breakForce) { OnJointBreakEvent(breakForce); } public void OnLevelWasLoaded(int level) { OnLevelWasLoadedEvent(level); } public void OnMasterServerEvent(MasterServerEvent msEvent) { OnMasterServerEventEvent(msEvent); } public void OnMouseDown() { OnMouseDownEvent(); } public void OnMouseDrag() { OnMouseDragEvent(); } public void OnMouseEnter() { OnMouseEnterEvent(); } public void OnMouseExit() { OnMouseExitEvent(); } public void OnMouseOver() { OnMouseOverEvent(); } public void OnMouseUp() { OnMouseUpEvent(); } public void OnMouseUpAsButton() { OnMouseUpAsButtonEvent(); } public void OnNetworkInstantiate(NetworkMessageInfo info) { OnNetworkInstantiateEvent(info); } public void OnParticleCollision(GameObject other) { OnParticleCollisionEvent(other); } public void OnPlayerConnected(NetworkPlayer player) { OnPlayerConnectedEvent(player); } public void OnPlayerDisconnected(NetworkPlayer player) { OnPlayerDisconnectedEvent(player); } public void OnPostRender() { OnPostRenderEvent(); } public void OnPreCull() { OnPreCullEvent(); } public void OnPreRender() { OnPreRenderEvent(); } public void OnRenderImage(RenderTexture src, RenderTexture dest) { OnRenderImageEvent(src, dest); } public void OnRenderObject() { OnRenderObjectEvent(); } public void OnSerializeNetworkView(BitStream stream, NetworkMessageInfo info) { OnSerializeNetworkViewEvent(stream, info); } public void OnServerInitialized() { OnServerInitializedEvent(); } public void OnTriggerEnter(Collider other) { OnTriggerEnterEvent(other); } public void OnTriggerEnter2D(Collider2D other) { OnTriggerEnter2DEvent(other); } public void OnTriggerExit(Collider other) { OnTriggerExitEvent(other); } public void OnTriggerExit2D(Collider2D other) { OnTriggerExit2DEvent(other); } public void OnTriggerStay(Collider other) { OnTriggerStayEvent(other); } public void OnTriggerStay2D(Collider2D other) { OnTriggerStay2DEvent(other); } public void OnValidate() { OnValidateEvent(); } public void OnWillRenderObject() { OnWillRenderObjectEvent(); } public void Reset() { ResetEvent(); } public void Start() { StartEvent(); } public void Update() { UpdateEvent(); } }
I hope you find this class useful. If you have anything to add to it or find any issues with it, let me know in the comments.
#1 by henke37 on January 12th, 2015 ·
Next time, does unity have a preprocessor, and if so, can it be used to DRY up this code?
#2 by jackson on January 12th, 2015 ·
C# does, but it can’t be used like the C preprocessor to implement code-generating macros. I’d sure be interested in a way to DRY up the class though, so let me know if you have any ideas on how to do so.
#3 by taraa on March 11th, 2015 ·
Sorry for English, translated using google translator.
Could you show a more examples on the use of class EventForwarder, that I understand its effectiveness, but it Apply to the fullest extent I do not know how.
Hello from Ukraine :)
#4 by jackson on March 11th, 2015 ·
How you use
EventForwarder
really depends on the app logic you’re trying to create. Say the player has thrown a ball and is trying to hit a target. You’d be interested in when that ball collides with the target. You could useEventForwarder
to be notified when the collision occurs:Note: this is untested code so it may have errors
Hope that helps illustrate how to use
EventForwarder
with a slightly more realistic example than in the article.Oh, and hello from California! :)
#5 by taraa on March 12th, 2015 ·
I understand that all other components simply can not follow EventForwarder MonoBehaviour and you can add features that require corresponding event.
We must protest, thank you.
#6 by Ole S. on July 4th, 2017 ·
For Unity 5 you need to implement interfaces to get Mouse Events:
public class EventForwarder : MonoBehaviour, IPointerClickHandler, IPointerDownHandler, IPointerUpHandler, IPointerEnterHandler ,IPointerExitHandler
Took us a while to figure out.
#7 by Franz on November 20th, 2017 ·
Good to know, ty
#8 by Franz on November 20th, 2017 ·
This is great! Thank you!
#9 by Franz on November 27th, 2017 ·
Hello,
I can’t figure out some things. For example, in a 2D game: I have a Player with its Collider2D and I have a Gem with its Collider2D that is a Trigger. The behaviour I want is that when the Player collides with the gem, the latter’s position change. I wrote the function in my MainScript and set up the event listener. Other stuff I do in this event works great (like playing a sound), but how to access the gem object in order to change its transform.position?
#10 by jackson on November 27th, 2017 ·
Hi Franz,
You previously added the
EventForwarder
to a game object, either withAddComponent
or via a scene or prefab. In any case, you should have anEventForwarder
field of the script you putGemsEventForwarder_OnTriggerEnter2DEvent
in. SinceEventForwarder
is aMonoBehaviour
, you can use it to get at the game object it’s attached to. So the last line of your example function would look like this:#11 by Franz on November 27th, 2017 ·
Hi Jackson,
thanks for the reply. Of course, I did. I have:
Then I initialized the array and add the forwarders in a function executed in the Awake of the MainScript:
I have tried your suggestion and it works perfectly! I’m a bit overwhelmed by new things and lost the focus! Thank you!
Just for completeness: I then retrieve the right index from the array and use it to access the right event forwarder way to the gem object.
#12 by jackson on November 27th, 2017 ·
Glad to hear you’ve got it working. :)
#13 by Franz on December 7th, 2017 ·
Hello there,
I have another question on the same topic: in the case explained above, I can get the index of the gem because it just can be the third last spawned gem from the array. But what about if I have many objects spawned randomly in the scene and can’t know the exact index?
I have an array of EventForwarder and an array of objects they are attached to. How can I get the correct event forwarder (in order to access the correct object) in an event method (e.g. the private void GemsEventForwarder_OnTriggerEnter2DEvent(Collider2D collider) of the comment above)?
#14 by jackson on December 7th, 2017 ·
The core issue here is how the event handler function (
GemsEventForwarder_OnTriggerEnter2DEvent
) knows which gem the event is for so it can do something related to that particular gem. There are several ways to handle this, so I’ll list a few. One is to use an instance (i.e. non-static) method as your event handler:Because instance/non-static methods automatically have a reference to
this
, you can add fields to the class and use them from the event handler.Another approach is to use a lambda:
Finally, you could modify
EventHandler
so it always passesthis
as the first parameter:There are many more ways you could use, but those are a few strategies that might work out for you.
#15 by Franz on December 8th, 2017 ·
I have the eyes of a child in a candy shop in this moment!!! I have immediately implemented the third option you gave me. I think it suits perfectly! (I still can’t try if it works because I need to finish other parts before the project could be runnable, but I’m quite sure it will);
Thank you very much, you are so kind!
#16 by Franz on December 7th, 2017 ·
Hello there,
is it possible to have a specific method that registers an event forwarder for an event? I mean, something like this:
It would be very useful, but I can’t figure how to set up that Register method. I imagine that I need to overload the method in order to have a specific one for every type of param of the EventHandler1 and EventHandler2 can handle, but it won’t be so much work since I just need to handle a bunch of events.
#17 by jackson on December 7th, 2017 ·
Hi Franz,
You could have an
EventForwarderManager
static class that keepsEventForwarder
references. I’m not entirely sure why you’d do that though. Could you explain what the purpose of such a class and itsAddEventForwarder
andRegister
methods would be?#18 by Franz on December 9th, 2017 ·
Hi Jackson,
I would add the EventForwarderManager static class (with its static methods AddEventForwarder and Register) to the library that I am building for my code generator. It would allow a more clear main script and reuse of code. It would be easier to create a template for the generator too. BUT I read that the Register function as I imagined is not possible at all. So, at the moment I just wrote the Add method and I am managing the event registration / un-registration in the main script.
#19 by Franz on December 11th, 2017 ·
Hello, have you ever experienced this pattern with a Camera object?
I have just tried that and it does a strange thing: the game view goes black (UI still rendered but its elements overlap on themselves, it seems there is no clear screen between frames) and I got a warning saying: OnRenderImage() possibly didn’t write anything to the destination texture!
I have just registered the FixedUpdate event, in order to let the camera follow the player in the scene (the code is ok, I can see in the scene view that it works). I checked the settings of the Camera in the editor and it’s everything fine. If I disable the EventForwarder component from the editor, the Game View render a shot of what the camera “sees” but it’s not really rendering, it just shows a shot of that moment.
The problem persists even if I don’t register any event, but just attach an EventForwarder to the Camera object.
Did anyone had this problem? How can be fixed according to you?
#20 by jackson on December 11th, 2017 ·
Sometimes you don’t want to define message handles, such as with
OnRenderImage
where doing nothing is actually harmful. Think ofEventForwarder
more like a template that you can customize by deleting messages from it. until you’re left with just the ones you really want to handle. That’ll also improve performance as Unity won’t bother calling message handlers that you don’t define.#21 by Franz on December 12th, 2017 ·
You’re always right! I tried to disable just OnRenderImage() and it didn’t work, but with your suggestions, I dug more. If anyone will face this issue, just disable (or customize) OnPostRender, OnPreCull, OnPreRender, OnRenderImage, OnRenderObject and OnWillRenderObject.
#22 by Laca on November 8th, 2020 ·
Cannot assign to ‘OnCollisionEnter’ because it is a ‘method group’
What is the probleme here?:)
#23 by jackson on November 8th, 2020 ·
OnCollisionEnter
is the method that receives the message from Unity.OnCollisionEnterEvent
is the event it dispatches when it receives that message. So to subscribe to the event, useOnCollisionEnterEvent
instead ofOnCollisionEnter
: