Avoiding the Find() and SendMessage() methods at runtime

The SendMessage() method and family of GameObject.Find() methods are notoriously expensive, and should be avoided at all costs. The SendMessage() method is about 2,000 times slower than a simple function call, and the cost of the Find() method scales very poorly with Scene complexity since it must iterate through every GameObject in the Scene. It is sometimes reasonable to call Find() during initialization of a Scene, such as Awake() and Start(), only for objects that already exists in the Scene. However, using either method for inter-object communication at runtime is likely to generate a very noticeable overhead.

Relying on Find() and SendMessage() type methods is typically symptomatic of poor design, inexperience in programming with C# and Unity, or just plain laziness during prototyping. Their usage has become something of an epidemic among beginner- and intermediate-level projects, such that Unity Technologies feels the need to keep reminding users to avoid using them in a real game, over and over again in their documentation and at their conferences. They only exist as a less programmer-y way to introduce new users to inter-object communication, and for some special cases where they can be used in a lazy, but responsible, way.

To be fair, Unity targets a wide demographic of users, from individual hobbyists, students, and those with delusions of grandeur, to small, mid-sized, and large development teams. This results in an incredibly wide range of software development ability. When you're starting out with Unity, it can be difficult to figure out on your own what you should be doing differently, especially given how the Unity engine does not adhere to the design paradigms of many other game engines. It has some foreign and quirky concepts surrounding scenes and prefabs, as well as no god class entry points, nor any obvious raw-data storage systems to work with.

Since we're talking about scripting optimization in this section, let's explore the subject in some detail, discussing some alternative methods for inter-object communication.

Let's start by examining a worst-case example, which uses both Find() and SendMessage() methods, and discover some ways to improve upon it. The following example method attempts to instantiate a given number of enemies from a prefab, and then notifies an EnemyManager object of their existence:

public void SpawnEnemies(int numEnemies) {
  for(int i = 0; i < numEnemies; ++i) {
    GameObject enemy = (GameObject)GameObject.Instantiate(_enemyPrefab, Vector3.zero, Quaternion.identity);
    GameObject enemyManagerObj = GameObject.Find("EnemyManager");
    enemyManagerObj.SendMessage("AddEnemy", enemy, SendMessageOptions.DontRequireReceiver);
  }
}

Putting method calls inside a loop, which always output to the same result, is a big red flag for poor performance, and when we're dealing with expensive methods such as Find(), we should always look for ways to call them as few times as possible. Ergo, one improvement we can make is to move the Find() call outside of the for loop and cache the result in a local variable to be used within the loop.

We can also optimize our usage of the SendMessage() method by replacing it with a GetComponent() call. This replaces a very costly method with a much gentler variation, achieving effectively the same result.

This gives us the following:

public void SpawnEnemies(int numEnemies) {

  GameObject enemyManagerObj = GameObject.Find("EnemyManager");
  EnemyManagerComponent enemyMgr = enemyManagerObj.GetComponent<EnemyManagerComponent>();

  for(int i = 0; i < numEnemies; ++i) {
    GameObject enemyIcon = (GameObject)GameObject.Instantiate(_enemyPrefab, Vector3.zero, Quaternion.identity);
    enemyMgr.AddEnemy(enemy);
  }
}

If this method is called during the initialization of the Scene, and we're not overly concerned with loading time, then we can probably consider ourselves finished with our optimization work.

However, we often need new objects that are instantiated at runtime to find an existing object to communicate with. In this example, we want new enemy objects to register with our EnemyManagerComponent so that it can do whatever it needs to do to control the enemy objects in our Scene. We would like a reliable and fast way for new objects to find existing objects without unnecessary usage of the Find() method, due to the overhead involved.

There are multiple approaches we can take to solving this problem, each with their own benefits and pitfalls:

  • Static classes
  • Singleton Components
  • Assign references to pre-existing objects
  • A global messaging system

Static classes

This approach involves creating a class that is globally accessible to the entire codebase at any time. The object stays alive from the moment the application starts, to the moment it is closed. Global manager classes are often frowned upon, since the name doesn't say much about what it's meant to do, and they can be difficult to debug since changes can occur from anywhere, at any point during runtime. In addition, it is probably the least robust approach when it comes to changing or replacing it at a future date. Despite all of these drawbacks, it is by far the easiest solution to implement, and so we will cover it first.

The Singleton design pattern is a common way of ensuring that we have a globally-accessible object, and that only one instance ever exists in memory. However, the way that Singletons are primarily used (note the qualifier) in Unity projects, can be easily replaced with a simple C# Static class without the need to implement private constructors, and the unnecessary property access of an Instance variable. Essentially, implementing a typical Singleton design pattern in C# just takes more code, and time, to achieve the same result as a static class.

A static class that functions in much the same way as our EnemyManagerComponent is used in the previous example can be defined as follows:

using System.Collections.Generic;

public static class EnemyManager {
  static List<GameObject> _enemies;

  public static void AddEnemy(GameObject enemy) {
    _enemies.Add (enemy);
  }

  public static void RollCall() {
    for(int i = 0; i < _enemies.Count; ++i) {
      Debug.Log (string.Format("Enemy "{0}" reporting in...", _enemies[i].name));
    }
  }
}

Note that every member and method has the static keyword attached, which implies that only one instance of this object will ever reside in memory. Static classes, by definition, do not allow any nonstatic instance members to be defined, as that would imply that we could somehow duplicate the object.

Static classes can be given a static constructor, which can be used to initialize member data. A static constructor can be defined like so, and it is called the moment the class is first accessed (either through a member variable or a member function):

static EnemyManager() {
  _enemies = new List<GameObject>();
}

This type of global class is generally considered to be a cleaner and easier-to-use version of the typical Singleton design pattern in the world of C# development.

Singleton Components

The disadvantage of the static class approach is that they must inherit from the lowest form of class—Object. This means that static classes cannot inherit from MonoBehaviour and therefore we cannot make use of any of its Unity-related functionality, including the all-important event callbacks, as well as Coroutines. Also, since there's no object to select, we lose the ability to inspect the object's data at runtime through the Inspector. These are features that we may wish to make use of in our global Singleton classes.

A common solution to this problem is to implement a "Singleton as a Component" class that spawns a GameObject containing itself, and providing static methods to grant global access. Note that, in this case, we must essentially implement the typical Singleton design pattern, with private static instance variables, and a global Instance method for global access.

Here is the definition for a SingletonAsComponent class:

public class SingletonAsComponent<T> : MonoBehaviour where T : 
SingletonAsComponent<T> {
  
  private static T __Instance;
  
  protected static SingletonAsComponent<T> _Instance {
      get {
        if(!__Instance) {
              T [] managers = 
              GameObject.FindObjectsOfType(typeof(T)) as T[];
              if (managers != null) {
                  if(managers.Length == 1) {
                      __Instance = managers[0];
                      return __Instance;
                  } else if (managers.Length > 1) {
                      Debug.LogError("You have more than one " + 
                      typeof(T).Name + " in the scene. You only 
                      need 1, it's a singleton!");
                      for(int i = 0; i < managers.Length; ++i) {
                          T manager = managers[i];
                          Destroy(manager.gameObject);
                      }
                  }
              }

              GameObject go = new GameObject(typeof(T).Name, 
              typeof(T));
              __Instance = go.GetComponent<T>();
              DontDestroyOnLoad(__Instance.gameObject);
          }
          return __Instance;
      } 
      set {
          __Instance = value as T;
      }
  }
}

Since we wish this to be a global and persistent object, we need to call DontDestroyOnLoad() shortly after the GameObject is created. This is a special function that tells Unity that we wish the object to persist between Scenes for as long as the application is running. From that point onward, when a new scene is loaded, the object will not be destroyed and will retain all of its data.

This class definition assumes two things. Firstly, because it is using generics to define its behavior, it must be derived from in order to create a concrete class. Secondly, a method will be defined to assign the _Instance variable and cast it to/from the correct Type.

For example, the following is all that is needed to successfully generate a new SingletonAsComponent derived class called MySingletonComponent:

public class MySingletonComponent : SingletonAsComponent<MySingletonComponent> {
  public static MySingletonComponent Instance {
    get { return ((MySingletonComponent)_Instance); }
    set { _Instance = value; }
  }
}

This class can be used at runtime by having any other object access the Instance property at any time. If the Component does not already exist in our Scene, then the SingletonAsComponent base class will instantiate its own GameObject and attach an instance of the derived class to it as a Component. From that point forward, access through the Instance property will reference the Component that was created.

Tip

While it is possible, we should not place our SingletonAsComponent derived class in a Scene Hierarchy. This is because the DontDestroyOnLoad() method will never be called! This would prevent the Singleton Component's GameObject from persisting when the next Scene is loaded.

Proper cleanup of a Singleton Component can be a little convoluted because of how Unity tears down Scenes. An object's OnDestroy() method is called whenever it is destroyed during runtime. The same method is called during application shutdown, whereby every Component on every GameObject has its OnDestroy() method called by Unity. Application shutdown also takes place when we end Play Mode in the Editor and return to Edit Mode. However, destruction of objects occurs in a random order, and we cannot assume that the Singleton Component will be the last object destroyed.

Consequently, if any object attempts to do anything with the Singleton in the middle of their OnDestroy() method, then they will be calling the Instance property. If the Singleton has already been destroyed prior to this moment, then calling Instance during another object's destruction would create a new instance of the Singleton Component in the middle of application shutdown! This can corrupt our Scene files, as instances of our Singleton Components will be left behind in the Scene. If this happens, then Unity will throw the following error message at us:

Singleton Components

The reason some objects may wish to call into our Singleton during destruction is that Singletons often make use of the Observer pattern. This design pattern allows other objects to register/deregister with them for certain tasks, similar to how Unity latches onto callback methods, but in a less automated fashion. We will see an example of this in the upcoming section A global messaging system. Objects that are registered with the system during construction will want to deregister with the system during their own shutdown, and the most convenient place to do this is within its OnDestroy() method. Consequently, such objects are likely to run into the aforementioned problem, where Singletons are accidentally created during application shutdown.

To solve this problem, we need to make three changes. Firstly, we need to add an additional flag to the Singleton Component, which keeps track of its active state, and disable it at the appropriate times. This includes the Singleton's own destruction, as well as application shutdown (OnApplicationQuit() is another useful Unity callback for MonoBehaviours, which is called during this time):

private bool _alive = true;
void OnDestroy() { _alive = false; }
void OnApplicationQuit() { _alive = false; }

Secondly, we should implement a way for external objects to verify the Singleton's current state:

public static bool IsAlive {
  get {
    if (__Instance == null)
      return false;
    return __Instance._alive;
  }
}

Finally, any object that attempts to call into the Singleton during its own OnDestroy() method, must first verify the state using the IsAlive property before calling Instance. For example:

public class SomeComponent : MonoBehaviour {
    void OnDestroy() {
    if (MySingletonComponent.IsAlive) {
        MySingletonComponent.Instance.SomeMethod();
    }
  }
}

This will ensure that nobody attempts to access Instance during destruction. If we don't follow this rule, then we will run into problems where instances of our Singleton object will be left behind in the Scene after returning to Edit Mode.

The irony of the Singleton Component approach is that we are using one of Unity's Find() methods to determine whether or not one of these Singleton Components already exists in the Scene before we attempt to assign the __Instance reference variable. Fortunately, this will only happen when the Singleton Component is first accessed, but it's possible that the initialization of the Singleton would not necessarily occur during Scene initialization and can therefore cost us a performance spike at a bad moment during gameplay, when this object is first instantiated and Find() gets called. The workaround for this is to have some god class confirm that the important Singletons are instantiated during Scene initialization by simply calling Instance on each one.

The downside to this approach is that if we later decide that we want more than one of these manager classes executing at once, or we wish to separate its behavior to be more modular, then there would be a lot of code that needs to change.

There are further alternatives that we can explore, such as making use of Unity's built-in bridge between script code and the Inspector interface.

Assigning references to pre-existing objects

Another approach to the problem of inter-object communication is to use Unity's built-in serialization systems. Software design purists tend to get a little combative about this feature, since it breaks encapsulation; it makes variables marked private act in a way that treats them as public. Even though the value only becomes public with respect to the Unity Inspector and nothing else, this is still enough to wave some red flags.

However, it is a very effective tool for improving development workflow. This is particularly true when artists, designers, and programmers are all tinkering with the same product, where each has wildly varying levels of computer science and software programming knowledge. Sometimes it's worth bending a few rules in the name of productivity.

Whenever we create a public variable, Unity automatically serializes and exposes the value in the Inspector interface when the Component is selected. However, public variables are dangerous from a software design perspective—these variables can be changed through code at any time, which can make it hard to keep track of the variable, and introduce a lot of unexpected bugs.

As an alternative, we can take any private or protected member variable of a class and expose it to the Unity Editor Inspector interface with the [SerializeField] attribute. This approach is preferred over public variables, as it gives us more control of the situation. This way, at least we know the variables cannot be changed at runtime via code outside the class (or derived class), and therefore maintain encapsulation from the perspective of script code.

For example, the following class exposes three private variables to the Inspector:

public class EnemySpawnerComponent : MonoBehaviour {

  [SerializeField] private int _numEnemies;
  [SerializeField] private GameObject _enemyPrefab;
  [SerializeField] private EnemyManagerComponent _enemyManager;

  void Start() {
    SpawnEnemies(_numEnemies);
  }

    void SpawnEnemies(int _numEnemies) {
      for(int i = 0; i < _numEnemies; ++i) {
        GameObject enemy = (GameObject)GameObject.Instantiate(_enemyPrefab, Vector3.zero, Quaternion.identity);
        _enemyManager.AddEnemy(enemy);
      }
    }
}

Tip

Note that the private access specifiers shown in the preceding code are redundant keywords in C#, as member variables are always private unless specified otherwise, but these access specifiers are included for completeness.

Looking at this Component in the Inspector view reveals three values, initially given default values of 0, or null, which can be modified through the Inspector interface:

Assigning references to pre-existing objects

We can drag and drop a Prefab reference from the Projects window into the Enemy Prefab field, or, if we felt so inclined, even a reference to another GameObject that is present in the Scene. Although, given that it's being used like a Prefab in the code, it would be unwise to do this, since we would be cloning a GameObject that might have already undergone changes in the Scene. Prefabs serve the purpose of a blueprint from which to instantiate new GameObjects and should be used as such.

The Enemy Manager field is interesting because it is a Component reference and not a GameObject reference. If a GameObject is dropped into this field, then it will refer to the Component on the given object, as opposed to the GameObject that we dragged and dropped into the field. If the given object does not have the expected Component, then nothing will be assigned.

Tip

A common usage of the Component reference technique is to obtain references to Components attached to the same GameObject it is attached to. This is an alternative approach to the topic discussed in the section entitled Cache Component References, earlier in this chapter.

The danger here is that since Prefabs are essentially GameObjects, Prefabs with the required Component can be assigned to these fields, even though we might not wish them to be. Unity loads Prefabs into memory much like GameObjects, and assumes they'll be used in Prefab-like ways; that is, treated as nothing more than blueprints to be instantiated from on an as-needed basis. However, they still count as data stored in memory and can therefore be edited on a whim, making them susceptible to changes that directly affect all future GameObjects instantiated from them.

To make matters worse, these changes become permanent even if they are made during Play Mode, since Prefabs occupy the same memory space whether Play Mode is active or not. This means that we can accidentally corrupt our Prefabs, if we assign them to the wrong fields. Consequently, this approach is a more team-friendly way of solving the original problem of inter-object communication, but it is not ideal due to all of the risks involved with team members accidentally breaking things or leaving null references in place.

It is also important to note that not all objects can be serialized by the Inspector view. Unity can serialize all primitive data types (ints, floats, strings, and bools), various built-in types such as (Vector3, Quaternion, and so on) enums, classes, and structs, as well as arrays and lists of other serializable types. However, it is unable to serialize static fields, read-only values, properties, and dictionaries.

Tip

Some Unity developers like to implement pseudo-serialization of dictionaries via two separate lists for keys and values, along with a Custom Editor script, or via a single list of objects, which contain both keys and values. Both of these solutions are a little clumsy, but perfectly valid.

The last solution we will look at will provide a way to hopefully get the best of both worlds by combining ease of implementation, ease of extension, and strict usage that avoids too much human error.

A global messaging system

The final suggested approach to the problem of inter-object communication is to implement a global messaging system that any object can access and send messages through to any object that may be interested in listening to that specific type of message. Objects either send messages or listen for them, and the responsibility is on the listener to decide what messages it is interested in. The message sender can broadcast the message without caring at all who is listening. This approach is excellent for keeping our code modular and decoupled.

The kinds of message we wish to send can take many forms, such as including data values, references, instructions for listeners, and more, but they should all have a common, basic declaration that our messaging system can use to determine what the message is, and who it is intended for.

The following is a simple class definition for a Message object:

public class BaseMessage {
  public string name;
  public BaseMessage() { name = this.GetType().Name; }
}

The BaseMessage class's constructor caches the Type in a local string property to be used later for cataloguing and distribution purposes. Caching this value is important as each call to GetType().Name will result in a new string being allocated on the heap, and we want to minimize this as much as possible. Our custom messages must derive from this class, which allows them to add whatever superfluous data they wish, while still maintaining the ability to be sent through our messaging system. Take note that, despite acquiring the Type name during the base class constructor, the name property will still contain the name of the derived class, not the base class.

Moving on to our MessagingSystem class, we should define its features by what kind of requirements we need it to fulfill:

  • It should be globally accessible
  • Any object (MonoBehaviour or not) should be able to register/deregister as listeners, to receive specific message types (that is, the Observer pattern)
  • Registering objects should provide a method to call when the given message is broadcast
  • The system should send the message to all listeners within a reasonable timeframe, but not choke on too many requests at once

A globally accessible object

The first requirement makes the messaging system an excellent candidate for a Singleton object, since we should only ever need one instance of the system. Although, it is wise to think long and hard if this is truly the case before committing to implementing a Singleton. If we later decide that we want multiple instances of this object to exist, then it can be difficult to refactor due to all of the dependencies we will gradually introduce to our codebase as we use the system more and more.

For this example, we will assume that we are absolutely positive that we will only need one of these systems, and design it accordingly.

Registration

Meeting the second and third requirements can be achieved by offering some public methods that allow registration with the messaging system. If we force the listening object to provide us a delegate function to call when the message is broadcast, then this allows listeners to customize which method is called for which message. This can make our codebase much easier to understand, if we name the delegate after the message it is intended to process.

Tip

Delegate functions are incredibly useful constructs in C# that allows us to pass local methods around as arguments to other methods, and are typically used for callbacks. Check the MSDN C# Programming Guide for more information on delegates at https://msdn.microsoft.com/en-us/library/ms173171.aspx.

In some cases, we might wish to broadcast a general notification message and have all listeners do something in response, such as an "Enemy Spawned" message. Other times, we might be sending a message that specifically targets a single listener amongst a group. For example, we might wish to send an "Enemy Health Value Changed" message that is intended for a specific health bar object that is attached to the enemy that was damaged. If we implement a way for listeners to stop message processing early, then we can save a significant number of CPU cycles, if there are many listeners waiting for the same message type.

The delegate we define should therefore provide a way of retrieving the message via an argument, and return a response that determines whether or not processing for the message should stop when the listener is done with it. The decision on whether to stop processing or not can be achieved by returning a simple Boolean, where true implies that this listener has handled the message, and processing for the message must stop.

Here is the definition for the delegate:

public delegate bool MessageHandlerDelegate(BaseMessage message);

Listeners must define a method of this form and pass a reference to it when it registers with the MessagingSystem, thus providing an entry point for the messaging system to call when the message is broadcast.

Message processing

The final requirement for our messaging system is that this object has some kind of timing-based mechanism built in to prevent it from choking on too many messages at once. This means that, somewhere in the process, we will need to make use of MonoBehaviour event callbacks in order to work during Unity's Update() and be able to count time.

This can be achieved with the static class-based Singleton (which we defined earlier), which would require some other MonoBehaviour-based god class to call into it, informing it that the Scene has updated. Alternatively, we can use the SingletonAsComponent to achieve the same thing, but do so independently of any god class. The only difference between the two is whether or not the system is dependent on the control of other objects.

The SingletonAsComponent approach is probably the best, since there aren't too many occasions where we wouldn't want this system acting independently, even if much of our game logic depends upon it. For example, even if the game was paused, we wouldn't want the game logic to pause our messaging system. We still want the messaging system to continue receiving and processing messages so that we can, for example, keep UI-related Components communicating with one another while the gameplay is in a paused state.

Implementing the messaging system

Let's define our messaging system by deriving from the SingletonAsComponent class, and provide a method for objects to register with it:

using System.Collections.Generic;

public class MessagingSystem : SingletonAsComponent<MessagingSystem> {

  public static MessagingSystem Instance {
    get { return ((MessagingSystem)_Instance); }
    set { _Instance = value; }
  }

  private Dictionary<string,List<MessageHandlerDelegate>> _listenerDict = new Dictionary<string,List<MessageHandlerDelegate>>();

  public bool AttachListener(System.Type type, MessageHandlerDelegate handler) {
    if (type == null) {
      Debug.Log("MessagingSystem: AttachListener failed due to no message type specified");
      return false;
    }

    string msgName = type.Name;

    if (!_listenerDict.ContainsKey(msgName)) {
      _listenerDict.Add(msgName, new List<MessageHandlerDelegate>());
    }

    List<MessageHandlerDelegate> listenerList = _listenerDict[msgName];
    if (listenerList.Contains(handler)) {
      return false; // listener already in list
    }

    listenerList.Add(handler);
    return true;
  }
}

The _listenerDict variable is a dictionary of strings mapped to lists of MessageHandlerDelegates. This dictionary organizes our listener delegates into lists by which message type they wish to listen to. Thus, if we know what message type is being sent, then we can quickly retrieve a list of all delegates that have been registered for that message type. We can then iterate through the list, querying each listener to see if one of them wants to handle it.

The AttachListener() method requires two parameters; a message type in the form of a System.Type, and a MessageHandlerDelegate to send the message to when it comes through the system.

Message queuing and processing

In order to process messages, our MessagingSystem should maintain a queue of incoming message objects so that we can process them in the order they are broadcasted:

private Queue<BaseMessage> _messageQueue = new Queue<BaseMessage>();

public bool QueueMessage(BaseMessage msg) {
  if (!_listenerDict.ContainsKey(msg.name)) {
    return false;
  }
  _messageQueue.Enqueue(msg);
  return true;
}

The method simply checks if the given message type is present in our dictionary before adding it to the queue. This effectively tests whether or not an object actually cares to listen to the message before we queue it to be processed later. We have introduced a new private member variable, _messageQueue, for this purpose.

Next, we'll add a definition for Update(). This method will be called regularly by the Unity Engine. Its purpose is to iterate through the current contents of the message queue, one message a time, verify whether or not too much time has passed since we began processing, and if not, pass them along to the next stage in the process:

private float maxQueueProcessingTime = 0.16667f;

void Update() {
  float timer = 0.0f;
  while (_messageQueue.Count > 0) {
    if (maxQueueProcessingTime > 0.0f) {
      if (timer > maxQueueProcessingTime)
        return;
    }

    BaseMessage msg = _messageQueue.Dequeue();
    if (!TriggerMessage(msg))
      Debug.Log("Error when processing message: " + msg.name);

    if (maxQueueProcessingTime > 0.0f)
      timer += Time.deltaTime;
  }
}

The time-based safeguard is in place to make sure that it does not exceed a processing time limit threshold. This prevents the messaging system from freezing our game if too many messages get pushed through the system too quickly. If the total time limit is exceeded, then all message processing will stop, leaving any remaining messages to be processed during the next frame.

Lastly, we need to define the TriggerMessage() method, which distributes messages to listeners:

public bool TriggerMessage(BaseMessage msg) {
  string msgName = msg.name;
  if (!_listenerDict.ContainsKey(msgName)) {
    Debug.Log("MessagingSystem: Message "" + msgName + "" has no listeners!");
    return false; // no listeners for message so ignore it
  }

  List<MessageHandlerDelegate> listenerList = _listenerDict[msgName];

  for(int i = 0; i < listenerList.Count; ++i) {
    if (listenerList[i](msg))
      return true; // message consumed by the delegate
  }
  return true;
}

This method is the main workhorse of the messaging system. The TriggerEvent()'s purpose is to obtain the list of listeners for the given message type and give each of them an opportunity to process it. If one of the delegates returns true, then processing of the current message ceases and the method exits, allowing the Update() method to process the next message.

Normally, we would want to use QueueEvent() to broadcast messages, but TriggerEvent() can be called instead. This method allows message senders to force their messages to be processed immediately without waiting for the next Update() event. This bypasses the throttling mechanism, but this might be necessary for messages that need to be sent during critical moments in gameplay, where waiting one additional frame might result in strange-looking behavior.

Implementing a custom message

We've created the messaging system, but an example of how to use it would help us wrap our heads around the concept. Let's start by defining a simple message class, which we can use to transmit some data:

public class MyCustomMessage : BaseMessage {
  public readonly int _intValue;
  public readonly float _floatValue;
  public MyCustomMessage(int intVal, float floatVal {
    _intValue = intVal;
    _floatValue = floatVal;
  }
}

Good practice for message objects is to make their member variables readonly. This ensures that the data cannot be changed after the object's construction. This safeguards the content of our messages against being altered, as they're passed between one listener and another.

Message registration

Here's a simple class that registers with the messaging system, requesting to have its HandleMyCustomMessage() method called whenever a MyCustomMessage object is broadcast from anywhere in our codebase:

public class TestMessageListener : MonoBehaviour {

  void Start() {
    MessagingSystem.Instance.AttachListener(typeof(MyCustomMessage), this.HandleMyCustomMessage);
  }

  bool HandleMyCustomMessage(BaseMessage msg) {
    MyCustomMessage castMsg = msg as MyCustomMessage;
    Debug.Log (string.Format("Got the message! {0}, {1}", castMsg._intValue, castMsg._floatValue));
    return true;
  }
}

Whenever a MyCustomMessage object is broadcast (from anywhere!), this listener will retrieve the message through its HandleMyCustomMessage() method. It can then typecast it into the appropriate derived message type and handle the message in its own unique way. Other classes can register for the same message, and handle it differently through its own custom delegate method (assuming an earlier object didn't return true from its own delegate).

We know what type of message will be provided by the msg argument of the HandleMyCustomMessage() method, because we defined it during registration through the AttachListener() call. Due to this, we can be certain that our typecasting is safe, and we can save time by not having to do a null reference check, although, technically, there is nothing stopping us using the same delegate to handle multiple message types! In these cases, though we would need to implement a way of determining which message object is being passed, and treat it accordingly. The best approach is to define a unique method for each message type in order to keep things appropriately decoupled.

Note how the HandleMyCustomMessage method definition matches the function signature of MessageHandlerDelegate, and that it is being referenced in the AttachListener() call. This is how we tell the messaging system what method to call when the given message type is broadcast, and how delegates ensure type safety. If the function signature had a different return value or a different list of arguments, then it would be an invalid delegate for the AttachListener() method, and we would get compiler errors.

The beautiful part is that we're free to give the delegate method whatever name we want. The most sensible approach is to name the method after the message which it handles. This makes it clear to anyone reading our code what the method is used for and what message object type is required to call it.

Message sending

Finally, let's implement an example of sending a message so that we can test this system out! Here's a component that will broadcast an instance of our MyCustomMessage class through the messaging system when the Space Bar is pressed:

public class TestMessageSender : MonoBehaviour {

  public void Update() {
    if (Input.GetKeyDown (KeyCode.Space)) {
      MessagingSystem.Instance.QueueMessage(new MyCustomMessage(5, 13.355f));
    }
  }
}

If we add both the TestMessageSender and TestMessageListener objects to our Scene and press the Space bar, we should see a log message appear in the console, informing us of a successful test:

Message sending

Our MessagingSystem Singleton object will be created immediately upon Scene initialization, when the TestMessageListener's Start() method is called and it registers the HandleMyCustomMessage delegate. No additional effort is required on our part to create the Singleton we need.

Message cleanup

Since message objects are classes, they will be created dynamically in heap memory and will be disposed of shortly afterwards when the message has been processed and distributed amongst all listeners. However, as you will learn in Chapter 7, Masterful Memory Management, this will eventually result in a garbage collection as heap memory accumulates over time. If our application runs for long enough, it will eventually result in the occasional garbage collection. Therefore, it is wise to use the messaging system sparingly and avoid spamming messages too frequently on every update.

The more important clean-up operation to consider is deregistration of delegates if an object needs to be destroyed or de-spawned. If we don't handle this properly, then the messaging system will hang on to delegate references that prevent objects from being fully destroyed and freed from memory.

Essentially, we need to pair every AttachListener() call with an appropriate DetachListener() method when the object is destroyed, disabled, or we otherwise decide that we no longer need it to be queried when messages are being sent.

The following method definition in the MessagingSystem class will detach a listener for a specific event:

public bool DetachListener(System.Type type, MessageHandlerDelegate handler)
  {
    if (type == null) {
      Debug.Log("MessagingSystem: DetachListener failed due to no message type specified");
      return false;
    }

    string msgName = type.Name;

    if (!_listenerDict.ContainsKey(type.Name)) {
      return false;
    }

    List<MessageHandlerDelegate> listenerList = _listenerDict[msgName];
    if (!listenerList.Contains (handler)) {
      return false;
    }

    listenerList.Remove(handler);
    return true;
  }

Here is an example usage of the DetachListener() method, added to our TestMessageListener class:

void OnDestroy() {
  if (MessagingSystem.IsAlive) {
    MessagingSystem.Instance.DetachListener(typeof(MyCustomMessage), this.HandleMyCustomMessage);
  }
}

Note how this definition makes use of the IsAlive property declared in the SingletonAsComponent class. This safeguards us against the aforementioned problems during application shutdown, where we cannot guarantee that the Singleton was destroyed last.

Wrapping up the messaging system

Congratulations are in order, as we have finally built a fully functional global messaging system that any and all objects can interface with, to send messages between one another! A useful feature of this approach is that it is MonoBehaviour-agnostic, meaning that the message senders and listeners do not even need to derive from MonoBehaviour to interface with the messaging system; it just needs to be a class that provides a message type and a delegate function of the matching function signature.

As far as benchmarking the MessagingSystem class goes, we should find that if it is capable of processing hundreds, if not thousands of messages in a single frame with minimal CPU overhead (depending on the CPU, of course). The CPU usage is essentially the same whether one message is being distributed to 100 different listeners, 100 messages are distributed to just one listener. It costs about the same either way.

Even if we're predominantly sending messages during UI or gameplay events, this is probably far more power than we need. So, if it does seem to be causing performance problems, then it's far more likely to be caused by what the listener delegates are doing with the message than the messaging system's ability to process those messages.

There are many ways to enhance the messaging system to provide more useful features we may need in the future, such as:

  • Allow message senders to suggest a delay (in time or frame count) before a message is processed and distributed
  • Allow message listeners to define a priority for how urgently it should receive messages compared to other listeners waiting for the same message type—a way of skipping to the front of the queue if it registered later than other listeners
  • Implement some safety checks to handle situations where a listener gets added to the list of message listeners for a particular message, while a message of that type is still being processed—C# will throw an enumeration exception at us since the delegate list will be changed by AttachListener(),while it is still being iterated through in TriggerEvent()

At this point, we have probably explored messaging systems enough, so these tasks will be left as an academic exercise for you to undertake, if you become comfortable using this solution in your games.

Let's explore some further techniques that we can use to improve performance through scripting.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.7.208