Inter-VM Communication (GE ↔ VE ↔ UI)
Complete guide to cross-VM communication — GE to VE commands, VE to GE callbacks, peer vehicle messaging, UI events, and the electrics data bus.
BeamNG's three Lua contexts (GE, VE, UI) each run in isolation. This guide covers every bridge method available for passing data between them — from sending commands to vehicles, to streaming telemetry to the UI, to coordinating behavior between vehicles.
Architecture Overview
BeamNG.drive uses separate Lua VMs for different contexts:
| Context | VM | Purpose | Main Objects |
|---|---|---|---|
| GE | Game Engine | Global game state, UI, missions, level logic | be, extensions, guihooks |
| VE | Vehicle Engine | Per-vehicle physics, controllers, powertrain | obj, v, electrics |
| UI | Chromium CEF | HTML/JavaScript interface layer | JavaScript event handlers |
1. VE → GE: obj:queueGameEngineLua
Send Lua code from a vehicle's VE context to the Game Engine (GE).
-- VE Context
obj:queueGameEngineLua("core_gamestate.setVehicleSpecificName(" .. obj:getId() .. ", 'Hero')")Common Patterns:
-- Notify GE of vehicle state change
obj:queueGameEngineLua("extensions.hook('onVehicleCustomEvent', " .. obj:getId() .. ")")
-- Update mission progress
obj:queueGameEngineLua("gameplay_missions.onVehicleCheckpoint(" .. obj:getId() .. ")")
-- Spawn something from vehicle context
obj:queueGameEngineLua("be:createVehicle('pickup', pos, rot, color)")
-- Call a GE extension function with data
local data = {speed = electrics.values.wheelspeed, gear = electrics.values.gear}
obj:queueGameEngineLua("myExtension.onVehicleData(" .. obj:getId() .. ", " .. serialize(data) .. ")")Use for:
- Reporting vehicle events to gameplay systems
- Triggering level/mission logic from vehicle actions
- Requesting GE-level operations (spawning, scene changes)
- Sending vehicle telemetry to GE extensions
2. GE → VE: be:sendToVehicle / veh:queueLuaCommand
Send commands from GE to a specific vehicle's VE context.
Primary Method: veh:queueLuaCommand
-- GE Context
local veh = be:getObjectByID(vehId)
if veh then
veh:queueLuaCommand("electrics.values.hazard = 1")
endDirect Messaging: be:sendToVehicle
-- GE Context
be:sendToVehicle(vehId, "setTargetSpeed", 30) -- Tell vehicle to target 30 m/sNote: The VE must implement onExternalCommand or similar handlers to receive sendToVehicle messages.
Patterns:
-- Send data via JSON encoding
local data = jsonEncode({speed = 50, mode = "eco"})
veh:queueLuaCommand("myMod.receiveData('" .. data .. "')")
-- Trigger GE callback from VE result
veh:queueLuaCommand(string.format(
"partCondition.initConditions(nil, %d) obj:queueGameEngineLua('myExt.onFinished(%d)')",
condVal, vehId
))
-- Send to all vehicles
for i = 0, be:getObjectCount() - 1 do
be:getObject(i):queueLuaCommand("extensions.load('fuelMultiplier')")
endUse for:
- Triggering vehicle functions from gameplay/missions
- Setting electrics values remotely
- Activating vehicle features from UI callbacks
- Configuring vehicle controllers from GE
3. VE → VE (Peer Vehicles): obj:queueObjectLuaCommand
Vehicles can communicate directly with each other via their unique object IDs.
-- VE Context (Vehicle A)
local targetId = 1234 -- ID of Vehicle B
obj:queueObjectLuaCommand(targetId, "print('Message from Vehicle A')")
-- Send data to another vehicle
obj:queueObjectLuaCommand(targetId, "electrics.values.customSignal = 1")
-- Complex inter-vehicle communication
local message = {
type = "convoy_follow",
leaderId = obj:getId(),
speed = electrics.values.wheelspeed
}
obj:queueObjectLuaCommand(followerId, "convoySystem.receiveCommand(" .. serialize(message) .. ")")Use for:
- Convoy/platoon systems
- Vehicle-to-vehicle signaling
- Multi-vehicle coordinated behaviors
4. VE → UI: guihooks
The UI runs in a browser context (Chromium/CEF). Use guihooks to trigger events or stream data.
One-Time Events
-- VE Context
-- Trigger a one-time event
guihooks.trigger("VehicleAlert", {message = "Engine Overheating!", severity = "high"})
-- Common UI events
guihooks.trigger("AIStateChange") -- Refresh AI app state
guihooks.trigger("HydrosUpdate") -- Update UI with hydraulic states
guihooks.trigger("appMessage", {msg = "Custom message"})Streaming Data (High Frequency)
-- Stream high-frequency data (use in onGraphicsStep)
guihooks.queueStream("customSpeed", electrics.values.airspeed * 2.237) -- mph
guihooks.queueStream("engineTemp", electrics.values.watertemp)
-- Send all queued streams to UI (called automatically in onGraphicsStep)
guihooks.sendStreams()
-- Immediate stream send (bypass queue)
guihooks.triggerStream("emergency_telemetry", {status = "critical"})Notifications
-- Display UI notification toast
guihooks.message("Safety Alert!", 5, "safety", "warning")
-- Full notification table
guihooks.trigger("Notification", {
type = "warning", -- 'info', 'warning', 'error', 'success'
title = "Engine Overheat",
message = "Temperature exceeding safe limits!"
})Use for:
- Vehicle alerts and warnings
- Telemetry dashboards
- Real-time data visualization
- Player notifications
5. GE → UI: guihooks (GE Context)
Same API as VE → UI, but called from GE extensions.
-- GE Context
guihooks.trigger("MenuOpenModule", "vehicleselect")
guihooks.trigger("MissionStateChanged", {
active = true,
missionId = "delivery_01",
progress = 0.5
})
-- Request/Response pattern
guihooks.triggerRequest("GetPlayerData", {}, function(data)
print(dump(data))
end)See BeamNGGE/references/guihooks.md for complete GE → UI reference.
6. Extension Hooks: Cross-Module Communication
Extensions communicate via the hook system. Hooks chain across all loaded extensions.
VE Extension Hooks
-- In your VE extension (lua/vehicle/extensions/myExt.lua)
local M = {}
function M.onInit()
-- Your init logic
end
function M.onUpdate(dt)
-- Called every frame
end
function M.onReset()
-- Reset state
end
-- Custom hook for inter-extension communication
function M.onCustomEvent(data)
-- Handle event from other extensions
end
return MTriggering Hooks (Both GE and VE)
-- Call a hook on all loaded extensions
extensions.hook("onReset")
extensions.hook("onVehicleSpawned", vehID)
extensions.hook("onCustomEvent", data)
-- VE-only: Check if hook exists
if extensions.hookExists("onMyHook") then
extensions.hook("onMyHook", data)
endAvailable VE Lifecycle Hooks
| Hook | Timing | Use For |
|---|---|---|
onInit | Extension load | One-time setup |
onUpdate(dt) | Every frame | Continuous updates |
onReset() | Vehicle reset | Clean/reset state |
onDespawn() | Vehicle removed | Final cleanup |
See extensions.md for VE extension system details.
7. The electrics Bus (VE Internal Shared Memory)
Within the same vehicle VM, the electrics module acts as a global blackboard. Use it to share data between controllers and extensions without direct coupling.
-- Controller A sets a value
electrics.values.myMod_value = 1.0
-- Extension B reads it
local val = electrics.values.myMod_value
-- Standard electrics values
electrics.values.rpm = 3000
electrics.values.wheelspeed = 25.5
electrics.values.lights = 1Best Practice: Namespace your custom electrics: myMod_value not value
Communication Matrix
| From | To | Method | Syntax |
|---|---|---|---|
| VE | GE | C++ Bridge | obj:queueGameEngineLua("code") |
| GE | VE | C++ Bridge | veh:queueLuaCommand("code") |
| GE | VE | Direct Message | be:sendToVehicle(id, ...) |
| VE | VE | Peer-to-Peer | obj:queueObjectLuaCommand(id, "code") |
| VE | UI | Event | guihooks.trigger("event", data) |
| VE | UI | Stream | guihooks.queueStream("key", value) |
| GE | UI | Event | guihooks.trigger("event", data) |
| VE | VE (Internal) | Data Bus | electrics.values.key = value |
| GE | GE | Extension Hook | extensions.hook("event", ...) |
| VE | VE | Extension Hook | extensions.hook("event", ...) |
Bidirectional Patterns
Pattern: GE ↔ Vehicle State Sync
-- VE Extension (lua/vehicle/extensions/stateReporter.lua)
local M = {}
function M.onUpdate(dt)
-- Report state to GE periodically
if dt > 1.0 then -- Throttle to 1Hz
local state = {
rpm = electrics.values.rpm,
speed = electrics.values.wheelspeed
}
obj:queueGameEngineLua("vehicleStateExt.update(" .. obj:getId() .. ", " .. jsonEncode(state) .. ")")
end
end
return M-- GE Extension (lua/ge/extensions/vehicleStateExt.lua)
local M = {}
M.vehicleData = {}
function M.update(vehID, state)
M.vehicleData[vehID] = state
-- Notify UI
guihooks.trigger("VehicleStateUpdated", {id = vehID, state = state})
end
function M.setVehicleConfig(vehID, config)
local veh = be:getObjectByID(vehID)
if veh then
veh:queueLuaCommand("extensions.load('" .. config.extension .. "')")
end
end
return MPattern: UI ↔ Vehicle Control Bridge
-- GE Extension bridging UI and Vehicle
local M = {}
-- Called from UI via guihooks
guihooks.trigger("SetVehicleLights", {vehId = 123, state = 1})
function M.onSetVehicleLights(data)
local veh = be:getObjectByID(data.vehId)
if veh then
veh:queueLuaCommand("electrics.setLightsState(" .. data.state .. ")")
end
end
return MBest Practices
1. Always Validate Objects
-- GE → VE
local veh = be:getObjectByID(vehID)
if veh then -- Critical check!
veh:queueLuaCommand("...")
end
-- VE → GE (obj always valid in VE context)
obj:queueGameEngineLua("...") -- Safe2. Keep Commands Simple
-- GOOD: Simple, single-purpose command
veh:queueLuaCommand("electrics.values.hazard = 1")
-- AVOID: Complex logic in strings
-- veh:queueLuaCommand("if electrics.values.speed > 10 then ... end")3. Use Hooks for Cross-Extension Comms
-- GOOD: Hook pattern
extensions.hook("onMyCustomEvent", data)
-- AVOID: Direct calls that may break if extension unloaded
-- myOtherExtension.func(data)4. Escape Data Properly
-- Use jsonEncode for complex data
local data = jsonEncode({speed = 50, gear = "D"})
veh:queueLuaCommand("myController.receiveData('" .. data .. "')")
-- In VE sending to GE
local result = {rpm = electrics.values.rpm}
obj:queueGameEngineLua("myGEExt.onData(" .. obj:getId() .. ", '" .. jsonEncode(result) .. "')")5. Sync UI After State Changes (No Ghost Values!)
-- VE: After modifying state, notify UI
myMod.state.active = true
guihooks.trigger("MyModStateChanged", myMod.state)
-- GE: Same pattern
M.updateState = function(newState)
M.state = newState
guihooks.trigger("MyGEStateChanged", M.state)
end6. Throttle High-Frequency Communication
-- Don't send every physics frame
local timer = 0
function M.onUpdate(dt)
timer = timer + dt
if timer > 0.1 then -- 10Hz max
timer = 0
obj:queueGameEngineLua("...")
end
endSee Also
- guihooks.md - Complete UI communication reference (VE)
- obj.md -
objbinding including queue methods - extensions.md - VE extension system
- electrics.md - Internal data bus
- BeamNGGE/references/communication.md - GE-side communication
- BeamNGGE/references/guihooks.md - GE UI communication
- BeamNGGE/references/be.md -
beengine binding - BeamNGGE/references/extensions.md - GE extension system
How Mods Are Loaded
How BeamNG discovers, mounts, and activates mods — ZIP structure, virtual filesystem overlay, and the mod manager API.
Vehicle Boot Sequence & Lifecycle
The exact order in which vehicle systems initialize — from JBeam compilation to physics loops. Essential for timing your VE code correctly.