RLS Studios
ProjectsPatreonCommunityDocsAbout
Join Patreon
BeamNG Modding Docs

Guides

Reference

UI

UI Apps - HUD WidgetsBridge/API Layer - Lua ↔ UI CommunicationBeamNG UI Development Cheat SheetCommon Components - Bng* UI ElementsLua ↔ UI Integration - Full Round-Trip PatternsServices - Shared UtilitiesVue Modules - Full-Page Screens

Resources

BeamNG Game Engine Lua Cheat SheetGE Developer RecipesMCP Server Setup

// RLS.STUDIOS=true

Premium Mods for BeamNG.drive. Career systems, custom vehicles, and immersive gameplay experiences.

Index

HomeProjectsPatreon

Socials

DiscordPatreon (RLS)Patreon (Vehicles)

© 2026 RLS Studios. All rights reserved.

Modding since 2024

UI Framework

Bridge/API Layer - Lua ↔ UI Communication

The bridge layer connects JavaScript UI code to the Lua game engine. It provides event handling, stream subscriptions, and typed Lua function calls.

The bridge layer connects JavaScript UI code to the Lua game engine. It provides event handling, stream subscriptions, and typed Lua function calls.


Overview

The bridge provides:

  • Events - Fire-and-forget messages from Lua to UI
  • Streams - High-frequency data channels (electrics, sensors, etc.)
  • Lua calls - Invoke Lua functions from JavaScript
  • Hooks - Global event dispatch system

Using the Bridge

Import Methods

// Method 1: useBridge composable
import { useBridge } from '@/bridge'
const { lua, api, events, streams } = useBridge()

// Method 2: Direct imports
import { lua } from '@/bridge'

// Method 3: Via useLibStore (in apps)
import { useLibStore } from '@/services'
const { $game } = useLibStore()
// $game.lua, $game.events, $game.streams, $game.api

Events

Events are one-way messages from Lua → UI.

Listening for Events

import { useEvents } from '@/services/events'

// Auto-cleanup on component unmount
const events = useEvents()

events.on('MyEvent', (data) => {
  console.log('Received:', data)
})

events.once('OneTimeEvent', (data) => {
  // Only fires once
})

// Manual cleanup (if needed)
events.off('MyEvent', handler)

Common Events

EventDataDescription
VehicleChange-Player vehicle changed
VehicleFocusChanged{mode}Camera focus changed
ChangeState{state, params}Navigate to UI state
Message{category, text, icon, ttl}Show HUD message
toastrMsg{type, title, msg}Toast notification
MenuHide-Hide menu overlay
ClearTasklist-Clear task list

Emitting Events (UI → UI)

events.emit('MyCustomEvent', { data: 'value' })

Streams

Streams are high-frequency data channels, typically updated every frame.

Using useStreams Composable

import { useStreams } from '@/services/events'

// Subscribe to streams (auto-cleanup on unmount)
useStreams(['electrics', 'sensors'], (streams) => {
  if (streams.electrics) {
    const speed = streams.electrics.wheelspeed
    const rpm = streams.electrics.rpm
  }
  if (streams.sensors) {
    const yaw = streams.sensors.yaw
  }
})

Manual Stream Management

import { useBridge } from '@/bridge'
const { streams, events } = useBridge()

// Subscribe
streams.add(['electrics', 'engineInfo'])

// Listen for updates
events.on('onStreamsUpdate', (data) => {
  console.log('Speed:', data.electrics?.wheelspeed)
})

// Unsubscribe
streams.remove(['electrics', 'engineInfo'])

Common Streams

StreamContentsUpdate Rate
electricsSpeed, RPM, gear, lights, fuelEvery frame
sensorsYaw, pitch, roll, g-forcesEvery frame
engineInfoMax RPM, idle RPM, shift pointsOn vehicle load
damageDataPart damage valuesOn damage
minimapLocation name, distance to targetVaries

Stream Data Examples

// electrics stream
{
  wheelspeed: 15.5,       // m/s
  rpm: 3500,
  gear: 3,
  gearIndex: 4,           // Including neutral/reverse
  fuel: 0.75,
  lowbeam: true,
  highbeam: false,
  signal_L: false,
  signal_R: false,
  hazard: false,
  ignitionLevel: 2,       // 0=off, 1=acc, 2=on
  parkingbrake: 0,
  abs: false,
  esc: false
}

// sensors stream
{
  yaw: 1.57,              // radians
  pitch: 0.02,
  roll: 0.01,
  gravity: { x: 0, y: 0, z: -9.81 },
  gx: 0.1,                // G-forces
  gy: 0.0,
  gz: 1.0
}

// engineInfo stream
{
  maxRPM: 7000,
  idleRPM: 800,
  shiftUpRPM: 6500,
  shiftDownRPM: 2000
}

Calling Lua Functions

Using Typed Signatures

import { lua } from '@/bridge'

// Call extension function (returns Promise)
const result = await lua.extensions.myExtension.myFunction(arg1, arg2)

// Call career module
await lua.career_modules_fuel.requestRefuelingTransactionData()

// Call core function
const details = await lua.core_vehicles.getCurrentVehicleDetails()

// Check extension state
const isActive = await lua.career_career.isActive()

Function Signature Types

Signatures in LuaFunctionSignatures.js define parameter types:

// bridge/LuaFunctionSignatures.js
export default {
  career_career: {
    isActive: () => {},                              // No params
    createOrLoadCareerAndStart: (id, auto, tut) =>  // Typed params
      [String, Any, Boolean]
  },
  
  guihooks: {
    trigger: hookName => String                      // String param
  },
  
  core_vehicles: {
    getCurrentVehicleDetails: () => {}               // No params, returns object
  }
}

Raw Lua Execution

For functions not in signatures:

import { runRaw } from '@/bridge/libs/Lua.js'

// Execute arbitrary Lua code
const result = await runRaw('extensions.myMod.customFunction("arg")')

// Without promise (fire-and-forget)
runRaw('print("Hello from JS")', false)

Using API for Vehicle Lua

import { useBridge } from '@/bridge'
const { api } = useBridge()

// Execute on active vehicle (VE context)
api.activeObjectLua('electrics.setIgnitionLevel(2)')

// Execute on game engine
api.engineLua('print("Hello")', (result) => {
  console.log('Result:', result)
})

Hooks System

The Hooks module handles global event dispatch.

How Hooks Work

  1. Lua triggers hook - guihooks.trigger('EventName', data)
  2. CEF receives - window.multihookUpdate(hooksData)
  3. Hooks dispatches - To both Angular and Vue event buses
  4. Components receive - Via events.on('EventName', handler)

Internal Hook Handler

// bridge/libs/Hooks.js
const mainHookHandler = function(hookName, args) {
  // Dispatch to Vue event bus
  window.vueEventBus?.emit(hookName, ...args)
  
  // Dispatch to Angular (legacy)
  window.globalAngularRootScope?.$broadcast(hookName, ...args)
}

Stream Update Flow

  1. Lua queues stream data - guihooks.queueStream('electrics', data)
  2. Lua flushes - guihooks.sendStreams() (typically in updateGFX)
  3. CEF receives - window.streamUpdate(data)
  4. StreamManager processes - Merges vehicle/global streams
  5. Event emitted - vueEventBus.emit('onStreamsUpdate', data)
  6. Coordinator syncs - Waits for Vue/Angular updates, signals completion

Stream Coordinator

The coordinator ensures UI updates complete before signaling the game engine.

// bridge/coordinator/lite.js

// Before broadcasting streams
coordinator.beforeBroadcast()

// Emit stream update
vueEventBus.emit('onStreamsUpdate', data)

// After broadcast - wait for Vue.nextTick and Angular $timeout
coordinator.afterBroadcast(() => {
  // Signal game engine that UI frame is complete
  window.beamng?.uiFrameCallback?.()
})

Coordinator Features

  • Vue sync - Waits for Vue.nextTick()
  • Angular sync - Waits for $timeout(0)
  • Safety timeout - Forces completion after 2000ms
  • Skip detection - Handles overlapping updates

Game Blurrer

Region-based blur system for UI elements. Each blur region is tracked by ID and synced to Lua.

import { gameBlurrer } from '@/bridge'

// Register a blur region (returns an ID string)
const id = gameBlurrer.register({ x: 0, y: 0, width: 100, height: 100 })

// Update a registered blur region
gameBlurrer.update(id, { x: 10, y: 10, width: 200, height: 200 })

// Unregister a blur region
gameBlurrer.unregister(id)

The blurrer lazily loads/unloads the ui_gameBlur Lua extension as needed. When the region list is empty, it unloads the extension automatically.


UI Units

Handle unit conversions (metric/imperial). The UIUnits class manages settings for length, speed, temperature, weight, torque, power, volume, pressure, etc.

import { useBridge } from '@/bridge'
const { units } = useBridge()

// Unit settings are keyed by category
// e.g. 'uiUnitLength', 'uiUnitTemperature', 'uiUnitWeight', etc.
// Categories: length, speed, temperature, weight, consumptionRate,
//             torque, energy, date, power, volume, pressure

// The units object provides conversion methods per category
// Settings can be 'metric'/'imperial' or specific unit strings like 'bhp', 'psi', 'gal'

Common Patterns

Fetch Data on Mount

import { onMounted, ref } from 'vue'
import { lua } from '@/bridge'

const data = ref(null)

onMounted(async () => {
  data.value = await lua.myExtension.getData()
})

React to Events

import { ref, onMounted, onUnmounted } from 'vue'
import { useBridge } from '@/bridge'

const { events } = useBridge()
const isActive = ref(false)

function onStateChange(data) {
  isActive.value = data.active
}

onMounted(() => {
  events.on('MyStateChanged', onStateChange)
})

onUnmounted(() => {
  events.off('MyStateChanged', onStateChange)
})

Stream + Event Combo

import { ref, onMounted, onUnmounted } from 'vue'
import { useLibStore } from '@/services'

const { $game } = useLibStore()
const speed = ref(0)
const vehicleName = ref('')

onMounted(() => {
  // Stream for continuous data
  $game.streams.add(['electrics'])
  $game.events.on('onStreamsUpdate', onStreams)
  
  // Event for discrete changes
  $game.events.on('VehicleChange', onVehicleChange)
})

onUnmounted(() => {
  $game.streams.remove(['electrics'])
  $game.events.off('onStreamsUpdate', onStreams)
  $game.events.off('VehicleChange', onVehicleChange)
})

function onStreams(streams) {
  speed.value = streams.electrics?.wheelspeed || 0
}

async function onVehicleChange() {
  const details = await $game.lua.core_vehicles.getCurrentVehicleDetails()
  vehicleName.value = details?.configs?.Name || 'Unknown'
}

Gotchas

  1. Lua calls return Promises - Always use await or .then()
  2. Stream data may be undefined - Check before accessing properties
  3. Event names are case-sensitive - Match Lua guihooks.trigger exactly
  4. Clean up subscriptions - Memory leaks if you don't
  5. Streams are reference-counted - Multiple subscribers OK, but each must unsubscribe

See Also

  • GUIHooks Reference - Lua side of communication
  • Services - useEvents, useStreams wrappers
  • Lua Integration - Full round-trip patterns

UI Apps - HUD Widgets

UI Apps are in-game HUD widgets like the tachometer, compass, minimap, and lap times. They run as Vue components overlaid on the game view.

BeamNG UI Development Cheat Sheet

40+ high-frequency one-liners and snippets for UI development.

On this page

OverviewUsing the BridgeImport MethodsEventsListening for EventsCommon EventsEmitting Events (UI → UI)StreamsUsing useStreams ComposableManual Stream ManagementCommon StreamsStream Data ExamplesCalling Lua FunctionsUsing Typed SignaturesFunction Signature TypesRaw Lua ExecutionUsing API for Vehicle LuaHooks SystemHow Hooks WorkInternal Hook HandlerStream Update FlowStream CoordinatorCoordinator FeaturesGame BlurrerUI UnitsCommon PatternsFetch Data on MountReact to EventsStream + Event ComboGotchasSee Also