Warning
Warning: Unity Simulation is deprecated as of December 2023, and is no longer available.
DummyConnection
DummyConnection is a local, in-memory implementation of the IConnector interface. It demonstrates an implementation of IConnector and allows you to integrate an IConnector into a Unity project.
DummyConnectionComponent
The "Dummy Connection Component" can be attached to any Unity GameObject. The DummyConnectionComponent implements the abstract base class BaseConnectionComponent. As such, it exposes the GetConnector method, which will return a DummyConnection.
Subscribe
/// <summary>
/// Subscribe a callback for the specified topic
/// </summary>
/// <param name="topic">The specified topic</param>
/// <param name="callback">The subscriber callback Action to handle an IMessage</param>
/// <typeparam name="T">The type of the IMessage handled by the subscriber</typeparam>
public void Subscribe<T>(
string topic,
Action<T> callback) where T : IMessage;
The Subscribe method will register the specified callback for the specified topic. Whenever a message is received for the specified topic, the specified callback will be invoked with the new message. A message will be received when it is published by IPublisher Publish method. See Publish, below.
Publish
/// <summary>
/// Create or get an IPublisher for the specified topic and message type
/// </summary>
/// <param name="topic">The specified topic</param>
/// <typeparam name="T">The type of the IMessage</typeparam>
/// <returns>The IPublisher</returns>
public IPublisher RegisterPublisher<T>(
string topic) where T : IMessage;
The RegisterPublisher method will return an IPublisher interface for the specified topic. The user can then call the IPublisher Publish method to publish a message to that topic. Any subscribers to that topic will receive the new message. See Subscribe, above.
/// <summary>
/// Publish the specified IMessage to all subscribers
/// </summary>
/// <param name="msg">The IMessage to publish</param>
public void Publish(
IMessage msg);
ImplementService
/// <summary>
/// Create the service listener to handle requests
/// </summary>
/// <param name="serviceName">The service name</param>
/// <param name="callback">The callback to handle the request and return the response</param>
/// <typeparam name="TRequest">The type of the IMessage request</typeparam>
/// <typeparam name="TResponse">The type of the IMessage response</typeparam>
public void ImplementService<TRequest, TResponse>(
string serviceName,
Func<TRequest, TResponse> callback) where TRequest : IMessage where TResponse : IMessage;
The ImplementService method will register the specified callback for the specified service name. Whenever a request is received for the specified service name, the specified callback will be invoked with the new request. The specified callback should return an appropriate response. A request will be received when it is sent by IServiceClient SendRequest method. See SendRequest, below.
SendRequest
/// <summary>
/// Create the service client to send requests
/// </summary>
/// <param name="serviceName">The service name</param>
/// <typeparam name="TRequest">The type of the IMessage request</typeparam>
/// <typeparam name="TResponse">The type of the IMessage response</typeparam>
/// <returns>The IServiceClient</returns>
public IServiceClient RegisterServiceClient<TRequest, TResponse>(
string serviceName) where TRequest : IMessage where TResponse : IMessage;
The RegisterServiceClient method will return an IServiceClient interface for the specified service name. The user can then call the IServiceClient SendRequest method to send a request to that service and receive the response. See ImplementService, above.
/// <summary>
/// Send the request to the service and handle the response
/// </summary>
/// <param name="request">The TRequest message to send</param>
/// <param name="callback">The callback Action to handle the TResponse response</param>
/// <typeparam name="TRequest">The type of the IMessage request</typeparam>
/// <typeparam name="TResponse">The type of the IMessage response</typeparam>
public void SendRequest<TRequest, TResponse>(
TRequest request,
Action<TResponse> callback) where TRequest : IMessage where TResponse : IMessage;
This SendRequest method will send the request to the service. This SendRequest is a non-blocking, asynchronous method. The callback method will be invoked in the future when a response is received.
/// <summary>
/// Send the request to the service and return the Task to handle the response
/// </summary>
/// <param name="request">The TRequest message to send</param>
/// <typeparam name="TRequest">The type of the IMessage request</typeparam>
/// <typeparam name="TResponse">The type of the IMessage response</typeparam>
/// <returns>A Task<TResponse> that will complete when the response is received</returns>
public Task<TResponse> SendRequest<TRequest, TResponse>(
TRequest request) where TRequest : IMessage where TResponse : IMessage;
This SendRequest method will send the request to the service. This SendRequest is a non-blocking, asynchronous method. It will return a Task. The task will complete in the future when a response is received.
Example
Imagine we want to move a cube in a circle. We publish the position of the cube to an external component, and based on that position of the cube, the component publishes the new position of the cube on a circular path. Initially, we assume that we use an implementation of IConnector interface and BaseConnectionComponent that connects to an external component
Flow of Information using Dummy Connector
Flow of Information using an external component
To Implement the above example, we would have two scripts. One script would be called the publisher. This script will publish the position of the cube to the "external" component.
public class PosePublisher : AutoPublisher<TransformMessage>
{
[SerializeField]
GameObject m_Cube;
public override string DefaultTopic => "/current_cube_position";
public override TransformMessage CreateMessage() => new TransformMsg()
{
translation = new Vector3Msg(this.transform.position.x, this.transform.position.y, this.transform.position.z),
rotation = new QuaternionMsg(this.transform.rotation.x, this.transform.rotation.y, this.transform.rotation.z, this.transform.rotation.w)
}
}
Similarly we would have a another function receiving new position of the cube from the external component based on the messages published on the "/cube_circular_position" topic.
public class PoseSubscriber : SubscriberBehaviour<TransformMessage>
{
[SerializeField]
public override string DefaultTopic => "/cube_circular_position";
public override System.Action<TransformMessage> Callback => ChangeCubePosition;
ChangeCubePosition(IMessage msg)
{
var transfromMsg = (TransfromMsg) msg;
var translation = new Vector3((float)msg.translation.x,(float)msg.translation.y,(float)msg.translation.z);
var quartenion = new Vector3((float)msg.rotation.x,(float)msg.rotation.y,(float)msg.rotation.z,(float)msg.rotation.w);
this.transform.rotation = quartenion;
this.transform.translation = translation.
}
}
We have a MockTestScript that mimics the functionality of an external component. This script sends out a new position for the cube every update such that the cube follows a circular trajectory. After one circular trajectory is completed, it uses the current position of the cube to calculate the center of the new circle based on cube's current position.
public class MockTestScript : MonoBehaviour
{
BaseConnectionComponent connectionComponent;
IConnector connector;
IPublisher cubeCommandPublisher;
Vector3 cubePosition;
Quartenion cubeRotation;
static int angle;
static float a;
static float r;
static int axis;
bool reset = true;
void Start()
{
angle = 0f;
axis = 0;
cubePosition = new Vector3();
cubeRotation = new Quartenion();
var connector = ConnectorInjector.FindConnector(this.gameObject);
connector.RegisterSubscriber<TransformMsg>("/current_cube_position", CheckCubePosition);
cubeCommandPublisher = connector.RegisterPublisher<TransformMsg>("/cube_circular_position");
}
void Update()
{
if(reset)
SetCircleParameters();
var newCubePosition = new Vector3();
switch(axis%3):
{
case 0:
newCubePosition.y = a + ( r * Mathf.sin(angle * Mathf.Deg2Rad) );
newCubePosition.z = ( r * Mathf.cos(angle * Mathf.Deg2Rad) );
break;
case 1:
newCubePosition.x = a + ( r * Mathf.sin(angle * Mathf.Deg2Rad) );
newCubePosition.z = ( r * Mathf.cos(angle * Mathf.Deg2Rad) );
break;
case 2:
newCubePosition.x = a + ( r * Mathf.sin(angle * Mathf.Deg2Rad) );
newCubePosition.y = ( r * Mathf.cos(angle * Mathf.Deg2Rad) );
break;
}
var msg = new TransfromMsg();
msg.translation.x = newCubePosition.transform.position.x;
msg.translation.y = newCubePosition.transform.position.y;
msg.translation.z = newCubePosition.transform.position.z;
msg.rotation.x = cubeRotation.x;
msg.rotation.y = cubeRotation.y;
msg.rotation.z = cubeRotation.z;
msg.rotation.w = cubeRotation.w;
cubeCommandPublisher.Publish(msg);
angle++;
if(angle > 360)
{
angle = 0;
reset = true;
}
}
void SetCircleParameters()
{
a = cubsPosition[axis%3];
var circleCenter = new Vector3();
circleCenter[axis%3] = cubePosition[axis%3];
r = Vector3.Distance(cubePosition, circleCenter);
axis++;
reset = false;
}
}
void CheckCubePosition(IMessage msg)
{
cubePosition.x = (float)msg.translation.x;
cubePosition.y = (float)msg.translation.y;
cubePosition.z = (float)msg.translation.z;
cubeRotation.x = (float)msg.rotation.x;
cubeRotation.y = (float)msg.rotation.y;
cubeRotation.z = (float)msg.rotation.z;
cubeRotation.w = (float)msg.rotation.w;
}
}
Now to use an external component, we can replace the DummyConnectionComponent being previously used with BaseConnectionComponent. By adding the BaseConnectionComponent, we are moving the function of topic publishing and subscribing outside Unity.
First we will remove the DummyConnectionComponent from the project
And we replace it with an implementation of BaseConnectionComponent that communicates with an external component. In this case we are communicating with ROS, so we will add the RosEndpointConnector