GE Cross-VM Communication
How GE communicates with Vehicle Engines and the UI — bridge methods, hooks, and patterns for cross-context data flow.
BeamNG runs separate Lua VMs for the game engine and each vehicle. They can't share variables directly — all communication happens through bridge methods. Getting this right is essential for any mod that needs to read vehicle data, control vehicles from gameplay code, or update the UI.
Communication Overview
BeamNG.drive uses separate Lua VMs for different contexts:
| Context | Purpose | Main Objects |
|---|---|---|
| GE | Global game state, UI, missions, level logic | be, extensions, guihooks |
| VE | Per-vehicle physics, controllers, powertrain | obj, v, electrics |
| UI | HTML/JavaScript interface | Chromium (CEF) layer |
1. GE to Vehicle (VE): be Methods
veh:queueLuaCommand(code) - Primary Method
Send Lua code from GE to a specific vehicle's VE context.
-- GE Context
local veh = be:getObjectByID(vehId)
if veh then
veh:queueLuaCommand("electrics.values.hazard = 1")
endUse for:
- Triggering vehicle functions from gameplay/missions
- Setting electrics values remotely
- Activating vehicle features from UI callbacks
be:queueAllObjectLua(code) - Broadcast to All Vehicles
Send Lua code to all vehicles' VE contexts at once.
-- GE Context
be:queueAllObjectLua("obj:setGravity(-9.81)")2. Vehicle (VE) to GE: obj:queueGameEngineLua
From within a vehicle's Lua context, execute code in the GE context.
-- VE Context
obj:queueGameEngineLua("core_gamestate.setVehicleSpecificName(" .. obj:getId() .. ", 'Hero Car')")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() .. ")")
-- Trigger a GE extension hook from vehicle context
obj:queueGameEngineLua("extensions.hook('onMyVehicleEvent', " .. obj:getId() .. ")")Use for:
- Reporting vehicle events to gameplay systems
- Triggering level/mission logic from vehicle actions
- Requesting GE-level operations (spawning, scene changes)
3. GE to UI: guihooks
Bridge between GE Lua and the HTML/JavaScript UI layer.
Event Triggering
-- Send one-way event to UI
guihooks.trigger("MenuOpenModule", "vehicleselect")
-- Send with data payload
guihooks.trigger("MissionStateChanged", {
active = true,
missionId = "delivery_01",
progress = 0.5
})Streaming Data to UI
-- High-frequency data via streams (batched, called in onUpdate)
guihooks.queueStream('vehicleSpeed', speedData)
-- Streams are flushed automatically in main.lua render loop
-- Immediate stream send
guihooks.triggerStream('missionData', data)
-- Simple message popup
guihooks.message("Hello!", 5, "info")Common UI Events
| Event | Data | Purpose |
|---|---|---|
MenuOpenModule | moduleName (string) | Open a UI module |
MenuHide | nil | Hide menu |
ChangeState | {state = 'menu'} | Change game state |
Notification | {type, title, message} | Show toast notification |
ScenarioRewinded | nil | Reset scenario UI state |
UpdateMissionProgress | {id, progress} | Update mission UI |
4. Extension Hooks: Cross-Module Communication
Extensions communicate via the hook system. Hooks chain across all loaded extensions.
Triggering Hooks
-- Call a hook on all loaded extensions
extensions.hook("onResetGameplay", playerID)
extensions.hook("onVehicleSpawned", vehID)
extensions.hook("onCustomEvent", data)Handling Hooks
-- In your GE extension (lua/ge/extensions/myExtension.lua)
local M = {}
local function onVehicleSpawned(vehID)
-- React to vehicle spawn
end
local function onResetGameplay(playerID)
-- Clean up state
end
local function onMissionStarted(missionId)
-- Other extensions can listen for this
end
M.onVehicleSpawned = onVehicleSpawned
M.onResetGameplay = onResetGameplay
M.onMissionStarted = onMissionStarted
return MAvailable GE Lifecycle Hooks
| Hook | Timing | Use For |
|---|---|---|
onExtensionLoaded | Extension load | Setup, registration |
onClientPostStartMission(levelPath) | Level objects available | Level-specific init |
onWorldReadyState(state) | World ready (2=loaded) | Post-spawn setup |
onUpdate(dtReal, dtSim, dtRaw) | Every frame | Game logic, timers |
onPreRender(dtReal, dtSim, dtRaw) | Every frame (after physics) | Visual updates |
onVehicleSpawned(vid, vehObj) | Vehicle added | Track vehicles |
onVehicleDestroyed(vid) | Vehicle removed | Cleanup |
onResetGameplay(playerID) | Gameplay reset | Clean state |
onClientEndMission(levelPath) | Leaving level | Level cleanup |
5. Bidirectional Patterns
Pattern: GE ↔ Vehicle State Sync
-- GE Extension (lua/ge/extensions/vehicleManager.lua)
local M = {}
local vehicleData = {}
local function onVehicleSpawned(vehID)
vehicleData[vehID] = { spawnTime = os.time() }
local veh = be:getObjectByID(vehID)
if veh then
veh:queueLuaCommand("extensions.load('customController')")
end
end
local function updateVehicleState(vehID, state)
if vehicleData[vehID] then
vehicleData[vehID].state = state
guihooks.trigger("VehicleStateUpdated", {id = vehID, state = state})
end
end
M.onVehicleSpawned = onVehicleSpawned
M.updateVehicleState = updateVehicleState
return MPattern: UI ↔ Vehicle Control
-- GE Extension bridging UI and Vehicle
local M = {}
local function setVehicleLights(vehID, state)
local veh = be:getObjectByID(vehID)
if veh then
veh:queueLuaCommand("electrics.setLightsState(" .. state .. ")")
end
end
local function getVehicleInfo(vehID)
local veh = be:getObjectByID(vehID)
if not veh then return nil end
return {
id = vehID,
position = veh:getPosition(),
name = veh:getJBeamFilename()
}
end
M.setVehicleLights = setVehicleLights
M.getVehicleInfo = getVehicleInfo
return M6. The electrics Bus (VE Internal)
While GE cannot directly access VE's electrics, it can read/write via commands:
-- GE reading vehicle state (via command)
local veh = be:getObjectByID(vehID)
if veh then
-- This sets a value; reading requires callback pattern
veh:queueLuaCommand([[
obj:queueGameEngineLua("myExtension.onElectricsUpdate(" .. obj:getId() .. ", " .. (electrics.values.rpm or 0) .. ")")
]])
endCommunication Matrix
| From | To | Method | Syntax |
|---|---|---|---|
| GE | VE | C++ Bridge | veh:queueLuaCommand("code") |
| GE | All VEs | Broadcast | be:queueAllObjectLua("code") |
| VE | GE | C++ Bridge | obj:queueGameEngineLua("code") |
| GE | UI | Event | guihooks.trigger("event", data) |
| GE | UI | Stream | guihooks.queueStream("key", data) |
| GE | GE | Extension Hook | extensions.hook("event", ...) |
Best Practices
1. Always Validate Objects
local veh = be:getObjectByID(vehID)
if veh then -- Critical check!
veh:queueLuaCommand("...")
end2. 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 .. "')")5. Sync UI After State Changes
-- No ghost values rule
myMod.state.active = true
myMod.saveState()
guihooks.trigger("MyModStateChanged", myMod.state)See Also
- UI Bridge - Lua to UI communication details
- GUI Hooks - UI event reference
- Vehicle Control - Vehicle operations
- Architecture - Extension system reference