VR-Engage  2.2
Loading...
Searching...
No Matches
Input and Control

The VR-Engage input system provides a flexible framework for handling user input from keyboards, mice, game controllers, joysticks, and specialized simulation hardware. This section covers the architecture from a developer's perspective, including how to create custom input handlers, define new actions, and integrate with the backend simulation.

Overview and Architecture

The input system follows a layered architecture that separates physical device handling from logical action processing. This separation enables flexible input remapping, multi-device support, and runtime configuration without code changes.

Input system architecture

flowchart TB
    subgraph Devices["Physical Devices"]
        KB[Keyboard]
        Mouse[Mouse]
        Gamepad[Gamepad]
        HOTAS[HOTAS/Flight Stick]
        Custom[Custom Hardware]
    end

    subgraph InputLayer["Input Layer"]
        DeviceManager[Device Manager]
        RawEvents[Raw Input Events]
    end

    subgraph MappingLayer["Mapping Layer"]
      ConfigFiles[Lua config files]
        ActionMapper[Action Mapper]
        LayerStack[Layer Stack]
    end

    subgraph ActionLayer["Action Layer"]
        ActionDispatcher[Action Dispatcher]
        Handlers[Action Handlers]
    end

    subgraph Components["Components"]
        ControlLogic[Control Logic]
        UIComponents[UI Components]
    end

    KB --> DeviceManager
    Mouse --> DeviceManager
    Gamepad --> DeviceManager
    HOTAS --> DeviceManager
    Custom --> DeviceManager

    DeviceManager --> RawEvents
    RawEvents --> ActionMapper
    ConfigFiles --> ActionMapper
    ActionMapper --> LayerStack
    LayerStack --> ActionDispatcher
    ActionDispatcher --> Handlers
    Handlers --> ControlLogic
    Handlers --> UIComponents

The device manager polls connected devices and generates raw input events. The action mapper transforms these events into named actions based on configuration files. The layer stack manages priority when multiple mapping configurations are active simultaneously. Finally, action handlers in components respond to the dispatched actions.

Frontend and backend coordination

Input processing spans both frontend and backend processes. The frontend captures physical input and generates actions, while the backend validates and applies those actions to the simulation state.

sequenceDiagram
    participant Device as Input Device
    participant Frontend as Frontend Process
    participant Message as Message Layer
    participant Backend as Backend Process
    participant Sim as Simulation State

    Device->>Frontend: Raw Input Event
    Frontend->>Frontend: Map to Action
    Frontend->>Frontend: Local Feedback (UI)
    Frontend->>Message: Control Command
    Message->>Backend: Receive Command
    Backend->>Backend: Validate Command
    Backend->>Sim: Apply to Simulation
    Sim->>Message: State Update
    Message->>Frontend: Receive Update
    Frontend->>Frontend: Update Display

The frontend provides immediate feedback for responsive user interaction, while the backend maintains authoritative simulation state. This separation produces consistent behavior across networked simulations while preserving responsiveness.

Quick Start

Here are the essential steps for the most common input customizations:

Adding a new input action

  1. Define the mapping in a Lua config file (e.g., driver/keyboard.lua):
    {type="key"; key="H"; action="horn"};
  2. Register the handler in your C++ component's initialize() method:
    myDriverInputLogic.addActionHandler(inputGroup, "horn",
    DtActionDelegate(this, &DtDriverControlLogic::activateHorn));
  3. Implement the handler with the required signature:
    void DtDriverControlLogic::activateHorn(float value)
    {
    // value is 1.0 on press, 0.0 on release
    myVehicle->setHornActive(value > 0.5f);
    }

Remapping an existing action to different hardware

Modify or add entries in the appropriate device config file. No C++ changes are required—the action name binds the config to the existing handler.

Supporting custom hardware

  1. Subclass DtInputDevice and implement init(), tick(), and shutdown()
  2. Register the device type with DtInputDeviceFactory::addCreator<>()
  3. Create a Lua config file mapping the device's inputs to actions

Configuration

Input mappings are defined in Lua configuration files within the simulation model set. These files specify how physical inputs map to logical actions and support features like dead zones, scaling, and inversion.

Config file locations

Input configuration files are loaded from the path specified by playerStationApp.inputSettingsPath in the application's Lua startup script. The default VR-Engage configuration sets this to:

inputSettingsPath = "$(DATA_DIR)/simulationModelSets/VR-Engage/input/";

Custom applications can point to their own input directory. For example, the vehicle blinker example uses:

inputSettingsPath = "$(DATA_DIR)/simulationModelSets/examples/vehicleBlinker/input/";

To customize input mappings, either modify files in your simulation model set's input directory or create a custom model set with its own input configuration path.

Supported device types

The following device type strings are recognized in configuration files:

DeviceType Description
"keyboard" Standard keyboard
"mouse" Mouse buttons and movement
"gamepad" Xbox-style game controllers
"joystick" Generic joystick/flight stick
"hotas" HOTAS (Hands On Throttle-And-Stick) devices
"touchController" VR touch controllers

Discovering device axis and button IDs

For gamepads and joysticks, axis and button IDs in configuration files correspond to SDL (Simple DirectMedia Layer) indices. VR-Engage includes a Game Controller Tool (gameControllerTool.exe) to help identify these values for your hardware.

Basic usage

List all connected devices:

gameControllerTool.exe --listDevices

Example output:

Found 2 joystick(s)
Joystick Name: 'Xbox Wireless Controller'
Joystick Number: 0
Number of Axes: 6
Number of Buttons: 11
Number of Hats: 1
Number of Balls: 0
Joystick Name: 'Logitech Extreme 3D'
Joystick Number: 1
Number of Axes: 4
Number of Buttons: 12
Number of Hats: 1
Number of Balls: 0

Detailed device information

Get verbose information about a specific device, including axis and button names:

gameControllerTool.exe -n 0 --verbose

This shows the mapping between numeric IDs and named controls (for recognized game controllers).

Real-time input monitoring

Monitor live input events to see exactly which axis or button corresponds to each physical control:

gameControllerTool.exe -n 0 --debug

This mode prints events as you move axes or press buttons:

SDL_CONTROLLERAXISMOTION: controller: 0 axis: 0 (leftx) value: -12453 norm: -0.38
SDL_CONTROLLERBUTTONDOWN: controller: 0 button: 0 (a)
SDL_CONTROLLERBUTTONUP: controller: 0 button: 0 (a)

Use the axis and button IDs from this output directly in your Lua configuration files.

Basic mapping configuration

Top-level role input files list one or more per-device mapping files. For example, the driver role input file loads keyboard, gamepad, and joystick mappings:

-- filepath: data/data/simulationModelSets/VR-Engage/input/driverInput.lua
configFiles = {
"driver/gamepad.lua";
"driver/keyboard.lua";
"driver/joystick.lua";
}

Each per-device file defines one or more inputDevices entries with mapping groups and actions. For example, driver keyboard mappings:

-- filepath: data/data/simulationModelSets/VR-Engage/input/driver/keyboard.lua
inputDevices={
{
DeviceType = "keyboard";
MappingGroups={
{
Group="driving";
Mappings = {
{type="key"; key="A"; action="steer-left"};
{type="key"; key="D"; action="steer-right"};
{type="key"; key="W"; action="throttle"};
{type="key"; key="S"; action="brake"};
{type="key"; key="E"; action="gear-up"};
{type="key"; key="Q"; action="gear-down"};
{type="key"; key="X"; action="engine-enable"};
{type="key"; key="L"; action="headlights"};
{type="key"; key="P"; action="parking-brake"};
};
};
};
};
};

Value transform configuration

Analog axes and other continuous inputs use valueTransforms to adapt device characteristics to application requirements. Supported transform types are registered in DtVreInputManager::valueTransformFactory() and include:

  • dead-zone – ignore small input magnitudes (field: value)
  • gain – amplify or attenuate input (field: value)
  • invert – flip the sign of the input
  • scale – multiply input by a constant factor (field: value)
  • remap – remap one numeric range into another (fields: inputMin, inputMax, outputMin, outputMax)

Example gamepad steering axis with dead zone and gain:

-- filepath: data/data/simulationModelSets/examples/inputDevice/input/driver/gamepad.lua
inputDevices={
{
DeviceType="gamepad";
MappingGroups={
{
Group="driving";
Mappings={
{type="axis"; id=0; action="steering";
valueTransforms = {
{type="dead-zone"; value=0.01};
{type="gain"; value=0.5};
};
};
};
};
};
};
};

Example joystick throttle and brake remapped from device range to 0–1:

-- filepath: data/data/simulationModelSets/VR-Engage/input/driver/joystick.lua
{type="axis"; id=1; action="throttle";
valueTransforms = {
{type="remap"; inputMin=-1; inputMax=1; outputMin=0; outputMax=1}
}
};
{type="axis"; id=2; action="brake";
valueTransforms = {
{type="remap"; inputMin=1; inputMax=-1; outputMin=0; outputMax=1}
}
};

C++ Implementation

Registering action handlers

Components typically use DtInputLogic (or a subclass) to load input mappings and register action handlers during initialization.

// filepath: libsrc/commonRoleLibraries/vreCommonComponents/driverControlLogic.cxx
bool DtDriverControlLogic::initialize(DtPlayerStation* player, DtInitTable& config)
{
if (!DtEntityControlLogic::initialize(player, config))
{
return false;
}
myDriverInputLogic.initialize();
std::string inputGroup = "driving";
myDriverInputLogic.loadInputConfig(myInputConfigFile, inputGroup);
myDriverInputLogic.addActionHandler(
inputGroup, "steering", DtActionDelegate(this, &DtDriverControlLogic::setSteering));
myDriverInputLogic.addActionHandler(
inputGroup, "steer-left", DtActionDelegate(this, &DtDriverControlLogic::steerLeft));
myDriverInputLogic.addActionHandler(
inputGroup, "steer-right", DtActionDelegate(this, &DtDriverControlLogic::steerRight));
myDriverInputLogic.addActionHandler(
inputGroup, "throttle", DtActionDelegate(this, &DtDriverControlLogic::setThrottle));
myDriverInputLogic.addActionHandler(inputGroup, "brake", DtActionDelegate(this, &DtDriverControlLogic::setBrake));
myDriverInputLogic.addActionHandler(inputGroup, "gear-up", DtActionDelegate(this, &DtDriverControlLogic::setGearUp));
myDriverInputLogic.addActionHandler(
inputGroup, "gear-down", DtActionDelegate(this, &DtDriverControlLogic::setGearDown));
myDriverInputLogic.addActionHandler(
inputGroup, "engine-enable", DtActionDelegate(this, &DtDriverControlLogic::setEngineEnable));
myDriverInputLogic.addActionHandler(
inputGroup, "parking-brake", DtActionDelegate(this, &DtDriverControlLogic::setParkingBrake));
myDriverInputLogic.addActionHandler(
inputGroup, "headlights", DtActionDelegate(this, &DtDriverControlLogic::setHeadlights));
return true;
}

Important: The action name strings (e.g., "steering", "throttle") must match exactly between the Lua configuration and the C++ handler registration. These strings are case-sensitive. If a Lua config defines an action with no registered handler, the input is silently ignored. If a handler is registered for an action not defined in any active config, it simply never fires.

Action handler signature

Action handlers are registered via DtActionDelegate, which is defined as:

using DtActionDelegate = DtDelegate<void, float>;

Handlers therefore have a single float parameter:

Parameter Type Description
value float Normalized action value (typically -1.0 to 1.0 for axes, 0.0 or 1.0 for buttons)

Press/release semantics for buttons are represented by the value (for example, 0.0 for released, 1.0 for pressed); axis actions use the full continuous range supplied by the input mappings.

Mapping layers and priority

The input system supports multiple active mapping layers. Each call to DtInputLogic::loadInputConfig adds a mapping layer backed by a Lua config file and a mapping group. Layers are stored in a stack inside DtVreInputManager, with the front of the stack processed first.

// filepath: libsrc/framework/vrePlayerStation/playerStationAppInputLogic.cxx
void DtPlayerStationAppInputLogic::initialize()
{
DtInputLogic::initialize();
std::string inputGroup = "app";
// Adds the appInput.lua mapping layer for group "app"
loadInputConfig("appInput.lua", inputGroup, false, true);
addActionHandler(inputGroup, "escape", DtActionDelegate(this, &DtPlayerStationAppInputLogic::escape));
addActionHandler(inputGroup, "toggle-application-mode",
DtActionDelegate(this, &DtPlayerStationAppInputLogic::toggleApplicationMode));
}

DtVreInputManager::addMappingLayer takes an alwaysOnTop flag as well as an alwaysEnabled flag. Layers marked alwaysOnTop stay at the top of the stack; otherwise, layers are inserted below any existing alwaysOnTop layers. When multiple layers define the same action, the first layer in the stack that handles the input wins, enabling context-sensitive remapping.

Layer priority example

Consider a scenario where both the application layer and the driver layer define an "escape" action:

Stack (top to bottom):
[1] appInput.lua (alwaysOnTop=true) -> "escape" opens pause menu
[2] driverInput.lua (alwaysOnTop=false) -> "escape" exits vehicle

When the user presses Escape, layer [1] handles the input first. If the app layer's handler consumes the event (returns without propagating), the driver layer never sees it. This enables modal UI contexts like menus to intercept inputs that would otherwise control the simulation.

Custom device integration

For specialized hardware not supported by the built-in devices, implement a custom device class that inherits from DtInputDevice and register it with DtInputDeviceFactory.

When do you need a custom device? The built-in device types handle standard keyboards, mice, gamepads, and joysticks automatically. You only need a custom device for:

  • Hardware with proprietary SDKs (motion platforms, custom cockpit panels)
  • Virtual input sources (touch screen overlays, network-based remote controls)
  • Devices requiring special initialization or polling logic
// filepath: examples/inputDevice/qmlGamepadInputDevice.h
#pragma once
{
public:
bool init(makVre::DtVreInputManager& mgr) override;
void tick(double dt) override;
void shutdown() override;
void reportCurrentState() override {}
void deviceEventCallback(std::string eventType, int eventId, double eventValue);
private:
std::list<makVre::DtInputData> myEventQueue;
};
Custom input device adapter for VR-Engage's input mapping system.
Definition qmlGamepadInputDevice.h:67
virtual void shutdown() override
Clean up device resources and unregister event handlers.
std::list< makVre::DtInputData > myEventQueue
Event queue to gather device events between tick calls Events received via callbacks are queued here,...
Definition qmlGamepadInputDevice.h:162
virtual bool init(makVre::DtVreInputManager &mgr) override
Initialize input device and register for device events.
virtual ~DtQmlGamepadInputDevice()
void deviceEventCallback(std::string eventType, int eventId, double eventValue)
Callback receiving events from the virtual gamepad device.
virtual void tick(double dt) override
Process queued input events and submit to input manager.
void reportCurrentState()
Report current device state (required by base class, not used)
Definition qmlGamepadInputDevice.h:132
Abstract base class for hardware input device implementations.
Definition inputDevice.h:25
Central manager for input device handling, action mapping, and input processing.
Definition vreInputManager.h:81
Defines data structures and enumerations for input device data.
Abstract base class for input device implementations.
// filepath: examples/inputDevice/qmlGamepadInputDevice.cxx
bool DtQmlGamepadInputDevice::init(DtVreInputManager& mgr)
{
myManager = &mgr;
// Register callbacks with the underlying device and queue events in deviceEventCallback.
return true;
}
{
for (auto data : myEventQueue)
{
}
myEventQueue.clear();
}
void DtQmlGamepadInputDevice::deviceEventCallback(std::string eventType, int eventId, double eventValue)
{
DtInputData data;
data.myDeviceType = DtDeviceType::DtTouchController;
data.myDeviceName = "Virtual Gamepad";
data.myDeviceIndex = 0;
data.myTimestamp = myManager->simulationTime();
data.myInputType = stringToInputType(eventType);
data.myInputID = eventId;
data.myInputValue = static_cast<float>(eventValue);
myEventQueue.push_back(data);
}
DtVreInputManager * myManager
Pointer to the input manager that owns this device.
Definition inputDevice.h:75
virtual bool processInput(const DtInputData &data)
Processes raw input data from a device.

Register the custom device type during plugin initialization:

// filepath: examples/inputDevice/plugin.cxx
extern "C"
{
{
app->inputDeviceFactory()->addCreator<DtQmlGamepadInputDevice>("DtQmlGamepadInputDevice");
return true;
}
}
void addCreator(std::string name="")
Registers a creator for a specific type.
Definition factory.h:112
Top-level class representing the VR-Engage application.
Definition playerStationApp.h:163
virtual DtInputDeviceFactory * inputDeviceFactory()
Gets the input device factory.
VRECOMMONCOMPONENTS_DLL bool initPlayerStationModule(makVre::DtPlayerStationApp *app)
Module initialization function declarations using C linkage.

Reference

Troubleshooting

Common issues and diagnostic approaches for input problems:

Verifying config files are loaded

Enable input system logging by setting the log level for the vreInput category:

-- In your application config or startup script
DtLogManager:setLevel("vreInput", "debug")

This logs each config file as it loads and reports parsing errors with line numbers.

Confirming handler invocation

Add temporary logging in your action handler to verify it receives events:

void DtDriverControlLogic::setSteering(float value)
{
LOG_DEBUG("DriverControlLogic") << "Steering handler called with value: " << value << std::endl;
// ... rest of implementation
}
#define LOG_DEBUG(channel)
Macro to log a debug message to log files.
Definition logger.h:84

Common problems

Symptom Likely Cause Solution
Action never fires Typo in action name (Lua vs C++) Check case-sensitive spelling in both locations
Wrong device responds Incorrect DeviceType in config Verify device type string matches your hardware
Axis inverted or scaled wrong Missing or incorrect valueTransforms Add invert or adjust gain/scale values
Config changes ignored File in wrong directory Verify inputSettingsPath in your startup script
Lua syntax error Missing semicolons or braces Check log output for parser errors with line numbers
Unknown axis/button ID Using wrong numeric ID Run gameControllerTool.exe --debug to identify correct IDs
Device not detected Device not connected or recognized Run gameControllerTool.exe --listDevices to verify detection

User Guide reference

For end-user documentation on supported controllers, default key mappings, and controller configuration procedures, see "Chapter 12. Mouse, Keypad, and Controller Mappings" in the VR-Engage User Guide.

See also