|
VR-Engage
2.2
|
The Player Station Framework provides the core infrastructure for VR-Engage frontend development. It manages the application lifecycle, coordinates component interactions, and provides the foundation for customizing and extending the user experience.
This page describes how to implement frontend plugins and components for VR-Engage.
Unless otherwise noted, the types described here are defined in the makVre namespace and are part of the VR-Engage frontend libraries.
The Player Station Framework organizes frontend functionality into a hierarchy of cooperating classes. At the top, DtPlayerStationApp manages the overall application lifecycle and global services. Below that, DtPlayerStation instances represent individual players, each with their own role, components, and state. Components derived from DtPlayerComponent implement specific functionality like vehicle control, weapon systems, or sensor displays.
Key concepts
DtPlayerStationApp): Top-level application controller and customization entry point.DtPlayerStation): Represents a single player's session, role, and components.DtPlayerComponent): Base class for all frontend functionality units.DtAttributeHandle via playerAttributeStore()): Shared key/value store for component interaction and UI binding.This architecture promotes modularity through composition. Rather than creating monolithic vehicle classes, developers assemble vehicles from discrete components. A tank role might combine DtDriverControlLogic for movement, DtGunnerControlLogic for the main gun, DtCommanderControlLogic for situational awareness, and various sensor and radio components. This approach allows reusing components across vehicle types and testing components in isolation.
Typical workflow
DtPlayerStationApp initializes global services and loads plugins.DtPlayerStation is created (or updated) and the configured components are instantiated.initialize() / postInitialize()) and then updated every frame via tick() while the user is engaged in the role.DtPlayerStationApp serves as the top-level application controller and entry point for frontend customization. It manages global services, plugin loading, and the main update loop. Most developers interact with this class during plugin initialization to register component factories.
Advanced customization can subclass DtPlayerStationApp to override initialization behavior, add custom services, or modify the update loop. However, most extensions are better implemented as components registered through the standard plugin mechanism.
DtPlayerStation represents a single player's session within the application. It manages the player's current role, instantiated components, and player-specific state. When a user selects a role (such as "Tank Commander"), the player station instantiates the components specified in that role's configuration.
The player station provides access to player-specific services:
The state manager controls high-level application flow through a state machine. These states determine when components are active and what services are available.
stateDiagram-v2
[*] --> Disconnected
Disconnected --> Connecting: Connect requested
Connecting --> Disconnected: Connection failed
Connecting --> RoleSelection: Connected
RoleSelection --> Engaged: Role selected
Engaged --> RoleSelection: Role exited
RoleSelection --> Disconnected: Disconnect
Engaged --> Disconnected: Connection lost
Components specified in a role configuration are instantiated when entering the Engaged state and destroyed when leaving it. Resources are therefore allocated only when needed and properly cleaned up during role transitions.
Role definitions are discovered and loaded collaboratively by the Lua role loader script and the C++ player creation infrastructure. The loader script (data/factory/scripts/loadDefinition.lua) composes role templates from role files, component groups, and inheritance chains. The DtPlayerCreationPalette class catalogs available entities and roles from simulation model sets, uses the loader to build complete role configurations, and returns a merged DtInitTable when users select roles. For a detailed walkthrough of this composition pipeline, see the "How composition is implemented" section on the Role Configuration page.
Role definition files are Lua scripts that specify which component groups to instantiate and how to configure them. They live under vre-roles-dir, which by default is data/simulationModelSets/VR-Engage/roles:
Each component entry maps to a C++ class registered with the component factory (see Role Configuration for full details of the role system and file structure). Keys in the components table (such as configuration parameters) become fields in the DtInitTable passed to the component's initialize() method. For a catalog of configuration parameters supported by built-in toolkit components, see the Doxygen group Role Configuration Parameters.
DtPlayerComponent is the base class for all frontend components. Custom components inherit from this class and override lifecycle methods to implement their functionality.
Components follow a deterministic lifecycle managed by the framework. This lifecycle governs proper resource management and helps avoid common bugs.
stateDiagram-v2
[*] --> Constructed: Factory creates
Constructed --> Initialized: initialize() called
Initialized --> PostInitialized: postInitialize() called
PostInitialized --> Ticking: tick() called each frame
Ticking --> PostInitialized: tick() returns
PostInitialized --> Destroyed: shutdown() called
Destroyed --> [*]
note right of Initialized
Read configuration
Initialize members
Validate parameters
end note
note right of PostInitialized
Locate other components
Acquire shared resources
Subscribe to messages
end note
note right of Destroyed
Unsubscribe messages
Release resources
Save state if needed
end note
initialize(DtPlayerStation*, DtInitTable&) when creating the component. Use the DtInitTable to read configuration parameters from the role definition and initialize member variables. Return false to indicate failure, which prevents the component from being used.postInitialize() on each component. Use this phase to locate other components, look up shared attributes in the player attribute store, and subscribe to messages.tick(double dt) every frame with the time delta since the last frame. Perform per-frame updates here, such as smoothing input values, updating animations, or refreshing UI state.shutdown() before destroying the component or when the player station is shut down. Unsubscribe from messages, release resources, and perform final cleanup.A complete component implementation demonstrates the lifecycle methods and common patterns:
Component configuration originates from multiple sources, with later sources overriding earlier ones (the merge process is described in more detail on the Role Configuration page):
Fields in the Lua components tables become entries in the DtInitTable passed to the component's initialize() method. For example, warningRpmThreshold in the Lua configuration is read via config.findDataOr<double>("warningRpmThreshold", 1500.0) in the C++ component.
Putting it together, the typical flow for a custom component is:
const char* type() const and returns a stable type string (for example, "DtEngineMonitor").Plugin registration: The plugin's initPlayerStationModule() function registers the component with the application-wide factory using the same type string:
Role configuration: Role and component-group Lua files use that type string in the componentType field:
DtEngineMonitor instance, calls initialize(DtPlayerStation*, DtInitTable&) with a merged DtInitTable containing values from component defaults, component groups, role files, and entity overrides, and then calls postInitialize() before beginning regular tick() updates.Design components with single, well-defined responsibilities. A component that combines unrelated functionality becomes difficult to test, configure, and reuse. If a component requires conditional behavior based on role, consider splitting it into separate components composed through configuration. For example, avoid a single component that implements both complex HUD rendering and weapon control; instead, separate these concerns into independent components.
Minimize dependencies between components. Components that directly reference other components create coupling that complicates testing and limits reusability. Use message passing and the player attribute store for component interaction instead of direct references.
The frontend process uses two primary threads that affect component implementation:
Main thread executes rendering, component updates, and UI event processing. Per-frame component tick() methods, input handlers, and most message callbacks run in the main thread. Components that block this thread cause frame rate drops and input latency. Long-running operations should execute asynchronously with results delivered via message callbacks.
Network thread handles communication with the backend process. Network message reception, serialization, and initial processing occur on this thread. The message manager automatically marshals messages to the main thread for component delivery, ensuring thread-safe callback execution. Components rarely interact directly with the network thread.
Component developers work primarily with the main thread. Message subscriptions, attribute store access, and framework service calls execute on the main thread unless explicitly documented otherwise. Avoid blocking operations in message callbacks and tick() methods.
Components that create worker threads must implement explicit synchronization when accessing shared state. The framework provides no automatic thread safety. Use standard C++ synchronization primitives (such as mutexes and condition variables) to protect shared data structures accessed from multiple threads. Document thread-safety requirements clearly in component interfaces.
Frontend components execute on the main rendering thread, so unnecessary work directly impacts frame rate and input latency. Keep tick() implementations lightweight and avoid per-frame allocations, complex container operations, or blocking I/O. Prefer incremental updates and caching over recomputing expensive results every frame. Minimize logging in hot paths and use debug-only logging guards for verbose output. Avoid long-running work in message callbacks or QML property notifications; offload heavy computation to worker threads and return results via messages or attributes.
The Player Attribute Store provides frontend-local hierarchical state storage shared by all components and UI for a single player station. Unlike messages that represent discrete events, attributes represent persistent state that components can query at any time. The store supports change notifications, allowing reactive programming patterns where components respond to state changes rather than polling.
Within a player station, attributes are organized in a tree structure similar to a file system. Navigation uses the bracket operator ([]) to traverse the hierarchy, with each bracket accessing a child node by name. This organization groups related attributes logically and supports efficient subtree operations like iteration.
Path-based access supports both specific attribute queries and subtree iteration. Components navigate the hierarchy using chained bracket operators (for example, attrs["systems"]["engine"]["rpm"] or attrs["controls"]["throttle"]).
The attribute store provides type-safe accessors for reading and writing values. The API supports common types including booleans, integers, floating-point numbers, strings, and vectors.
The getOr<T>() method provides default values when attributes are unset, simplifying initialization and handling missing data gracefully. For attributes that must exist, the get<T>() method asserts if the attribute is not set or if the type does not match.
String-based path lookups are convenient but incur overhead from string parsing and tree traversal. For attributes accessed frequently (such as every frame), typed handles provide cached direct access that bypasses the path resolution.
Handle-based access is significantly faster than path-based access for high-frequency operations. The initial handle creation performs the path resolution once, caching the result for subsequent access. Handles remain valid as long as the attribute exists in the store.
Components can register callbacks to receive notifications when specific attributes change. This allows reactive patterns where components respond to state changes rather than polling for updates.
Change callbacks fire synchronously when attributes are modified, enabling immediate response to state changes. For attributes that change frequently, consider batching updates or using rate limiting to avoid excessive callback invocations.
The player attribute store is the primary bridge between simulation logic and UI. HUD elements and QML overlays bind to attribute paths (for example, attrs["systems"]["engine"]["rpm"] or attrs["systems"]["weapons"]["armed"]) and update automatically when those attributes change. Component code is responsible for publishing clean, stable attribute names and values; UI code should avoid performing complex simulation logic and instead react to attributes maintained by components.
The attribute store supports operations on entire subtrees, enabling efficient enumeration and bulk operations.
When defining attributes for a player station, follow these guidelines to keep the store predictable and easy to consume from UI and other components:
["systems"]["engine"]["rpm"], ["controls"]["throttle"], ["ui"]["warnings"]["engine"]).Each DtPlayerStation owns a single player attribute store instance that is created when the station is created and cleared when the station shuts down or the role changes. The store is a frontend-only data structure; it does not perform any network synchronization or conflict resolution on its own.
Attribute access and change callbacks occur on the main thread where components run. Components that create worker threads must use explicit synchronization when they read or write attributes from those threads. The framework does not provide automatic thread safety for attribute operations.
See also