Sunday, September 16, 2012

Messaging System


At the start of the project I brainstormed how objects and various components would communicate with each other. Researching online one finds a number of message passing systems. I wanted something slim to fit the small scope of my game. I found a design, I thought fairly useful, by Matthew Wegner on his Blurst Technology website. Reference provided below.

It works by instantiating a singleton instance of the Communicator component. All other components make use of the Communicator's function RegisterListener, to assign a listener for a given message type. A function to call when this message type is received. 












Modifications


A number of modifications have been made to the original script. First, It is rewritten in C#, a superficial change. Second, more importantly, it does not use the Component.SendMessage function. There are debates on forums about the performance impact of this function, mainly because it uses reflection. Whether this is true or not I don't know with any certainty. I decided to avoid the function altogether since I planned on using this system in future projects. Instead I use 'events' and 'delegates' to call the appropriate function when a message is received. To transmit data a different types of message classes must be created. More on that below. Lastly, one can block and unblock certain types of messages from being transmitted to there destination. I found this to be occasionally useful.

Message Class And MessageType


Message class is the base class of all messages. Since one cannot anticipate what concrete messages will be created, the Communicator transmits all messages in the form of the base class. The message is then cast into it's concrete class, if necessary, when it is received by the receiver. The base class has one attribute which represents the type of message it is. For the base class it is of MessageType.Message. Every new type of message created must to be added to the MessageType Enum.

public class Message {
private MessageType m_Name;
public MessageType MessageType
{ get { return m_Name;} set { m_Name = value; } }
public Message()
{ m_Name = MessageType.MESSAGE; }
public Message(MessageType type)
{ m_Name = type; }
}
view raw Message.cs hosted with ❤ by GitHub
public enum MessageType
{
MESSAGE,
LEVEL_START
};
view raw MessageType.cs hosted with ❤ by GitHub

Say for example we have a MessageType to indicate a level start. Since we need to pass extra information  to the receiver we create a new class to represent this type of message and subclass to that of Message. The constructor sets what type of message this is.
MessageType = MessageType.LEVEL_START;

How Broadcasting Works




How To Use The Communicator/Message classes


The following code demonstrates how to use the message and communicator classes. Being a trivial example, both functions called are in the same script. However, this does not have to be the case, and most likely would not be, since it is useless for the calling script to call its own functions via the Communicator. To run this example, attach the Driver script to a gameobject and run the scene.

using UnityEngine;
using System.Collections;
public class Driver : MonoBehaviour {
// Specific Message Type
public class LevelStartMessage : Message
{
// Extra information not found in generic message class
private int m_LevelNum;
public int LevelNum
{
get { return m_LevelNum; }
set { m_LevelNum = value; }
}
// Param: id : Is the level number
public LevelStartMessage(int id)
{
// Set message type variable in parent class
MessageType = MessageType.LEVEL_START;
m_LevelNum = id;
}
}
private Message msg;
private LevelStartMessage levelStartMessage;
void Start ()
{
// This is a generic message type
// For generic messages we specify the message type in the constructer
// This is done because I often found a need to trigger a function, but the message
// to do this required no extra information, and thus was exactly the same as a
// generic message.
// Example:
// msg = new Message(MessageType.Pause);
// Pausing the game may require no information. We simply want to trigger the function
// to handle the pause mechanism. This way proves to be flexible, since we can
// use the same message variable and simply change the message type when needed to be
// use used as another message.
msg = new Message(MessageType.MESSAGE);
// This is a specific message type with extra attributes. Its message type is set internally
// in the constructor.
levelStartMessage = new LevelStartMessage(1);
// The nice thing is if the method listed does not exist, or has the wrong signature it results
// in a compile time error.
Communicator.Instance.RegisterListener(MessageType.MESSAGE, ActionHandleMessage);
Communicator.Instance.RegisterListener(MessageType.LEVEL_START, ActionHandleLevelStartMessage);
Communicator.Instance.Broadcast(msg);
Communicator.Instance.Broadcast(levelStartMessage);
}
// This will trigger when msg is broadcasted
public void ActionHandleMessage(Message msg)
{
Debug.Log("Generic message request received");
}
// This will trigger when levelStartMessage is broadcasted
public void ActionHandleLevelStartMessage(Message msg)
{
// Convert the generic message into a LevelStartMessage we are expecting
LevelStartMessage startMsg = msg as LevelStartMessage;
if(startMsg != null)
{
// Now we can access the specific attributes of the level start message.
Debug.Log("Level " + startMsg.LevelNum );
}
}
}
view raw Driver.cs hosted with ❤ by GitHub


The Communicator

This is the current version of the Communicator class used in the Cube Logic demo.



using UnityEngine;
using System.Collections;
/*******************************************************************************
* Singleton Class:
* All gameobjects register with 'Communicator' Enabling them receive messages
* from other objects and send messages to other objects.
* One object will handle all internal game messages ex updating player life,
* death ect... and external messages communication with the game structure ex
* menu toggling, game start, end, ect....
******************************************************************************/
public class Communicator : MonoBehaviour {
private static Communicator m_Instance;
private Hashtable m_Listeners;
private Hashtable m_BlockedMessages;
public delegate void EventHandler(Message msg);
private static event EventHandler handleMsgEvent;
public static Communicator Instance
{
get
{
if (m_Instance == null)
{
m_Instance = new GameObject("Communicator").AddComponent<Communicator>();
}
return m_Instance;
}
}
/*******************************************************************************
* Do: Associates eventhandler with a string message name
* How: Hashes eventhandler via messagename as key if same key exists then multi
* aggregate the eventhandler.
*
*******************************************************************************/
public void RegisterListener(MessageType name, EventHandler msgHandler)
{
bool found = m_Listeners.ContainsKey(name);
if(found)
{
handleMsgEvent = m_Listeners[name] as EventHandler;
handleMsgEvent+= msgHandler;
m_Listeners[name] = handleMsgEvent;
}
else if(!found)
{
m_Listeners.Add(name, msgHandler);
}
}
/*******************************************************************************
* Do: Unregister specfied handler.
* How: Get handler from hashtable via message name as key. Remove handler.
* Prereq: Handler must exist;
*******************************************************************************/
public void UnregisterListener(MessageType name, EventHandler msgHandler)
{
bool found = m_Listeners.ContainsKey(name);
if(found)
{
handleMsgEvent = m_Listeners[name] as EventHandler;
handleMsgEvent-= msgHandler;
m_Listeners[name] = handleMsgEvent;
}
}
/*******************************************************************************
* Do: Blocks certain types of messages from being transmitted.
* How: If a message type is not blocked it adds it to the block table
* Prereq: Must not already be blocked
*******************************************************************************/
public void BlockIncomingMessages(MessageType blockedMsgType)
{
if( !m_BlockedMessages.ContainsKey(blockedMsgType))
{
m_BlockedMessages.Add(blockedMsgType, false);
}
}
/*******************************************************************************
* Do: Unblocks certain types of messages so can be transmitted.
* How: If a message type is blocked, it is removed from the block table
* Prereq: Message type must have been previously blocked
*******************************************************************************/
public void UnblockIncomingMessages(MessageType unblockedMsgType)
{
if(m_BlockedMessages.ContainsKey(unblockedMsgType))
{
m_BlockedMessages.Remove(unblockedMsgType);
}
}
/*******************************************************************************
* Do: Mulitcast event to all listeners
* How: Get listener from hashtable via message name as key. Multicast event.
*******************************************************************************/
public void Broadcast(Message msg)
{
bool found = m_Listeners.ContainsKey(msg.MessageType);
bool isBlocked = m_BlockedMessages.ContainsKey(msg.MessageType);
if(found & !isBlocked)
{
handleMsgEvent = m_Listeners[msg.MessageType] as EventHandler;
handleMsgEvent(msg);
}
}
// Initialize this component
void Awake ()
{
enabled = false;
m_Listeners = new Hashtable();
m_BlockedMessages = new Hashtable();
}
// Cleanup
public void OnApplicationQuit ()
{
m_Listeners.Clear();
m_BlockedMessages.Clear();
m_Listeners = null;
m_BlockedMessages = null;
handleMsgEvent = null;
m_Instance = null;
}
}
view raw Communicator.cs hosted with ❤ by GitHub

References
Original messaging system design found at:

No comments:

Post a Comment