-
Notifications
You must be signed in to change notification settings - Fork 21
Controlling Objects in Unity with AMQP
This tutorial demonstrates how to use the library to control the Position, Rotation, and Scale of an object in the scene using AMQP messages.
You will need to have Unity 3D installed.
Also install the Unity3D.Amqp asset package into your project. For more instructions on how to get the asset package and import it into a new project please see the Quick Start tutorial.
You will need access to a RabbitMQ server for this tutorial. This tutorial assumes a local install of RabbitMQ server, but you could also use a hosted version such as CloudAMQP (who offer a free tier). If you are going to use a hosted service like CloudAMQP then please refer to the README as it explains some of the differences in port configuration and SSL usage that will differ from this tutorial.
- Getting Started
- Configuring AMQP Connections
- Configuring the Object for Control
- Understanding the Message Format
- Playing the Scene & Updating the Cube
- Going Further
- Efficiently Controlling Lists of Objects
- Don't Forget to Turn Off Debug Logging!
Once you have imported the package into your project, locate the example scene ObjectControlDemo:
Before you can use the AMQP library you will need to ensure at least one connection has been written to your project.
From Unity's file menu, select: AMQP → Configuration
This tutorial will assume you have the default install of RabbitMQ server installed on your localhost with the default guest/guest admin user.
The first time opening the AMQP Configuration window should create the AmqpConfiguration.json file in the /Assets/Resources folder of your project. To be safe you can press the Save button.
Now in the scene locate the AmqpClient object and script and select it in the Hierarchy window in the scene:
Make sure that in the Connection drop down in Unity's inspector that the AMQP connection that you would like to use is selected.
Locate the Cube in the scene and select it. Locate the AmqpObjectController script. This script is an example script that shows how to listen for incoming messages on a particular AMQP exchange and to apply the Position
, Rotation
, and Scale
values received in the messages to the Cube's transform.
Here is a brief explanation of the script's inspector properties:
- Id Filter - An optional ID filter that can be used to make sure this object instance only responds to message with a matching ID. Keep in mind this object will still receive messages, but ignore them if its own ID does not match the ID in the received message. For more efficient message routing strategies consider using routing keys instead.
- Exchange Name - The name of the exchange to subscribe to that messages will be received from; the default is 'amq.topic' which all RabbitMQ servers have by default
- Exchange Type - The type of the exchange being subscribed to; keep in mind it is critical to get the correct exchange type as the RabbitMQ client will drop the connection when the incorrect type is passed (for example 'fanout' when the expected was 'topic')
- Routing Key - The optional routing key to use; since this demo uses the default topic exchange 'amq.topic' a routing key is added so that this script will only receive messages intended for the demo and not other messages being published to the 'amq.topic' exchange on the server - this also means published messages will have to include the matching routing key
- Update Position - Whether or not the script will update the object's position based on incoming position data; when disabled, position data is ignored
- Update Rotation - Whether or not the script will update the object's rotation based on incoming rotation data; when disabled, rotation data is ignored
- Update Scale - Whether or not the script will update the object's scale based on incoming rotation data; when disabled, scale data is ignored
- Update In World Space - When enabled, position and rotation will be updated in world space; when disabled, position and rotation will be updated in local space
- Debug Log Messages - When enabled, received messages will be logged to Unity's debug console
You should open up the AmqpObjectController.cs script and familiarize yourself with how it works to see how you can expand upon this technique.
In this example we will be using a custom JSON message structure that is expected by the AmqpObjectController script.
JSON Message Structure
{ "id":"optional ID filter", "posX": 0, "posY":0, "posZ": 0, "rotX":0, "rotY":0, "rotZ":0, "sclX":1, "sclY":1, "sclZ":1 }
Note: Since AMQP does not specify anything about the message's body/content itself, you could use whatever message format you would like (binary, ASCII, etc.), however you will need to implement a custom parser in the Unity script that will be receiving and making use of the message.
All properties in the JSON message are optional; you can just include the ones you need. The id
property is meant to match the AmqpObjectController.IdFilter
property. By making the ID values match it is possible to have many instances of AmqpObjectController in the scene each only listening for their own messages. This would be more efficiently achieved using routing keys and avoiding the IdFilter
property entirely, but for this example it is kept simple.
Once everything is configured play the scene in Unity. Watch the debug console for useful output about whether or not the AmqpClient has successfully connected to the AMQP server and subscribed to listen for messages.
Once you have successfully established a connection to the server you can now send messages to the exchange to control the object. There are many ways to publish messages to the AMQP server, but if you are using RabbitMQ, one simple way is to publish a message directly from RabbitMQ's management interface. If you have installed RabbitMQ locally and the management plugin is enabled then you can access it using: http://localhost:15672.
Assuming you are using the defaults of this tutorial then browse to the Exchanges section and select amq.topic. To publish a message using the management interface, scroll down to the Publish message section.
Note: If you are using a routing key in the Unity scene (default amqpdemo.objects), make sure you also include the same routing key when publishing messages.
In the Payload field, enter a JSON message to update the object. Let's start with just Y rotation for now. The message is as follows:
{ "rotY":45 }
Once you have entered the routing key and payload, press the Publish message button. If the message was successfully routed you will see a green confirmation. If it was not successfully routed you will see a different color.
If your message was successfully published then go back to the Unity scene and check to see that the Cube received the update.
The Cube in your scene should now have its Y rotation value set to 45. You may want to enable the Debug Log Messages option on the AmqpObjectController script to aid in verifying that messages are being received.
If everything has worked correctly, congratulations, you are now controlling Unity from AMQP messages!
If you would like to try a message that updates all properties, try using the following message:
{ "posX": -2, "posY":1, "posZ": -1, "rotX":15, "rotY":45, "rotZ":35, "sclX":0.5, "sclY":1.5, "sclZ":1.75 }
You should see all of the matching properties of the Cube updated in Unity:
You can also try setting the Id Filter property and setting a matching id
property in the JSON message:
{ "id":"myObject1", "posX": 2, "posY":-1, "posZ": 1 }
In Unity, set the Id Filter property of the AmqpObjectController to myObject1
in the inspector. Messages that have the id
property set to myObject1
should be received by the Cube. However messages that do not have the id
property or a value different than myObject1
should not be received by the Cube.
The AmqpObjectController is sort of an all-in-one solution to controlling objects in the scene via AMQP. It talks to the AmqpClient script and takes care of its own subscriptions and message handling. It ends up being a bit simpler of an implementation and easy to understand.
As a consequence, it is not a very scalable or efficient solution for controlling many objects in the scene. If for each object to control over AMQP you put a copy of the AmqpObjectController, they would subscribe individually; one subscription per object. At the very least there is a time involved while you wait for each subscription to be created between the AMQP client and server. Ideally the faster you can be subscribed and start receiving messages, the better. Depending on how you were to configure things it is even possible to send an awful lot of redundant, wasted messages.
There is a slightly more complicated solution that will scale much better when controlling many objects. An implementation of some of these designs can be found in the AmqpObjectListController example script. There is another demo scene included which shows efficient object list control. Find and open the ObjectListControlDemo scene.
In the scene locate and click on the object named AmqpObjectListController. There will be a script by the same name on it. In this case this script acts similar to AmqpObjectController in that it subscribes to the AMQP server and handles messages received from it. However rather than doing anything with the messages itself, it finds the matching object registered by its message ID and routes the message to it individually.
In the demo scene there are 3 registered objects: Cube1, Cube2, and Cube3 (with these AMQP IDs respectively: cube1, cube2, cube3). Select one of the cubes in the scene and look for the AmqpObjectControlReference script. This script provides 2 very simple functions to this design. First, it allows you to enter the object's AMQP ID that it will be connected to. Second, it self-registers with the AmqpObjectListController script on start. This connects the Unity game object, and its AMQP message ID, to the list controller that is handling received messages. An internal look-up table efficiently matches incoming IDs to game objects and updates them all from a single subscription.
Play the scene and in the Debug Console you should see the registration message for each one of the cubes as they connect to the AmqpObjectListController.
If everything looks like it is running correctly go back into your RabbitMQ management console and get ready to send new update messages to the scene. For now, try sending a message that just targets one of the cubes.
{ "id":"cube2", "rotY":45 }
Remember to include the correct routing key. Publish the message.
Go back to Unity and verify that the message was received by the correct cube, in this case Cube2 in the scene.
With the implementation so far we've made the design more efficient by using a single AMQP subscription and an in-memory ID-to-Unity-object mapping dispatcher for updating objects (AmqpObjectListController's internal implementation).
This is a huge improvement, but we are still only processing individual messages. What if we had a bunch of object states that we wanted to set all at once? We could send each object update in a single message, one after there other, as fast as possible. That will still probably be pretty fast; possibly even appearing instantaneous in Unity depending on the number of messages and objects. But we can do better.
First lets consider the penalties we pay when we send a single AMQP message in general and in this setup:
- The custom data we send in a message is wrapped in many more bytes. The AMQP protcol has a "frame" around our data that includes protocol bytes including the AMQP message header, exchange, routing key, metadata, etc. Think of this framing as a size and processing penalty.
- Each message is wrapped in a TCP packet which further adds a bit more of a size and processing penalty.
- AmqpClient must queue each message it decodes. In its Update() loop it safely dequeues the messages and dispatches them, calling subscription handlers to process the messages.
These penalties all waste bandwidth and processing time/CPU cycles.
Now think about it as a ratio of cost paid: (time|memory|bandwidth) / message sent. You can scale the system better if you can reduce the cost you pay per message sent.
By packing more than 1 JSON update message in a single AMQP message we improve the cost per message to send OUR messages over AMQP and the network. If we send 10 messages instead of 1 we pay 1/10th the cost for each one of our messages.
These savings aren't infinite, but they do stack up at scale (lots of messages, very fast time scales, or both).
The AmqpObjectListController supports batch messaging by checking to see if the incoming message is a JSON array or a single message object. If an array is detected it is deserialized as a list of object update messages and each will be processed at once. If an object is detected it is assumed to be a single update message for a single object.
Try publishing a message that will update all 3 cubes in the scene at once:
[{ "id":"cube1", "rotX":45 }, { "id":"cube2", "rotY":45 }, { "id":"cube3", "rotZ":45 }]
In Unity you should see a single batch message received in the Debug Console and all 3 cubes should have their rotations changed, each on a different axis.
This technique will allow you to control a larger list of objects in Unity much more efficiently. It will have faster start up times with a single subscription versus a subscription per object. Large lists of updates happening to many objects at once will happen much faster. In cases where having grouped updates all occur on the same frame is necessary, batching messages into a JSON array can maximize processing speed and efficiency. When there are too many messages in a single batch to be processed per frame, the one large batch can be split into smaller batches and individual messages spread out over multiple frames.
When you are happy with your testing, don't forget to turn off the Debug Log Messages inspector property on all instances of AmqpObjectController and AmqpObjectListController scripts. If you start sending large numbers of messages very quickly logging all of it to the Debug Console can slow performance dramatically, sometimes causing Unity to hang. This property is only intended to be used for troubleshooting and debugging!