
Message registration
The following code contains a pair of simple classes that register with the messaging system, each requesting to have one of their methods called whenever certain types of messages have been broadcast from anywhere in our code base:
public class EnemyManagerWithMessagesComponent : MonoBehaviour {
private List<GameObject> _enemies = new List<GameObject>();
[SerializeField] private GameObject _enemyPrefab;
void Start() {
MessagingSystem.Instance.AttachListener(typeof(CreateEnemyMessage),
this.HandleCreateEnemy);
}
bool HandleCreateEnemy(Message msg) {
CreateEnemyMessage castMsg = msg as CreateEnemyMessage;
string[] names = { "Tom", "Dick", "Harry" };
GameObject enemy = GameObject.Instantiate(_enemyPrefab,
5.0f * Random.insideUnitSphere,
Quaternion.identity);
string enemyName = names[Random.Range(0, names.Length)];
enemy.gameObject.name = enemyName;
_enemies.Add(enemy);
MessagingSystem.Instance.QueueMessage(new EnemyCreatedMessage(enemy,
enemyName));
return true;
}
}
public class EnemyCreatedListenerComponent : MonoBehaviour {
void Start () {
MessagingSystem.Instance.AttachListener(typeof(EnemyCreatedMessage),
HandleEnemyCreated);
}
bool HandleEnemyCreated(Message msg) {
EnemyCreatedMessage castMsg = msg as EnemyCreatedMessage;
Debug.Log(string.Format("A new enemy was created! {0}",
castMsg.enemyName));
return true;
}
}
During initialization, the EnemyManagerWithMessagesComponent class registers to receive messages of the CreateEnemyMessage type, and will process the message through its HandleCreateEnemy() delegate. During this method, it can typecast the message into the appropriate derived message type and resolves the message in its own unique way. Other classes can register for the same message and resolve it differently through its own custom delegate method (assuming that an earlier listener didn't return true from its own delegate).
We know what type of messages will be provided by the msg argument of the HandleCreateEnemy() 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, nothing is stopping us using the same delegate to handle multiple message types. In these cases, though, we will need to implement a way to determine which message object is being passed and treat it accordingly. However, the best approach is to define a unique method for each message type to keep things appropriately decoupled. There really is little benefit in trying to use one monolithic method to handle all message types.
Note how the HandleEnemyCreated() method definition matches the function signature of MessageHandlerDelegate (that is, it has the same return type and argument list), 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. Also, note that HandleEnemyCreated() is a private method, and yet our MessagingSystem class can call it. This is a useful feature of delegates in that we can allow only systems we give permission to call this message handler. Exposing the method publicly might lead to some confusion in our code's API, and developers may think that they're meant to call the method directly, which is not its intended use.
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 that it handles. This makes it clear to anyone reading our code what the method is used for and what message object type must be sent to call it. This makes the future parsing and debugging of our code much more straightforward since we can follow the chain of events by the matching names of the messages and their handler delegates.
During the HandleCreateEnemy() method, we also queue another event, which broadcasts EnemyCreatedMessage instead. The second class, EnemyCreatedListenerComponent, registers to receive these messages and then prints out a message containing that information. This is how we would implement a way for subsystems to notify other subsystems of changes. In a real application, we might register a UI system to listen for these types of messages and update a counter on the screen to show how many enemies are now active. In this case, the enemy management and UI systems are appropriately decoupled such that neither needs to know any specific information about how the other operates to do their assigned tasks.
If we now add EnemyManagerWithMessagesComponent, EnemyCreatorComponent, and EnemyCreatedListenerComponent to our scene, and press the spacebar several times, we should see log messages appear in the Console window, informing us of a successful test:

Note that a MessagingSystem singleton object will be created during scene initialization, when either the EnemyManagerWithMessagesComponent or EnemyCreatedListenerComponent object's Start() methods are called (whichever happens first), since that is when they register their delegates with the messaging system, which accesses the Instance property, and hence creates the necessary GameObject instance containing the singleton component. No additional effort is required on our part to create the MessagingSystem object.