Warning
Warning: Unity Simulation is deprecated as of December 2023, and is no longer available.
Create a custom sensor
Now that you have learned about the out-of-the-box sensors available in the Unity Simulation Authoring tools, let's look at the basics of creating a new custom sensor. We'll walk you through all the steps using a simple example: a pose publisher.
1. Write the sensor code
Each sensor in SimulationSensors is packaged as a Unity Prefab. The logic for each sensor is implemented through a derivation of the Sensor
class. The package provides several "node" classes that can be used to perform sensor functionality, such as publishing data or collecting IMU observations.
Let's start by writing the logic for a sensor that takes a character string as an input parameter, finds the GameObject with the corresponding name, and creates a message containing the pose of the corresponding GameObject.
Create a new C# script and name the file GameObjectPoseNode.cs
. Paste the following code into the script.
GameObjectPoseNode.cs
/*
1. Import namespaces
Add the using directives for the sensor-related namespaces in order to access the APIs.
*/
using System;
using Unity.Simulation.Foundation.GeometryMessages;
using Unity.Simulation.Foundation;
using Unity.Simulation.Sensors;
using UnityEngine;
/*
2. Declare the sensor node
The node derives from SynchronizedSensorNode so it will be ticked by the Clock.
The [Serializable] attribute is added so that the node will show up in the inspector.
*/
[Serializable]
public class GameObjectPoseNode : SynchronizedSensorNode
{
/*
3. Add an input port
Declares an input port for the GameObject name string. It is a simple property
that is also exposed to the inspector with the [field: SerializeField] attribute..
*/
//------------ Input ports --------------
[field: SerializeField]
public string GameObjectName { get; set; }
/*
4. Add an output port
Declares an output port for the TransformMsg that is created after observing the specified
GameObject. It is a SensorPort of type TransformMsg, and a property.
*/
//------------ Output ports -------------
public SensorPort<TransformMsg> GameObjectPoseOutputPort { get; private set; } = new();
GameObject m_GameObject;
/*
5. Initialize the node
Subscribes the node to the Scheduler with SynchronizedSensorNode.Initialize(), and
Finds the game object with the name specified by the property GameObjectName.
*/
public override void Initialize(Sensor sensor)
{
base.Initialize(sensor);
m_GameObject = GameObject.Find(GameObjectName);
}
/*
6. Implement CollectObservations()
Function called on every update of the sensor by the Scheduler.
*/
public override void CollectObservations()
{
/*
7. Create the output message
Creates a TransformMsg and writes it to the output port m_MessagePort.
The To<FLU>() calls are to convert the position vector and rotation quaternion from
Unity`s coordinate system (Right-Up-Forward) to ROS's coordinate system (Forward-Left-Up).
*/
if (m_GameObject)
{
GameObjectPoseOutputPort.SetData(new TransformMsg
{
translation = m_GameObject.transform.position.To<FLU>(),
rotation = m_GameObject.transform.rotation.To<FLU>()
}, Clock.Now);
}
}
}
The GameObjectPoseNode
can now be implemented in a new Sensor. Create a new C# script and name the file GameObjectPoseSensor.cs
. Paste the following code into the script.
GameObjectPoseSensor.cs
/*
1. Import namespaces
Add the using directives for the sensor-related namespaces in order to access the APIs.
*/
using System;
using Unity.Simulation.Sensors;
using UnityEngine;
/*
2. Declare the sensor
The sensor derives from Sensor to provide it some base functionality, such as
InitializeSensor, InitializeProperties, and SetPropertyValue.
*/
public class GameObjectPoseSensor : Sensor
{
/*
3. Add node properties
The sensor needs the new GameObjectPostNode to collect and output observations. It also
needs the PublisherNode to publish the observations to a connection such as ROS.
*/
[SerializeField]
GameObjectPoseNode m_GameObjectPoseNode = new();
[SerializeField]
PublisherNode m_PublisherNode = new();
/*
4. Initialize the sensor
Initializes the nodes and subscribes the PublisherNode to the output port of the GameObjectPoseNode.
*/
public override void InitializeSensor()
{
m_GameObjectPoseNode.Initialize(this);
m_PublisherNode.Initialize(this);
m_GameObjectPoseNode.GameObjectPoseOutputPort.OnData += value =>
{
if (value != null)
{
m_PublisherNode?.Send(value);
}
};
}
/*
5. Initialize the properties
The sensors provided in SimulationSensors all have properties that can be set via the SetPropertyValue()
function. This allows you to set properties from the URDF Importer. Add the properties to the
m_Properties dictionary like so in order to access them from SetPropertyValue().
*/
protected override void InitializeProperties()
{
try
{
m_Properties.Add("sensor/topic", str => m_PublisherNode.Topic = str);
m_Properties.Add("sensor/update_rate", str => m_GameObjectPoseNode.UpdateRate = double.Parse(str));
m_Properties.Add("sensor/gameobject_name", str => m_GameObjectPoseNode.GameObjectName = str);
}
catch (Exception e)
{
Debug.Log($"Could not initialize properties for sensor {name}. Exception: {e}");
}
}
}
The property names follow the xpath syntax and determine how they can be specified in the robot's description file. For example the values for the properties above can be specified as:
<sensor>
<gameobject_name> OBJECT_NAME </gameobject_name>
<topic> TOPIC_NAME </topic>
<update_rate> UPDATE_RATE </update_rate>
</sensor>
2. Test the new sensor
You can now test your new sensor by setting it to publish the pose of a game object.
- Add a new GameObject to a scene, and add the
GameObjectPoseSensor
script to it. - Set its properties in the Inspector window:
Game Object Pose Node/Update Rate
to a number (e.g.1.0
)Game Object Pose Node/Game Object Name
to the name of any object in the scenePublisher Node/Topic
to a topic name to publish to (e.g./unity/objectpose
)
- In Unity Editor, click on the Play (
▶
) button to enter the Play mode. TheGameObjectPoseSensor
will send the pose of the specified game object to the/unity/objectpose
topic, which can be sent through a connection.