Skip to the main content.

Did you know?

 

RTI is the world’s largest DDS supplier and Connext is the most trusted software framework for critical systems.

Success-Plan-Services-DSSuccess-Plan Services

Our Professional Services and Customer Success teams bring extensive experience to train, problem-solve, mentor, and accelerate customer success.

Learn more

Developers

From downloads to Hello World, we've got you covered. Find all of the tutorials, documentation, peer conversations and inspiration you need to get started using Connext today.

Try the Connectivity Selection Tool ⇢

Resources

RTI provides a broad range of technical and high-level resources designed to assist in understanding industry applications, the RTI Connext product line and its underlying data-centric technology.

Company

RTI is the infrastructure software company for smart-world systems. The company’s RTI Connext product is the world's leading software framework for intelligent distributed systems.

Contact Us

News & Events
Cooperation

MODULE 10:
Immersive Environment Simulation: Subscribing at Runtime

⇠ Back to All Modules

Case-Code-hero

Subscribing to Instances at Runtime

So far, all the Unreal Engine game objects have been static. That means I had to place them in the viewport before running it. When it runs, it will automatically subscribe and publish. However, the Shapes application can create shape publishers and delete them at runtime. Creating a subscriber in the Shapes application doesn’t automatically have a shape moving. It only appears and starts moving when a new instance of a shape is discovered. In this module we will implement similar functionality in the Unreal Engine. What this means is that the object needs to be created when a new instance is detected and destroy the instance when it is disposed of.

We need some kind of a manager which listens to the DDS traffic and then spawns the object when a new instance is detected.  The Unreal Engine Info class which is a subclass of an Actor like the Pawn class we have been using. 

Before we create the Info class we need a Shape class we can spawn. The class is similar to the previously created ShapeSub C++ class however we don’t need to create all the DDS instances since those will be created in the Game Mode class and passed to the Shapes class. Let’s create a new C++ Pawn class and call it ShapeSubDynamic.

This will open up the class in Visual Studio. Let’s first look at the header file. As for the static subscriber, add the include at the top of the file

#pragma warning(disable:4668)
#pragma warning(disable:4530)

#include <dds/dds.hpp>

Next add some UProperties. We need properties for the static mesh as well as for the different colors. A field for each predefined color in Shapes is added and a default. If the color value doesn’t match any of the colors the default color is selected.

public:
/* Mesh component */
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "MeshComponent")
UStaticMeshComponent* StaticMesh;



/* Colors */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Color")
UMaterial* Purple;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Color")
UMaterial* Blue;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Color")
UMaterial* Red;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Color")
UMaterial* Green;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Color")
UMaterial* Yellow;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Color")
UMaterial* Cyan;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Color")
UMaterial* Magenta;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Color")
UMaterial* Orange;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Color")
UMaterial* Default;

In the previous subscriber example we used the color value to create a query condition and just read the specific color. We could do the same here however the color is defined as key in the DDS data type definition. Key means that a different value for this field DDS will interpret as a different instance which means that instead of the actual color we can use the instance id. This is a much more generic approach since different instances of the same type would normally map to a different instance of the same game object. This means that for each instance we need to know the reader for the topic (data type) and the instance id. In the header file we create a placeholder for those variables and initialize it to null as follows.

private:
   dds::sub::DataReader<dds::core::xtypes::DynamicData> Reader = 
       dds::core::null;
   dds::core::InstanceHandle Instance = dds::core::null;

Lastly we also need an initialize function the Game Mode class can call to initialize the newly spawned object. Let’s add the prototype for the init function here. What is passed is the reader to use, the instance handle, and the color. The color isn’t absolutely necessary but it helps in resetting the color for the object. Another option would be to query for the key value (color) based on the reader and instance id. The prototype is:

void Initialize(
   dds::sub::DataReader<dds::core::xtypes::DynamicData> myReader, 
   dds::core::InstanceHandle myInstance, 
   FString myColor);

Now that we have everything we need defined, let’s look at the ShapeSubDynamic.cpp file. 

As for the static subscriber we need to set the static mesh in the constructor. AShapeSubDynamic::AShapeSubDynamic() and set the root component.

StaticMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("CustomStaticMesh"));
RootComponent = StaticMesh;

Since the DDS entities will be created in the Game Mode class we don’t have to do anything special for the beginPlay() method. However we need an initialize function which can be added at the end of the file. The initialize function will set the color of our instance and the private variables.

void AShapeSubDynamic::Initialize(
   dds::sub::DataReader<dds::core::xtypes::DynamicData> myReader,  
   dds::core::InstanceHandle myInstance, 
   FString myColor)
{
  Reader = myReader;
  Instance = myInstance;


   if (myColor == "PURPLE") StaticMesh->SetMaterial(0, Purple);
  else if (myColor == "BLUE") StaticMesh->SetMaterial(0, Blue);
  else if (myColor == "RED") StaticMesh->SetMaterial(0, Red);
  else if (myColor == "GREEN") StaticMesh->SetMaterial(0, Green);
  else if (myColor == "YELLOW") StaticMesh->SetMaterial(0, Yellow);
  else if (myColor == "CYAN") StaticMesh->SetMaterial(0, Cyan);
  else if (myColor == "MAGENTA") StaticMesh->SetMaterial(0, Magenta);
  else if (myColor == "ORANGE") StaticMesh->SetMaterial(0, Orange);
  else StaticMesh->SetMaterial(0, Default);
}

The tick function is where the shape position is updated. Once again this is similar to the previous subscriber class we had with the exception that we only read for a particular instance, and if the instance has been disposed of, it gets destroyed. If the instance state is disposed of or not alive, the object is destroyed. The code for the Tick(float DeltaTime) is:

/* Only read if the reader has been initialized */
if (Reader != dds::core::null)
{
   rti::sub::LoanedSamples<dds::core::xtypes::DynamicData> samples = 
       Reader.select().instance(Instance).take();


    /* Process each sample which is valid */
   for (const auto& sample : samples)
   {
       if (sample->info().valid())
       {
            /* Read the values we are interested (X and Y) from
               the dynamic data */
            int32 x = sample->data().value<int32>("x");
            int32 y = sample->data().value<int32>("y");
            /* Set the location. We want the shape to move
               horizontal and  vertical */
            FVector Location(0, 250 - x, 250 - y);
            SetActorLocation(Location);
       }
       bool die = false;
       /* Get the instance state */
       const dds::sub::status::InstanceState& state =
           sample.info().state().instance_state();
       if (state == 
           dds::sub::status::InstanceState::not_alive_disposed())
       {
           die = true;
       }
       if (state ==
           dds::sub::status::InstanceState::not_alive_no_writers()) 
       {
           die = true;
       }
       if (state ==
           dds::sub::status::InstanceState::not_alive_mask()) 
       {
           die = true;
       }
       if (die)
       {
           Destroy();
           Reader = dds::core::null;
           Instance = dds::core::null;
}
   }
}

Now that we have a class which can be instantiated it is time to create the manager class. The manager listens to the DDS traffic and then spawns the object when a new instance is detected.  As mentioned above for this we use the Unreal Engine Info class. Info is a subclass of Actor the same as the Pawn class we have been however the info class does not have a physical representation in the world. The Info class is used for manager type classes. Create a new C++ class and select Info (Actor subclass).

We will give it a name, ShapesSubscriberManager.

This will create two files in Visual Studio ShapesSubscriberManager.h and ShapesSubscriberManager.cpp. In the header file, we need to include the definition of our dynamic Shape subscriber class.

#include "ShapeSubDynamic.h"

As usual, we need a few UProperties. Here we need the classes for the different shapes (object) the game mode controller can spawn and the domain id.

public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Shape Subscriber")
TSubclassOf<class AShapeSubDynamic> SquareSub;


UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Shape Subscriber")
TSubclassOf<class AShapeSubDynamic> CircleSub;


UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Shape Subscriber")
TSubclassOf<class AShapeSubDynamic> TriangleSub;


/* Domain ID */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Connext")
int32 DomainID = 0;

As far as private members, we need the strings for the QoS ad type definition as well as readers for the different topics

private:
FString QOS_URL = FString("Connext/Unreal_Shapes.xml");
FString TYPE_NAME = FString("ShapeType");
dds::sub::DataReader<dds::core::xtypes::DynamicData> SquareReader =
   dds::core::null;
dds::sub::DataReader<dds::core::xtypes::DynamicData> TriangleReader =
   dds::core::null;
dds::sub::DataReader<dds::core::xtypes::DynamicData> CircleReader =
   dds::core::null;

In the Game Mode class, we need to override the BeginPlay method where we create all the DDS instances and the Tick function where we check if new objects have been detected. We also need a constructor. The following prototypes need to be added to the header file:

public:
AShapesSubscriberManager();


protected:
virtual void BeginPlay() override;


virtual void Tick(float DeltaTime) override;

Now, the implementation of the ShapesSubscriberManager class. In the constructor, we need to set the bCanEverTick to true so the Tick function is called every frame. The constructor is:

AShapesSubscriberManager::AShapesSubscriberManager()
{
// Set this class to call Tick() every frame.
PrimaryActorTick.bCanEverTick = true;


}

The BeginPlay is where the DDS entities are being created. To keep the example simple I am not checking if the entities already exist and just create the entities. If you use multiple managers I suggest that you reuse the domain participant and if you use the same topics reuse the topic and reader. 

void AShapesSubscriberManager::BeginPlay()
{
 Super::BeginPlay();


  /* Construct the fully qualified name for the configuration 
    file (XML) location */
 FString xmlFile = FPaths::Combine(FPaths::ProjectContentDir(), QOS_URL);
 /* Read the configuration file and set the defaults*/
 rti::core::QosProviderParams provider_name;
 provider_name.url_profile({ TCHAR_TO_UTF8(*xmlFile) });
 dds::core::QosProvider::Default().extensions().default_provider_params(provider_name);


  /* Initialize the dynamic data type */
 const dds::core::xtypes::DynamicType& myType = dds::core::QosProvider::Default().extensions().type(TCHAR_TO_UTF8(*TYPE_NAME));


  /* Create a domain participant */
 dds::domain::DomainParticipant participant = dds::domain::DomainParticipant(DomainID);


  /* Get a reference to the implicit subscriber */
 dds::sub::Subscriber subscriber = rti::sub::implicit_subscriber(participant);


  /* Create the topic with the configured name for the participant 
    and dynamic type */
 auto SquareTopic   = dds::topic::Topic<dds::core::xtypes::DynamicData>(participant, "Square", myType);
 auto CircleTopic   = dds::topic::Topic<dds::core::xtypes::DynamicData>(participant, "Circle", myType);
 auto TriangleTopic = dds::topic::Topic<dds::core::xtypes::DynamicData>(participant, "Triangle", myType);


  SquareReader = dds::sub::DataReader<dds::core::xtypes::DynamicData>(subscriber, SquareTopic);
 CircleReader = dds::sub::DataReader<dds::core::xtypes::DynamicData>(subscriber, CircleTopic);
 TriangleReader = dds::sub::DataReader<dds::core::xtypes::DynamicData>(subscriber, TriangleTopic);
}

Finally, we get to the point where we spawn the objects, the Tick() method. Here we read the data for the 3 topics and see if a new instance has been discovered. We want to use the read function and not the take function as we don’t want to remove the sample from the receive queue. If we leave it in the receive queue, the newly spawned will immediately take it from the queue and process it. It doesn’t make much of a difference as the samples come at a reasonably fast rate, but with using read we can make sure that the first sample does not get lost. 

Also we are not interested in every single sample, we only need samples when a new instance has been discovered. Therefore we set a filter on the read with the instance state new. Here is the Tick function: 

 void AShapesSubscriberManager::Tick(float DeltaTime)
{
 Super::Tick(DeltaTime);


  UWorld* World = GetWorld();
 FActorSpawnParameters SpawnParams;
 SpawnParams.bNoFail = TRUE;


  if (SquareReader != dds::core::null)
 {
   /* Read only sample which indicate a new Square instance */
   rti::sub::LoanedSamples<dds::core::xtypes::DynamicData>  
samples = SquareReader.select().state(dds::sub::status::DataState::new_instance()).read();
   /* Process each sample which is valid */
   for (const auto& sample : samples)
   {
     if (sample->info().valid())
     {
       dds::sub::status::ViewState view_state = sample.info().state().view_state();
       if (view_state == dds::sub::status::ViewState::new_view()) 
       {
         FString color(sample->data().value<std::string>("color").c_str());
         const FTransform SpawnLocAndRotation;
         AActor* a = World->SpawnActorDeferred<AActor>(SquareSub, SpawnLocAndRotation);
         AShapeSubDynamic* s = Cast<AShapeSubDynamic>(a);
         s->Initialize(SquareReader, sample->info().instance_handle(), color);
         s->FinishSpawning(SpawnLocAndRotation);
       }
     }
   }
 }


  if (CircleReader != dds::core::null)
 {
   /* Read only sample which indicate a new Circle instance */
   rti::sub::LoanedSamples<dds::core::xtypes::DynamicData> samples = CircleReader.select().state(dds::sub::status::DataState::new_instance()).read();
   /* Process each sample which is valid */
   for (const auto& sample : samples)
   {
     if (sample->info().valid())
     {
       dds::sub::status::ViewState view_state = sample.info().state().view_state();
       if (view_state == dds::sub::status::ViewState::new_view()) 
       {
         FString color(sample->data().value<std::string>("color").c_str());
         const FTransform SpawnLocAndRotation;
         AActor* a = World->SpawnActorDeferred<AActor>(CircleSub, SpawnLocAndRotation);
         AShapeSubDynamic* s = Cast<AShapeSubDynamic>(a);
         s->Initialize(CircleReader, sample->info().instance_handle(), color);
         s->FinishSpawning(SpawnLocAndRotation);
       

        }
     }
   }
 }


  if (TriangleReader != dds::core::null)
 {
   /* Read only sample which indicate a new Triangle instance */
   rti::sub::LoanedSamples<dds::core::xtypes::DynamicData>samples = TriangleReader.select().state(dds::sub::status::DataState::new_instance()).read();
   /* Process each sample which is valid */
   for (const auto& sample : samples)
   {
     if (sample->info().valid())
     {
       dds::sub::status::ViewState view_state = sample.info().state().view_state();
       if (view_state == dds::sub::status::ViewState::new_view()) 
       {
         FString color(sample->data().value<std::string>("color").c_str());
         const FTransform SpawnLocAndRotation;
         AActor* a = World->SpawnActorDeferred<AActor>(TriangleSub, SpawnLocAndRotation);
         AShapeSubDynamic* s = Cast<AShapeSubDynamic>(a);
         s->Initialize(TriangleReader, sample->info().instance_handle(), color);
         s->FinishSpawning(SpawnLocAndRotation);
        

        }
     }
   }
 }
}

Now that we have the C++ classes implemented, we can go back to the Unreal Engine editor. First we create our dynamic subscriber blueprint classes for each of the Shape types. Right click on the ShapeTypeDynamic class in the content browser and select “Create Blueprint class based on ShapeSubDynamic”. 

Let’s call it SquareSubDynamic.

This will open the blueprint editor for the newly created SquareSubDynamic class. In the components window, select the “Static Mesh” and in the details windows, set the mesh to a cube. As in the previous example scale it down to 30% (0.3 of scale).

Then click on SquareSubDynamic(self) in the components view and in the components view, you will see the placeholder for all the different colors. You can assign materials to the different color definitions. The starter content has some existing materials you can use (e.g. Red, Green, Blue), which we used in the previous example. However, you can also create your own materials and if you implement all the colors the Shapes application supports, you need to add some of them. The provided project has them already defined or you can create them yourself. A simple way to create a color for a shape is to create a folder (e.g. Colors) in your content directory. Right click on the content view window and select Material.

Give the Material a name -For example Yellow.

Double click on it to open the properties.

Right click on the canvas and select constant3Vector to create a 3 vector. You can also hold the number 3 on the keyboard and right click, which will automatically insert a vector.

Double click on the black box inside the 3 vector which will bring up the color picker. Enter the color you like by adjusting the sliders or enter the numbers for the color values. Once you have the desired color, click OK.

Now all we have to do is hook up the output of the vector with the color input of our material.

Now create the other colors you want to use the same way. Once you have all the colors created, double click on the SquareSubDynamic to open up the editor and set the colors.

In my example I selected black as the default but feel free to use what you like.

Compile and save. 

Now create a class for CircleSubDynamic and TriangleSubDynamic. The only difference will be the Mesh component. For circles, select a sphere and for Triangle, a cone. Don’t forget to set the colors.

You should have the 3 classes SquareSubDynamic, CircleSubDynamic, and TriangleSubDynamic  in your content folder. 

We are almost ready to run our dynamic subscriber. What is left is to add the ShapesSubscriberManager to the viewport. You can directly drag the ShapesSubscriberManager C++ class into the viewport.

In the details view of the ShapesSubscriberManager instance set the Shapes publisher and Domain ID as shown above. The Square Sub is set to SquareSubDynamic, the Circle Sub is set to CircleSubDynamic, and the Triangle Sub is set to TriangleSubDynamic.

Go ahead and click play. Now go ahead and publish different shapes of different colors from the shapes application. You should see the 3D shape moving in Unreal Engine and when you delete the publisher in the Shapes application it will disappear from the 3D view. 

 

Module 10 Demo:

This video covers the steps for Module 10.

 

Keep Going! 

Module 11: Publishing at Runtime