Vortex Studio SDK: Adding User Controls

This section is aimed at developers who want to extend Vortex® with more functionality or communicate from Vortex to and from an external system.

Topics

A common task among developers who use the Vortex Studio SDK is the development of a user interface to control their application and thus the simulation. An example of such an interface can be seen in the Vortex Studio Player application. Some of its main features are:

  • Load content
  • Start/stop/pause the simulation
  • Record sessions which can then be replayed at a later time
  • Shows info, warning and error messages generated by the application

This section of the guide will show you how you can develop equivalent functionality inside your own application using the Vortex Studio SDK.

Adding an Operator Console

There is basically two ways to allow an external user control over a Vortex application: insert our user controls inside the application or make a separate application that communicates with Vortex through an external channel.

Here we will focus on the former method, which is both simpler and gives more direct access to Vortex through your user interface.

When you launch the Vortex Studio Player, you might have the impression that there are two distinct applications: the simulation application with its blue background and the user control application containing all widgets.

Actually, those two windows are part of the same Vortex application. The user control portion of the application is simply a plugin (see Vortex Studio SDK - Customizing Vortex - About Plugins) containing extensions (see Vortex Studio SDK - Customizing Vortex - Extensions) which handles the user interface and its logic. As with any extension, they must be handled by a module. Each extension will also typically implement an interface specific to your user interface. That interface can be checked against each extension received by your module's onExtensionAdded method.

Most of Vortex Studio's graphical applications uses the Qt GUI toolkit to develop native C++ graphical applications. This is in no way a restriction on which technology you may use for your own application. Since you will be making your own module to handle your UI extensions, you can use whatever UI technology is most appropriate for the task at hand.

Going back to the example of the Vortex Studio Player, each of its pages are classes deriving from VxSim::IExtension and also VxSim::UI::IQtPage. The VxQtModule, which handles extensions implementing the VxSim::UI::IQtPage, is added to the application setup document (VXC file). All UI extensions are also added to the setup document. When launching the application, the module will be automatically added to the application and it will manage all extensions it accepts in its onExtensionAdded method. It is also at that moment, when the extension is added to the module that you would signal the extension to start the creation of its user control elements.

Note
To ensure that operations done by your user interface do not negatively impact the performance of your simulation, be sure to separate both processes into different threads of execution. In other words, when your extension gets added to your module in its onExtensionAdded method, do not start creating widgets in the simulation thread. Instead signal your extension in another thread that it can now create its content and handle UI-related events.
For example, the IQtPage interface offers the createPage virtual method that is implemented by each specific extension and will be called from the UI thread.

The following is a small code example in C++ that demonstrates what was explained above using a custom IUIPage tagging interface which our module will use to distinguish extensions it is interested in managing.

#include <VxSim/IExtension.h>
#include <VxSim/ISimulatorModule.h>
#include <VxSim/VxSmartInterface.h>

#include "IUIPage.h"

// Tagging interface for our module
class IUIPage
{
	// Pure virtual function to implement in each derived class.
	virtual void createPage() = 0;
};

class ContentBrowser : public VxSim::IExtension, public IUIPage
{
public:
	// Methods overloaded from VxSim::IExtension
	virtual ~ContentBrowser();

	ContentBrowser(VxSim::VxExtension *proxy);

	...

	// Method overloaded from IUIPage
	virtual void createPage()
	{
		// Create widgets in UI thread.
		...
	}

	...

	void setApplication(VxSim::VxApplication* application)
	{
		mApplication = application;
	}

private:
	VxSim::VxApplication* mApplication;
};

class UIModule : public VxSim::ISimulatorModule
{
	...

	virtual void onExtensionAdded(VxSim::VxExtension* extension)
	{
		VxSim::VxSmartInterface<IUIPage> uiPage = extension;
		if (uiPage.valid())
		{
			_addManagedExtension(extension);
			uiPage->setApplication(getApplication());
			uiPage->createPage();
		}
	}

	...
};

The following sections detail common functionalities that developers typically want to expose in their Vortex application.

Loading Content

As described in Vortex Studio SDK - Integrating the Application - Loading Content, you can use the class VxSim::VxSimulationFileManager of your VxSim::VxApplication to load content on all network nodes. Sometimes this is enough but there are also other cases where the class VxSim::VxSimulationFileManagerFacade is more appropriate. The class VxSimulationFileManagerFacade has the added advantage of not requiring direct access to the VxApplication and of being thread safe. You can call the loadObject() method from your UI thread and it will dispatch an event to the VxApplication in the simulation thread to load your object. Additionally you can register instances of VxSimulationFileManagerFacade::Listener to the VxSimulationFileManagerFacade to be notified about the state of your load/unload request.

Available callbacks are:

These callbacks give you all the flexibility needed to adjust your user interface based on the state of the content.

Start, Pause, Stop

As explained in Vortex Studio SDK - Integrating the Application - Running the Application, there are three application modes. You initially load content in Editing mode and when you are ready to simulate you switch to Simulating mode. During the simulation you can easily pause, resume or even make it run for just one simulation update (i.e., simulating step-by-step).

The class VxSim::VxApplication contains many methods to perform each of these tasks. The methods which will trigger a change in the state of the application will dispatch an event which will perform the actual task on the simulation thread, they can thus safely be called from the UI thread.

Here is an excerpt from the VxApplication's class header. Some comments have been shortened, for the full documentation please refer to VxApplication.h.

// Changes the application mode asynchronously. The application mode will change when possible.
// Note that the current update will stay at the same mode.
bool setApplicationMode(eApplicationMode mode);

// Determines whether the paused flag is set on the simulation.
bool isPaused();

// Negation of isPaused() when the mode is VxSim::kModeSimulating.
bool isSimulationRunning();

// Pauses the simulation.
// It will take effect at the next application update.
void pause(bool pause = true);

// Resumes the simulation.
// It will take effect at the next application update.
void resume();

// Makes the simulation do one step and then pause.
void stepOnce();

Going back to our example ContentBrowser class, the following demonstrates how it may expose some of these functionalities.

void ContentBrowser ::simulate()
{
	mApplication->setApplicationMode(VxSim::kModeSimulating);
	mApplication->resume();
}
void ContentBrowser::pause()
{
	mApplication->pause();
}
void ContentBrowser ::step()
{
	mApplication->setApplicationMode(VxSim::kModeSimulating);
	mApplication->stepOnce();
}
void ContentBrowser ::stop()
{
	mApplication->setApplicationMode(VxSim::kModeEditing);
}

Record and Playback

When you want to capture what happens visually during a simulation for later review, the best tool at your disposal is the recorder. A recording is not a complete sequence of the state of all objects in the simulation at all times. A recording contains only the kinematic data of the object during the simulation. In other words, to keep the recording light, simple and performing well, we save only the position of objects during the recorded interval. These kinematic data can be replayed (playback) afterward resulting in a visual "replay" of the recorded simulation. You cannot, for example, pause a playback mid-way and start to simulate from that point (that is the role of a key frame). But it is the perfect tool if, for example, you want to save a student's performance in a particular scenario and review it later.

To control the recorder from inside your application, Vortex Studio SDK provides a convenient facade to its internal record and playback facilities called VxSim::Recorder::VxRecorder. Most methods of this class are asynchronous, ensuring no slow-down of your user interface, and all dispatch events to the internal objects running inside the simulation thread. Thus VxRecorder can safely be called directly from UI callbacks such as a click of a button.

The open() method is both used for previously recorded files you want to play() and to create new empty files to which you will record(). Using VxRecorder to play back a recording changes the application mode to VxSim::kModePlayingBack. This is done automatically and you do not need to manually call VxApplication::setApplicationMode(). However, when you are done using the VxRecorder's functionalities, you must call the close() method, which will set the application mode back to the old value upon switching to the VxSim::kModePlayingBack mode, and restores the application to the state it was in before entering playback.

Logging

There is a wide range of informative messages that are logged by Vortex Studio. These messages can easily be re-routed to your application instead of their default log file and you can even use Vortex's logging system to add your own messages. All those functionalities are exposed in C++ through the functions in the VxMessage.h header file.

You can log messages to Vortex using a set of increasing levels based on the severity of the message you wish to convey. The levels are represented in the following enum.

/// The log level defines which messages will be taken into consideration by the logging system.
enum eLogLevel
{
	/// Turn the logging Off.
	kOff,

	/// Fatal Error. The application cannot further proceed.
	kFatal,

	/// Error throws an exception giving a chance to the application to continue.
	kError,

	/// Warn message to the end user : reports an issue that requires an user action.
	/// Expect these to be immediately visible on a status console.
	kWarn,

	/// Information message to the end user : describes application level operations.
	/// These might be visible on a console, so be conservative and keep to a minimum.
	kInfo,

	/// Debug message to report potential issues.
	/// Expect these to be written to logs only.
	kDebug,

	/// Debug message to report finer-grained informational events than kDebug
	/// Expect these to be written to logs only.
	kTrace,

	/// Turn all messages logging On.
	kAll
};

By default there is no way to recover from a fatal error. It will halt your application. An error throws an exception which allows for a softer termination or even a recovery depending on the circumstances. If you implement your error message handler (see below) be sure to follow the guidelines listed in the eLogLevel enum as this what Vortex expects.

You can use the function LogSetLevel() to set the minimum level of messages you are interested in receiving. For example, if you call LogSetLevel(kWarn), you will only receive warnings, errors and fatal errors; info, debug and trace messages will be silently dropped.

To register your own message handler instead of the default one, use the function LogSetHandler() which takes a callback function of type void (*LogHandler)(eLogLevel level, const char *const format, va_list ap) as its sole argument. Your handler will be called for each new message from that point on with the message's log level, its format string and a variadic list of argument for the string. The LogSetHandler() function returns the previous handler in case you would want to put it back at some point.

To log your own messages, simply use the appropriate function for the log level of the message (LogFatal(), LogError(), LogWarn(), etc.). All of those functions use a format string with a variadic argument list, exactly like the C printf() function.

If you want to log to a file, use the setLogFile() function with the path to the file as a string. To disable file logging, simply pass an empty string to the function.

 

Next topic: Creating an Application