VR-Engage  2.2
Loading...
Searching...
No Matches
State Example

Overview

Purpose: This example demonstrates extending VR-Engage's built-in player station states with custom UI elements and message handling. It illustrates the complete pattern for replacing default states while integrating Qt Quick/QML for custom user interfaces.

Observable Behavior: When running this example, you will see a collapsible radio messages window appear in the upper-right corner of the screen when entering the engaged state. The window displays real-time text messages exchanged between entities, showing sender names, receiver names, timestamps, and message content. The window automatically appears when entering the engaged state and hides when exiting. Users can manually toggle window visibility using the expand/collapse button and clear all messages using the clear button.

Prerequisites:

  • Understanding of Player Station Framework state machine architecture
  • Familiarity with Qt Quick/QML model-view patterns and property binding
  • Knowledge of VR-Engage Messaging Framework for message handling

Related Examples:

Key Concepts Demonstrated

This example demonstrates:

  1. Player Station State Replacement - Replacing built-in DtEngagedState functionality with custom behavior. See Player Station Framework for state machine details. This example uses an identical state name to replace rather than extend the default engaged state.
  2. Qt Model/View Architecture - Implementing QAbstractListModel for dynamic QML data binding. The implementation uses custom role enumeration to map C++ data to QML property names and demonstrates proper model change notifications for automatic view updates.
  3. Asynchronous QML Integration - Loading QML user interfaces within VR-Engage's rendering pipeline. The example uses DtQtQuickRenderer for non-blocking QML file loading and shows context property registration enabling bidirectional C++ to QML communication.
  4. State Lifecycle Resource Management - Proper cleanup and reinitialization across state transitions. This includes message handler registration in onEnter() with mandatory cleanup in onExit(), and UI window management through state stacking operations.

Code Walkthrough

State Registration Pattern

The plugin registers a replacement state using the same identifier as the built-in state:

// File: examples/state/plugin.cxx
{
app->stateManager().registerState(new DtExtendedEngagedState(*app));
return true;
}
Top-level class representing the VR-Engage application.
Definition playerStationApp.h:163
virtual DtPlayerStationStateManager & stateManager()
Gets the application state manager.
virtual void registerState(DtPlayerStationState *state)
Registers a new state with the manager.
VRECOMMONCOMPONENTS_DLL bool initPlayerStationModule(makVre::DtPlayerStationApp *app)
Module initialization function declarations using C linkage.

The DtExtendedEngagedState constructor internally sets its name to "ENGAGED_STATE", which matches the built-in state's identifier. When the state manager encounters a state registration with an existing name, it automatically replaces the previous state with the new implementation. This approach allows seamless extension of built-in states without modifying core state transition logic.

State Lifecycle Implementation

The extended state manages both UI windows and message subscriptions across its complete lifecycle:

// File: examples/state/extendedEngagedState.cxx
void DtExtendedEngagedState::onEnter(const DtPlayerStationStateArgs* args)
{
if (!myChatWindow)
{
myChatWindow = new DtExampleChatWindow();
}
else
{
myChatWindow->open();
}
DtVreMessageManager::instance().addHandler(VrfTextMessage::theType(),
DtVreMessageDelegate(this, &DtExtendedEngagedState::handleTextMessage));
DtEngagedState::onEnter(args);
}
void DtExtendedEngagedState::onExit()
{
if (myChatWindow)
{
myChatWindow->close();
}
DtVreMessageManager::instance().removeHandler(VrfTextMessage::theType(),
DtVreMessageDelegate(this, &DtExtendedEngagedState::handleTextMessage));
DtEngagedState::onExit();
}
void DtExtendedEngagedState::onStacked()
{
if (myChatWindow)
{
myChatWindow->setVisibility(false);
}
DtEngagedState::onStacked();
}
void DtExtendedEngagedState::onExposed()
{
if (myChatWindow)
{
myChatWindow->setVisibility(true);
}
DtEngagedState::onExposed();
}
DtDelegate< DtVreMessageResult, DtVreMessage * > DtVreMessageDelegate
Delegate type for message handler callbacks.
Definition vreMessage.h:212

Message handlers must be explicitly removed in onExit() because they accumulate if not cleaned up, leading to memory leaks and duplicate message processing on subsequent state entries. The chat window object is reused rather than recreated across state transitions to maintain accumulated messages and improve performance. Calling the base class methods (DtEngagedState::onEnter(), etc.) preserves all standard engaged state functionality including entity control and simulation updates. The onStacked() and onExposed() methods manage window visibility when modal dialogs or other states temporarily overlay the engaged state.

QML Data Model Implementation

The chat model exposes C++ data to QML through the standard Qt model-view architecture:

// File: examples/state/exampleQtQuick.cxx
void DtChatQmlModel::addChatEntry(const DtChatEntry& entry)
{
beginInsertRows(QModelIndex(), rowCount(), rowCount());
myChatEntries << entry;
endInsertRows();
}
QHash<int, QByteArray> DtChatQmlModel::roleNames() const
{
QHash<int, QByteArray> roles;
roles[SenderRole] = "sender";
roles[ReceiverRole] = "receiver";
roles[TimeRole] = "time";
roles[MessageRole] = "msg";
return roles;
}
QVariant DtChatQmlModel::data(const QModelIndex& index, int role) const
{
if (!index.isValid() || index.row() >= myChatEntries.count())
return QVariant();
const DtChatEntry& entry = myChatEntries[index.row()];
switch (role)
{
case SenderRole: return entry.mySender;
case ReceiverRole: return entry.myReceiver;
case TimeRole: return entry.myTime;
case MessageRole: return entry.myMessage;
}
return QVariant();
}

The beginInsertRows() and endInsertRows() calls bracket the data modification and trigger automatic ListView updates in QML. The roleNames() method maps C++ role enumerations to string property names that QML delegate items can reference directly (e.g., sender, receiver, msg). This pattern enables real-time data binding where QML views automatically reflect changes to the underlying C++ model without explicit refresh calls.

Qt Quick Integration and Context Properties

The chat window demonstrates complete Qt Quick integration within VR-Engage's rendering system:

// File: examples/state/exampleQtQuick.cxx
DtExampleChatWindow::DtExampleChatWindow()
{
myTargetFile = "examples/exampleState.qml";
DtQtQuickRenderer::instance()->rootContext()->setContextProperty("chatModel",
QVariant::fromValue(&myChatModel));
DtQtQuickRenderer::instance()->rootContext()->setContextProperty("_chatPage", this);
DtQtQuickRenderer::instance()->loadQMLFile(this, "", myTargetFile,
[this](QQuickItem* root) {
this->myRoot = root;
this->initMain();
});
}

Context properties expose C++ objects directly to QML through globally accessible names, enabling the QML UI to display model data and invoke C++ methods marked with Q_INVOKABLE (such as clearChat()). The loadQMLFile() call is asynchronous to prevent blocking VR-Engage's main rendering thread during file I/O and QML parsing. The lambda callback executes after loading completes, storing the root QML item reference and performing any initialization that requires the fully-loaded QML scene.

Message Processing with Entity Resolution

The state processes incoming VR-Forces text messages and displays them with human-readable entity names:

// File: examples/state/extendedEngagedState.cxx
DtVreMessageResult DtExtendedEngagedState::handleTextMessage(DtVreMessage* message)
{
ASSERT_TYPE(message, VrfTextMessage, vrfMsg);
const auto er = app().appAttributeStore()->getAttribute<DtEntityResolver*>("DtEntityResolver");
if (!er)
{
return NOT_HANDLED;
}
const std::string senderName = er->findDisplayName(vrfMsg->getSenderEntityID());
const std::string receiverName = (vrfMsg->getReceiverEntityID() == DtEntityIdentifier::nullId())
? "All"
: er->findDisplayName(vrfMsg->getReceiverEntityID());
myChatWindow->addNewMessage(QString::fromStdString(senderName),
QString::fromStdString(receiverName), vrfMsg->getTime(),
QString::fromStdString(vrfMsg->getMessage()));
return HANDLED;
}
const T & getAttribute(const std::string &attribute) const
Gets the value of a child attribute.
DtAttributeHandle appAttributeStore()
Gets the application attribute store.
#define ASSERT_TYPE(msg, MsgType, msgCast)
Asserts that a message is of the expected type and casts it.
Definition vreMessageManager.h:205

Raw entity IDs consisting of site, application, and entity numbers are not meaningful to human operators, so the DtEntityResolver service translates these numeric identifiers to human-readable marking text or call signs configured in the simulation. A null receiver ID indicates a broadcast message addressed to all entities, displayed as "All" in the UI. Returning HANDLED from the message handler prevents other registered handlers from processing the same message, which is appropriate when the message has been fully consumed.

Deployment and Testing

Installation

Build the example (see Environment Setup & Build Guide):

cd examples\build
cmake --build . --config RelWithDebInfo --target state

Install the plugin to the VR-Engage installation:

cmake --install . --config RelWithDebInfo

This copies the plugin to <VR-Engage-Install-Dir>\plugins64\vrEngage\release\exampleState.dll.

Verify installation:

dir "<VR-Engage-Install-Dir>\plugins64\vrEngage\release\exampleState.dll"

The QML UI file is located at <VR-Engage-Install-Dir>\data\UI\examples\exampleState.qml and should be available in the standard VR-Engage installation.

Configuration

Option 1: Use any existing scenario (no configuration required):

  1. Launch VR-Engage with any scenario or in unhosted mode
  2. The extended engaged state automatically replaces the default state

Option 2: Verify with VR-Forces integration:

  1. Launch VR-Forces with a multi-entity scenario
  2. Connect VR-Engage to the same exercise
  3. Configure entities to send text messages for testing

Testing Procedure

  1. Launch VR-Engage and connect to an exercise (or run unhosted).
  2. Select any role and transition to the engaged state.
  3. Verify initial behavior. A collapsible "Radio Messages" window should appear in the upper-right corner. The window initially displays in a collapsed state with an expand button visible. Click the expand button to reveal the full chat interface, which includes a clear button (trash icon) in the header.
  4. Test window interaction. Click the expand/collapse button to toggle window visibility and verify the window animates smoothly between hidden and shown states. Click the clear button to remove all messages from the list.
  5. Test message display (requires entities sending text messages). Send text messages between entities using VR-Forces or another connected application. Messages should appear in the scrollable list formatted as [timestamp] (SenderName) ---> (ReceiverName): Message content. The window should automatically scroll to show the newest messages, and entity names should display as marking text rather than numeric IDs.
  6. Test state transitions. Exit the engaged state by disconnecting from the entity and verify the window disappears. Re-enter the engaged state and confirm the window reappears with previous messages intact. Test with modal dialogs to verify the window hides when other states overlay the engaged state.

Verification: Check the VR-Engage log (the most recent *.log file in the MAK log directory, typically C:/MAK/logs) for initialization messages such as [ExtendedEngagedState] Registered extended engaged state and [ExampleChatWindow] Chat window created successfully. Verify that the window appears only in the engaged state and hides during state transitions. Confirm that message formatting includes proper timestamps and entity name resolution.

Troubleshooting

Plugin not loading: Verify the DLL is in the correct plugins directory at <VR-Engage-Install-Dir>\plugins64\vrEngage\release\. Check Qt dependencies using Dependency Walker to ensure Qt5Core.dll, Qt5Qml.dll, and Qt5Quick.dll are accessible. Review the VR-Engage log (most recent *.log file in the MAK log directory) for DLL load errors or missing symbols.

QML file loading errors:

  • Symptom: Chat window doesn't appear, console shows QML parser errors
  • Cause: QML file missing or path incorrect in myTargetFile
  • Solution: Verify exampleState.qml exists at data\UI\examples\exampleState.qml

Window appears but no messages display:

  • Symptom: Chat window visible but remains empty during text message traffic
  • Cause: Message handler not registered or entity resolver unavailable
  • Solution: Check logs for handler registration errors, verify VR-Forces connection

Entity names show as numbers:

  • Symptom: Messages display with numeric IDs instead of entity names
  • Cause: Entity resolver not finding marking text for entity IDs
  • Solution: Ensure entities have proper marking text configured in VR-Forces scenario

Window doesn't hide on state exit:

  • Symptom: Chat window remains visible when leaving engaged state
  • Cause: State lifecycle methods not being called or window hide logic failing
  • Solution: Verify state transitions complete properly, check onExit() implementation

Technical Reference

File Structure

examples/state/
├── CMakeLists.txt # Build configuration
├── README.md # This documentation
├── plugin.h/.cxx # Plugin entry point
├── extendedEngagedState.h/.cxx # Custom state implementation
├── exampleQtQuick.h/.cxx # Qt Quick chat window
└── export.h # DLL export macros

Key Classes

Class Base Class Purpose Header
DtExtendedEngagedState DtEngagedState Custom engaged state with chat UI extendedEngagedState.h
DtExampleChatWindow QObject Qt Quick window manager exampleQtQuick.h
DtChatQmlModel QAbstractListModel Chat data model for QML exampleQtQuick.h
DtChatEntry (none) Chat message data container exampleQtQuick.h

API Methods Used

  • DtPlayerStationStateManager::registerState() - Register custom state with state machine
  • DtVreMessageManager::addHandler() / removeHandler() - Message handler lifecycle
  • DtQtQuickRenderer::loadQMLFile() - Asynchronous QML loading
  • QQmlContext::setContextProperty() - Expose C++ objects to QML
  • QAbstractListModel::beginInsertRows() / endInsertRows() - Model change notifications
  • DtEntityResolver::findDisplayName() - Entity ID to marking text resolution

Qt Model Roles

Role Enum Value QML Property Description
SenderRole Qt::UserRole + 1 sender Message sender entity name
ReceiverRole Qt::UserRole + 2 receiver Message receiver entity name
TimeRole Qt::UserRole + 3 time Message timestamp (simulation time)
MessageRole Qt::UserRole + 4 msg Message text content

Build Targets

  • Plugin: exampleState.dll (Windows)
  • Install Location: plugins64/vrEngage/

Related Documentation: