Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Save more on your purchases! discount-offer-chevron-icon
Savings automatically calculated. No voucher code required.
Arrow left icon
All Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Newsletter Hub
Free Learning
Arrow right icon
timer SALE ENDS IN
0 Days
:
00 Hours
:
00 Minutes
:
00 Seconds

Event-driven Programming

Save for later
  • 22 min read
  • 06 Feb 2015

article-image

In this article by Alan Thorn author of the book Mastering Unity Scripting will cover the following topics:

  • Events
  • Event management

(For more resources related to this topic, see here.)


The Update events for MonoBehaviour objects seem to offer a convenient place for executing code that should perform regularly over time, spanning multiple frames, and possibly multiple scenes. When creating sustained behaviors over time, such as artificial intelligence for enemies or continuous motion, it may seem that there are almost no alternatives to filling an Update function with many if and switch statements, branching your code in different directions depending on what your objects need to do at the current time. But, when the Update events are seen this way, as a default place to implement prolonged behaviors, it can lead to severe performance problems for larger and more complex games. On deeper analysis, it's not difficult to see why this would be the case. Typically, games are full of so many behaviors, and there are so many things happening at once in any one scene that implementing them all through the Update functions is simply unfeasible. Consider the enemy characters alone, they need to know when the player enters and leaves their line of sight, when their health is low, when their ammo has expired, when they're standing on harmful terrain, when they're taking damage, when they're moving or not, and lots more. On thinking initially about this range of behaviors, it seems that all of them require constant and continuous attention because enemies should always know, instantly, when changes in these properties occur as a result of the player input. That is, perhaps, the main reason why the Update function seems to be the most suitable place in these situations but there are better alternatives, namely, event-driven programming. By seeing your game and your application in terms of events, you can make considerable savings in performance. This article then considers the issue of events and how to manage them game wide.

Events


Game worlds are fully deterministic systems; in Unity, the scene represents a shared 3D Cartesian space and timeline inside which finite GameObjects exist. Things only happen within this space when the game logic and code permits them to. For example, objects can only move when there is code somewhere that tells them to do so, and under specific conditions, such as when the player presses specific buttons on the keyboard. Notice from the example that behaviors are not simply random but are interconnected; objects move only when keyboard events occur. There is an important connection established between the actions, where one action entails another. These connections or linkages are referred to as events; each unique connection being a single event. Events are not active but passive; they represent moments of opportunity but not action in themselves, such as a key press, a mouse click, an object entering a collider volume, the player being attacked, and so on. These are examples of events and none of them say what the program should actually do, but only the kind of scenario that just happened. Event-driven programming starts with the recognition of events as a general concept and comes to see almost every circumstance in a game as an instantiation of an event; that is, as an event situated in time, not just an event concept but as a specific event that happens at a specific time. Understanding game events like these is helpful because all actions in a game can then be seen as direct responses to events as and when they happen. Specifically, events are connected to responses; an event happens and triggers a response. Further, the response can go on to become an event that triggers further responses and so on. In other words, the game world is a complete, integrated system of events and responses. Once the world is seen this way, the question then arises as to how it can help us improve performance over simply relying on the Update functions to move behaviors forward on every frame. And the method is simply by finding ways to reduce the frequency of events. Now, stated in this way, it may sound a crude strategy, but it's important. To illustrate, let's consider the example of an enemy character firing a weapon at the player during combat.

Throughout the gameplay, the enemy will need to keep track of many properties. Firstly, their health, because when it runs low the enemy should seek out medical kits and aids to restore their health again. Secondly, their ammo, because when it runs low the enemy should seek to collect more and also the enemy will need to make reasoned judgments about when to fire at the player, such as only when they have a clear line of sight. Now, by simply thinking about this scenario, we've already identified some connections between actions that might be identified as events. But before taking this consideration further, let's see how we might implement this behavior using an Update function, as shown in the following code sample 4-1. Then, we'll look at how events can help us improve on that implementation:

// Update is called once per frame
void Update ()
{
   //Check enemy health
   //Are we dead?
   if(Health <= 0)
   {
         //Then perform die behaviour
         Die();
         return;
   }
   //Check for health low
   if(health <= 20)
   {
       //Health is low, so find first-aid
         RunAndFindHealthRestore();
         return;
   }
   //Check ammo
   //Have we run out of ammo?
   if(Ammo <= 0)
   {
         //Then find more
         SearchMore();
         return;
   }
   //Health and ammo are fine. Can we see player? If so, shoot
   if(HaveLineOfSight)
   {
           FireAtPlayer();
   }
}


The preceding code sample 4-1 shows a heavy Update function filled with lots of condition checking and responses. In essence, the Update function attempts to merge event handling and response behaviors into one and the results in an unnecessarily expensive process. If we think about the event connections between these different processes (the health and ammo check), we see how the code could be refactored more neatly. For example, ammo only changes on two occasions: when a weapon is fired or when new ammo is collected. Similarly, health only changes on two occasions: when an enemy is successfully attacked by the player or when an enemy collects a first-aid kit. In the first case, there is a reduction, and in the latter case, an increase.

Since these are the only times when the properties change (the events), these are the only points where their values need to be validated. See the following code sample 4-2 for a refactored enemy, which includes C# properties and a much reduced Update function:

using UnityEngine;
using System.Collections;
public class EnemyObject : MonoBehaviour
{
   //-------------------------------------------------------
   //C# accessors for private variables
   public int Health
   {
         get{return _health;}
         set
         {
               //Clamp health between 0-100
               _health = Mathf.Clamp(value, 0, 100);
              //Check if dead
               if(_health <= 0)
               {
                     OnDead();
                     return;
               }
               //Check health and raise event if required
               if(_health <= 20)
              {
                     OnHealthLow();
                     return;
               }
         }
   }
   //-------------------------------------------------------
   public int Ammo
   {
         get{return _ammo;}
         set
         {
             //Clamp ammo between 0-50
             _ammo = Mathf.Clamp(value,0,50);
               //Check if ammo empty
               if(_ammo <= 0)
               {
                     //Call expired event
                     OnAmmoExpired();
                     return;
               }
         }
   }
   //-------------------------------------------------------
   //Internal variables for health and ammo
   private int _health = 100;
   private int _ammo = 50;
   //-------------------------------------------------------
   // Update is called once per frame
   void Update ()
   {
   }
   //-------------------------------------------------------
   //This event is called when health is low
   void OnHealthLow()
   {
         //Handle event response here
   }
   //-------------------------------------------------------
   //This event is called when enemy is dead
   void OnDead()
   {
       //Handle event response here
   }
   //-------------------------------------------------------
   //Ammo run out event
   void OnAmmoExpired()
   {
       //Handle event response here
   }
   //-------------------------------------------------------
}


The enemy class in the code sample 4-2 has been refactored to an event-driven design, where properties such as Ammo and Health are validated not inside the Update function but on assignment. From here, events are raised wherever appropriate based on the newly assigned values. By adopting an event-driven design, we introduce performance optimization and cleanness into our code; we reduce the excess baggage and value checks as found with the Update function in the code sample 4-1, and instead we only allow value-specific events to drive our code, knowing they'll be invoked only at the relevant times.

Event management


Event-driven programming can make our lives a lot easier. But no sooner than we accept events into the design do we come across a string of new problems that require a thoroughgoing resolution. Specifically, we saw in the code sample 4-2 how C# properties for health and ammo are used to validate and detect for relevant changes and then to raise events (such as OnDead) where appropriate. This works fine in principle, at least when the enemy must be notified about events that happen to itself. However, what if an enemy needed to know about the death of another enemy or needed to know when a specified number of other enemies had been killed? Now, of course, thinking about this specific case, we could go back to the enemy class in the code sample 4-2 and amend it to call an OnDead event not just for the current instance but for all other enemies using functions such as SendMessage. But this doesn't really solve our problem in the general sense. In fact, let's state the ideal case straight away; we want every object to optionally listen for every type of event and to be notified about them as and when they happen, just as easily as if the event had happened to them. So the question that we face now is about how to code an optimized system to allow easy event management like this. In short, we need an EventManager class that allows objects to listen to specific events. This system relies on three central concepts, as follows:

  • Event Listener: A listener refers to any object that wants to be notified about an event when it happens, even its own events. In practice, almost every object will be a listener for at least one event. An enemy, for example, may want notifications about low health and low ammo among others. In this case, it's a listener for at least two separate events. Thus, whenever an object expects to be told when an event happens, it becomes a listener.
  • Event Poster: In contrast to listeners, when an object detects that an event has occurred, it must announce or post a public notification about it that allows all other listeners to be notified. In the code sample 4-2, the enemy class detects the Ammo and Health events using properties and then calls the internal events, if required. But to be a true poster in this sense, we require that the object must raise events at a global level.
  • Event Manager: Finally, there's an overarching singleton Event Manager object that persists across levels and is globally accessible. This object effectively links listeners to posters. It accepts notifications of events sent by posters and then immediately dispatches the notifications to all appropriate listeners in the form of events.

Starting event management with interfaces


The first or original entity in the event handling system is the listener—the thing that should be notified about specific events as and when they happen. Potentially, a listener could be any kind of object or any kind of class; it simply expects to be notified about specific events. In short, the listener will need to register itself with the Event Manager as a listener for one or more specific events. Then, when the event actually occurs, the listener should be notified directly by a function call. So, technically, the listener raises a type-specificity issue for the Event Manager about how the manager should invoke an event on the listener if the listener could potentially be an object of any type. Of course, this issue can be worked around, as we've seen, using either SendMessage or BroadcastMessage. Indeed, there are event handling systems freely available online, such as NotificationCenter that rely on these functions. However, we'll avoid them using interfaces and use polymorphism instead, as both SendMessage and BroadcastMessage rely heavily on reflection. Specifically, we'll create an interface from which all listener objects derive.

More information on the freely available NotificationCenter (C# version) is available from the Unity wiki at http://wiki.unity3d.com/index.php?title=CSharpNotificationCenter.


In C#, an interface is like a hollow abstract base class. Like a class, an interface brings together a collection of methods and functions into a single template-like unit. But, unlike a class, an interface only allows you to define function prototypes such as the name, return type, and arguments for a function. It doesn't let you define a function body. The reason being that an interface simply defines the total set of functions that a derived class will have. The derived class may implement the functions however necessary, and the interface simply exists so that other objects can invoke the functions via polymorphism without knowing the specific type of each derived class. This makes interfaces a suitable candidate to create a Listener object. By defining a Listener interface from which all objects will be derived, every object has the ability to be a listener for events.

The following code sample 4-3 demonstrates a sample Listener interface:

01 using UnityEngine;
02 using System.Collections;
03 //-----------------------------------------------------------
04 //Enum defining all possible game events
05 //More events should be added to the list
06 public enum EVENT_TYPE {GAME_INIT,
07                                GAME_END,
08                                 AMMO_EMPTY,
09                                 HEALTH_CHANGE,
10                                 DEAD};
11 //-----------------------------------------------------------
12 //Listener interface to be implemented on Listener classes
13 public interface IListener
14 {
15 //Notification function invoked when events happen
16 void OnEvent(EVENT_TYPE Event_Type, Component Sender, 
    Object Param = null);
17 }
18 //-----------------------------------------------------------


The following are the comments for the code sample 4-3:

  • Lines 06-10: This enumeration should define a complete list of all possible game events that could be raised. The sample code lists only five game events: GAME_INIT, GAME_END, AMMO_EMPTY, HEALTH_CHANGE, and DEAD. Your game will presumably have many more. You don't actually need to use enumerations for encoding events; you could just use integers. But I've used enumerations to improve event readability in code.
  • Lines 13-17: The Listener interface is defined as IListener using the C# interfaces. It supports just one event, namely OnEvent. This function will be inherited by all derived classes and will be invoked by the manager whenever an event occurs for which the listener is registered. Notice that OnEvent is simply a function prototype; it has no body.

More information on C# interfaces can be found at http://msdn.microsoft.com/en-us/library/ms173156.aspx.


Using the IListener interface, we now have the ability to make a listener from any object using only class inheritance; that is, any object can now declare itself as a listener and potentially receive events. For example, a new MonoBehaviour component can be turned into a listener with the following code sample 4-4. This code uses multiple inheritance, that is, it inherits from two classes. More information on multiple inheritance can be found at http://www.dotnetfunda.com/articles/show/1185/multiple-inheritance-in-csharp:

using UnityEngine;
using System.Collections;
public class MyCustomListener : MonoBehaviour, IListener
{
   // Use this for initialization
   void Start () {}
   // Update is called once per frame
   void Update () {}
   //---------------------------------------
   //Implement OnEvent function to receive Events
   public void OnEvent(EVENT_TYPE Event_Type, Component Sender, Object Param = null)
   {
   }
   //---------------------------------------
}

Creating an EventManager


Any object can now be turned into a listener, as we've seen. But still the listeners must register themselves with a manager object of some kind. Thus, it is the duty of the manager to call the events on the listeners when the events actually happen. Let's now turn to the manager itself and its implementation details. The manager class will be called EventManager, as shown in the following code sample 4-5. This class, being a persistent singleton object, should be attached to an empty GameObject in the scene where it will be directly accessible to every other object through a static instance property. More on this class and its usage is considered in the subsequent comments:

001 using UnityEngine;
002 using System.Collections;
003 using System.Collections.Generic;
004 //-----------------------------------
005 //Singleton EventManager to send events to listeners
006 //Works with IListener implementations
007 public class EventManager : MonoBehaviour
008 {
009     #region C# properties
010 //-----------------------------------
011     //Public access to instance
012     public static EventManager Instance
013       {
014             get{return instance;}
015            set{}
016       }
017   #endregion
018
019   #region variables
020       // Notifications Manager instance (singleton design pattern)
021   private static EventManager instance = null;
022
023     //Array of listeners (all objects registered for events)
024     private Dictionary<EVENT_TYPE, List<IListener>> Listeners 
          = new Dictionary<EVENT_TYPE, List<IListener>>();
025     #endregion
026 //-----------------------------------------------------------
027     #region methods
028     //Called at start-up to initialize
029     void Awake()
030     {
031             //If no instance exists, then assign this instance
032             if(instance == null)
033           {
034                   instance = this;
035                   DontDestroyOnLoad(gameObject);
036           }
037             else
038                   DestroyImmediate(this);
039     }
040//-----------------------------------------------------------
041     /// <summary>
042     /// Function to add listener to array of listeners
043     /// </summary>
044     /// <param name="Event_Type">Event to Listen for</param>
045     /// <param name="Listener">Object to listen for event</param>
046     public void AddListener(EVENT_TYPE Event_Type, IListener 
        Listener)
047    {
048           //List of listeners for this event
049           List<IListener> ListenList = null;
050
051           // Check existing event type key. If exists, add to list
052           if(Listeners.TryGetValue(Event_Type, 
                out ListenList))
053           {
054                   //List exists, so add new item
055                   ListenList.Add(Listener);
056                   return;
057           }
058
059           //Otherwise create new list as dictionary key
060           ListenList = new List<IListener>();
061           ListenList.Add(Listener);
062           Listeners.Add(Event_Type, ListenList);
063     }
064 //-----------------------------------------------------------
065       /// <summary>
066       /// Function to post event to listeners
067       /// </summary>
068       /// <param name="Event_Type">Event to invoke</param>
069       /// <param name="Sender">Object invoking event</param>
070       /// <param name="Param">Optional argument</param>
071       public void PostNotification(EVENT_TYPE Event_Type, 
          Component Sender, Object Param = null)
072       {
073           //Notify all listeners of an event
074
075           //List of listeners for this event only
076           List<IListener> ListenList = null;
077
078           //If no event exists, then exit
079           if(!Listeners.TryGetValue(Event_Type, 
                out ListenList))
080                   return;
081
082             //Entry exists. Now notify appropriate listeners
083             for(int i=0; i<ListenList.Count; i++)
084             {
085                   if(!ListenList[i].Equals(null))
086                   ListenList[i].OnEvent(Event_Type, Sender, Param);
087             }
088     }
089 //-----------------------------------------------------------
090     //Remove event from dictionary, including all listeners
091     public void RemoveEvent(EVENT_TYPE Event_Type)
092     {
093           //Remove entry from dictionary
094           Listeners.Remove(Event_Type);
095     }
096 //-----------------------------------------------------------
097       //Remove all redundant entries from the Dictionary
098     public void RemoveRedundancies()
099     {
100             //Create new dictionary
101             Dictionary<EVENT_TYPE, List<IListener>> 
                TmpListeners = new Dictionary
                <EVENT_TYPE, List<IListener>>();
102
103             //Cycle through all dictionary entries
104             foreach(KeyValuePair<EVENT_TYPE, List<IListener>> 
                Item in Listeners)
105             {
106                   //Cycle all listeners, remove null objects
107                   for(int i = Item.Value.Count-1; i>=0; i--)
108                   {
109                         //If null, then remove item
110                         if(Item.Value[i].Equals(null))
111                                 Item.Value.RemoveAt(i);
112                   }
113
114           //If items remain in list, then add to tmp dictionary
115                   if(Item.Value.Count > 0)
116                         TmpListeners.Add (Item.Key, 
                              Item.Value);
117             }
118
119             //Replace listeners object with new dictionary
120             Listeners = TmpListeners;
121     }
122 //-----------------------------------------------------------
123       //Called on scene change. Clean up dictionary
124       void OnLevelWasLoaded()
125       {
126           RemoveRedundancies();
127       }
128 //-----------------------------------------------------------
129     #endregion
130 }

Unlock access to the largest independent learning library in Tech for FREE!
Get unlimited access to 7500+ expert-authored eBooks and video courses covering every tech area you can think of.
Renews at €14.99/month. Cancel anytime

More information on the OnLevelWasLoaded event can be found at http://docs.unity3d.com/ScriptReference/MonoBehaviour.OnLevelWasLoaded.html.


The following are the comments for the code sample 4-5:

  • Line 003: Notice the addition of the System.Collections.Generic namespace giving us access to additional mono classes, including the Dictionary class. This class will be used throughout the EventManager class. In short, the Dictionary class is a special kind of 2D array that allows us to store a database of values based on key-value pairing. More information on the Dictionary class can be found at http://msdn.microsoft.com/en-us/library/xfhwa508%28v=vs.110%29.aspx.
  • Line 007: The EventManager class is derived from MonoBehaviour and should be attached to an empty GameObject in the scene where it will exist as a persistent singleton.
  • Line 024: A private member variable Listeners is declared using a Dictionary class. This structure maintains a hash-table array of key-value pairs, which can be looked up and searched like a database. The key-value pairing for the EventManager class takes the form of EVENT_TYPE and List<Component>. In short, this means that a list of event types can be stored (such as HEALTH_CHANGE), and for each type there could be none, one, or more components that are listening and which should be notified when the event occurs. In effect, the Listeners member is the primary data structure on which the EventManager relies to maintain who is listening for what.
  • Lines 029-039: The Awake function is responsible for the singleton functionality, that is, to make the EventManager class into a singleton object that persists across scenes.
  • Lines 046-063: The AddListener method of EventManager should be called by a Listener object once for each event for which it should listen. The method accepts two arguments: the event to listen for (Event_Type) and a reference to the listener object itself (derived from IListener), which should be notified if and when the event happens. The AddListener function is responsible for accessing the Listeners dictionary and generating a new key-value pair to store the connection between the event and the listener.
  • Lines 071-088: The PostNotification function can be called by any object, whether a listener or not, whenever an event is detected. When called, the EventManager cycles all matching entries in the dictionary, searching for all listeners connected to the current event, and notifies them by invoking the OnEvent method through the IListener interface.
  • Lines 098-127: The final methods for the EventManager class are responsible for maintaining data integrity of the Listeners structure when a scene change occurs and the EventManager class persists. Although the EventManager class persists across scenes, the listener objects themselves in the Listeners variable may not do so. They may get destroyed on scene changes. If so, scene changes will invalidate some listeners, leaving the EventManager with invalid entries. Thus, the RemoveRedundancies method is called to find and eliminate all invalid entries. The OnLevelWasLoaded event is invoked automatically by Unity whenever a scene change occurs. More information on the OnLevelWasLoaded event can be found online at: http://docs.unity3d.com/ScriptReference/MonoBehaviour.OnLevelWasLoaded.html.


#region and #endregion

The two preprocessor directives #region and #endregion (in combination with the code folding feature) can be highly useful for improving the readability of your code and also for improving the speed with which you can navigate the source file. They add organization and structure to your source code without affecting its validity or execution. Effectively, #region marks the top of a code block and #endregion marks the end. Once a region is marked, it becomes foldable, that is, it becomes collapsible using the MonoDevelop code editor, provided the code folding feature is enabled. Collapsing a region of code is useful for hiding it from view, which allows you to concentrate on reading other areas relevant to your needs, as shown in the following screenshot:

event-driven-programming-img-0Enabling code folding in MonoDevelop


To enable code folding in MonoDevelop, select Options in Tools from the application menu. This displays the Options window. From here, choose the General tab in the Text Editor option and click on Enable code folding as well as Fold #regions by default.

Using EventManager


Now, let's see how to put the EventManager class to work in a practical context from the perspective of listeners and posters in a single scene. First, to listen for an event (any event) a listener must register itself with the EventManager singleton instance. Typically, this will happen once and at the earliest opportunity, such as the Start function. Do not use the Awake function; this is reserved for an object's internal initialization as opposed to the functionality that reaches out beyond the current object to the states and setup of others. See the following code sample 4-6 and notice that it relies on the Instance static property to retrieve a reference to the active EventManager singleton:

//Called at start-up
void Start()
{
//Add myself as listener for health change events
EventManager.Instance.AddListener(EVENT_TYPE.HEALTH_CHANGE, this);
}


Having registered listeners for one or more events, objects can then post notifications to EventManager as events are detected, as shown in the following code sample 4-7:

public int Health
{
get{return _health;}
set
{
   //Clamp health between 0-100
   _health = Mathf.Clamp(value, 0, 100);
   //Post notification - health has been changed   EventManager.Instance.
PostNotification(EVENT_TYPE.HEALTH_CHANGE, this, _health);
}
}


Finally, after a notification is posted for an event, all the associated listeners are updated automatically through EventManager. Specifically, EventManager will call the OnEvent function of each listener, giving listeners the opportunity to parse event data and respond where needed, as shown in the following code sample 4-7:

//Called when events happen
public void OnEvent(EVENT_TYPE Event_Type, Component Sender, object Param = null)
{
//Detect event type
switch(Event_Type)
{
   case EVENT_TYPE.HEALTH_CHANGE:
         OnHealthChange(Sender, (int)Param);
   break;
}
}

Summary


This article focused on the manifold benefits available for your applications by adopting an event-driven framework consistently through the EventManager class. In implementing such a manager, we were able to rely on either interfaces or delegates, and either method is powerful and extensible. Specifically, we saw how it's easy to add more and more functionality into an Update function but how doing this can lead to severe performance issues. Better is to analyze the connections between your functionality to refactor it into an event-driven framework. Essentially, events are the raw material of event-driven systems. They represent a necessary connection between one action (the cause) and another (the response). To manage events, we created the EventManager class—an integrated class or system that links posters to listeners. It receives notifications from posters about events as and when they happen and then immediately dispatches a function call to all listeners for the event.

Resources for Article:





Further resources on this subject: