Messages

Introduction

Objects and abilities interact with each other using events and requests. For everything to work, it is necessary to create an output pin (or request output pin) at the sending ability, and similarly an input pin (or request input pin) at the receiving ability. Every pin has its name, unique within the given ability. After that it is necessary to connect the input and output pins, from that moment vrecko will automatically send messages from given output to given input. The connections can be defined in an XML file or in the code while the program is running, where they can also be deleted.

Message types

There are two types of messages: event and request.

Event

A one-way event sent immediately if possible, recipient cannot answer.

Events can have different priorities:

  • Express (default) – executed immediately as a part of the send() method, i.e. after the method is finished, the message is already processed. There are exceptions to this rule, e.g. if the message is sent from a different thread than the main (which would have to be specially programmed by the programmer), the priority automatically changes from Express to Priority.
  • Priority – the message is executed in the current snapshot after processing already prepared messages.
  • Normal – the message is executed in the next snapshot.

Request

Request immediately sends a request to the target ability, which can respond to it. It basically means calling a method of the target ability, but encapsulated in inputs/outputs.

If EventDispatcher doesn’t have a connection with the request recipient, it returns NULL. It is also possible to define a standard request recipient with a given name, i.e. say that if any ability sends a request to its output named XYZ, it will be answered by a certain defined ability.

Input and output names

Every input and output has to have a name. Names, in conjunction with identification of an object and ability or device, allow us to address a given input from anywhere in the program or an XML configuration file.

Name examples

In the XML configuration files, in the <Event> elements you will typically see strings of the following type:

  • EO|2073#Ab|Modeller::ModellingTool#Grab
  • Wr#Ab|navigation::KMTracker#MouseButtonLeft
  • De|6#MouseButton

Individual name parts

The part before the first # denotes under which object (EO|number) or device (De|number) the ability is. It can also be inserted in a scene without an owner (Wr).

The second part before another # specifies the exact ability (in case of a device it has already been defines in the first part). It is Ab|ability_ID, where ID is by default in the form plugin_name::ability_name, but it is possible to change it to anything else. In XML configuration it can be written like this:

<Ability>
	<Name>ObjectEffect</Name>
	<PluginName>ObjectUtils</PluginName>
	<ID>Effect1</ID>
	...
</Ability>

The last part is the input or output name.

Message content – data

Every message is a child of the VreckoMessage class, which is a child of osg::Referenced, so it should not be saved in variables of the VreckoMessage* type (or e.g. MessageVec3*), but in osg::ref_ptr<VreckoMessage> variables (or e.g. osg::ref_ptr<MessageVec3>). Management is much simpler and it’s not necessary to worry about e.g. memory deallocation, which is resolved automatically.

Reference pointers are an important part of OpenSceneGraph, so it is recommended to read an appropriate tutorial such as the one in OSG Quick Start Guide (external link).

The basic types for common messages are available in vrecko/Message.h, including e.g.

  • MessageBool
  • MessageInt
  • MessageFloat
  • MessageObjectID
  • MessageString
  • MessageVec3
  • MessageQuat
  • and MessageMatrix

All these messages include a variable named “data”, which is of the given type.

Defining your own message type is not a problem – it can be inherited from VreckoMessage as described in the commentary included in the header file of the class.

When creating a message with only one data variable inside (it can be a simple type, but also a class or a structure), there are two macros available to simplify the task. VRECKO_START_MESSAGE_EXTENDED and VRECKO_END_MESSAGE allow the definition of a simple message type with a “data” variable. For example, the complete definition of the MessageInt message looks like this:

VRECKO_START_MESSAGE_EXTENDED(MessageInt, int)
    virtual bool setValueFromString(const char *valueAsString) {
          data = atoi(valueAsString);
          return true;
    };
VRECKO_END_MESSAGE

An example of a message derived directly from VreckoMessage (without macros) could be MessageSensorTransformation, which includes the sensor number and its transformation (position and orientation in space. This message type is used e.g. in the Optitrack input device.

Preparation

Step 1 – Sender side

The ability must define its outputs. The following way is strongly recommended.

Say we have a new ability with the MyAbility class, which is a child of Ability.

At the end of class declaration, in the header file, we add a line with the DECLARE_OUTPUT macro (or DECLARE_REQUEST_OUTPUT) for every output. This will create an output pin with the given name, beginning with “output” (or “reqOutput”), e.g.

class MyAbility : public Ability {
    ...
    DECLARE_OUTPUT(Position, MessageVec3)
};

will define an output pin named “Position”, allowing us to send a message of the MessageVec3 type, represented by the outputPosition variable. For the request output it is necessary to add a return type as well, e.g.:

DECLARE_REQUEST_OUTPUT(SelectionByPointerPos, MessageVec3, MessageObjectID);

That is sadly not enough for operation, it is also necessary to copy both these lines to the constructor with a single modification – replace DECLARE by INIT.

MyAbility::MyAbility : Ability(...) {
    ...
    INIT_OUTPUT(Position, MessageVec3)
    INIT_REQUEST_OUTPUT(SelectionByPointerPos, MessageVec3, MessageObjectID);
}

Step 2 – Receiver side

It is very similar to sender side but input macros are used, e.g.:

DECLARE_INPUT(Position, MessageVec3)
DECLARE_REQUEST_INPUT(SelectionByPointerPos, MessageVec3, MessageObjectID);

In addition to variable declaration, these macros also declare the methods onMessagePosition() and onRequestPosition(), which will process the incoming data.

Again, both these lines are copied to the constructor with DECLARE replaced by INIT, and an addition in form of parameter with your class name, i.e.:

INIT_INPUT(Position, MessageVec3, MyAbility)
INIT_REQUEST_INPUT(SelectionByPointerPos, MessageVec3, MessageObjectID, MyAbility);

It is also necessary to create the method body for processing events, for which there also are macros, but this time with INIT replaced by METHOD. Within methods it is possible to use a message variable with the incoming message, which in case of simple types contains the data variable, so e.g.:

METHOD_INPUT(Position, MessageVec3, MyAbility)
{
   if (message->data.x() == 0) { // Is the vector's X-coordinate equal to zero?
     ...
   }
}

METHOD_REQUEST_INPUT(SelectionByPointerPos, MessageVec3, MessageObjectID, MyAbility)
{
    return new MessageObjectID(getSelectedObjectByPosition(message->data));
}

Step 3 – Ability interconnection

vrecko has to know from which output the event/request has to be sent to which input, thus the connection has to be defined. That can be done either in an XML configuration file or during the application’s runtime.

XML file

A lot of examples are contained in the .xml files distributed with vrecko, here are two typical cases:

<Event InterconnectionType="FORWARD_OUTPUT">
  <Sender>De|6#MousePosition</Sender>
  <Receiver>Wr#Ab|navigation::KMTracker#Mouse</Receiver>
</Event>
<Event InterconnectionType="ACTIVATE_INPUT">
  <Sender>De|6#MouseButton|MiddleButton</Sender>
  <Receiver>Wr#Ab|navigation::KMTracker#MouseButtonMiddle|1</Receiver>
</Event>

Creating connections while the program is running

Connections can be created or deleted while the program is running, using the methods EventDispatcher::insertEventInterconnection() and EventDispatcher::deleteEventInterconnection().

Sending messages

With DECLARE_OUTPUT(PinName, ...), or DECLARE_REQUEST_OUTPUT(PinName, …) we created the variables outputPinName, or reqOutputPinName. They are classes that will simplify the message sending process.

When sending messages a new instance of the message must be created (e.g. “new MessageInt”), filled with data and sent by calling the send() or request() method. It is often possible to put everything in a single line, e.g.:

outputNazevPinu->send(new MessageInt(10));

With requests a return message is expected, which should be saved in ref_ptr. If for example a MessageInt is sent and MessageFloat is returned, the script looks like this:

osg::ref_ptr<MessageFloat> retMsg = outputNazevPinu->request(new MessageInt(10));

Caution: Always save the return value in osg::ref_ptr<...>, not in a “regular” pointer. The following call:

MessageFloat *retMsg = outputNazevPinu->request(new MessageInt(10));
// CAUTION - this is WRONG on purpose

can be compiled and everything will work at the program’s launch time. However, a wrong value will be returned during the debugging process.

Explanation: What happens is that ref_ptr will be deleted, which in most cases deallocates the memory it’s pointing at. Your pointer will thus point to deleted memory, which may or may not contain correct data. Visual Studio tries to uncover these mistakes while debugging and fills the memory with its own numbers at deallocation, so the value will almost certainly be incorrect. In the release mode the memory is not rewritten, so it will often contain the old data even after deallocation (by chance) and you will not be able to discover the mistake (only the program will sometimes mysteriously crash).

Finally, thanks to the new C++ standard called C++11 (which is partially supported by the C++ compiler in MS Visual Studio 2010), the script can be shortened in the following way:

auto retMsg = outputNazevPinu->request(new MessageInt(10));