Lua Modding FAQ
Quick answers to the most common BeamNG Lua modding questions — extension structure, data persistence, UI communication, timers, and more.
Quick answers to questions that come up constantly in BeamNG modding. Each answer includes a code example you can copy directly.
How do I structure an extension?
Return a plain table M with dot-notation functions. No classes, no metatables, no self, no colon syntax.
local M = {}
local function onExtensionLoaded()
log('I', 'myMod', 'Extension loaded')
end
local function doThing(value)
-- logic here
end
M.onExtensionLoaded = onExtensionLoaded
M.doThing = doThing
return MNever write function M:doThing() or M.doThing = function() inline. Use local function then export at the bottom.
How do I persist data across level changes?
Register your extension in your mod script (scripts/mymod/modScript.lua) with setExtensionUnloadMode("mymod_feature", "manual") and call loadManualUnloadExtensions(). Local variables in your extension file persist as long as the extension stays loaded.
-- scripts/mymod/modScript.lua
setExtensionUnloadMode("mymod_feature", "manual")
loadManualUnloadExtensions()Do NOT put setExtensionUnloadMode inside the extension itself.
How do I communicate between extensions?
Broadcast to all extensions via hooks:
extensions.hook('mymod_somethingHappened', data)Call another extension directly:
if other_extension then
other_extension.doThing()
endHow do I send data to the UI?
Use guihooks.trigger() to push data from Lua to the UI layer:
guihooks.trigger('MyModDataUpdate', { score = 100, level = 3 })How do I call Lua from the UI?
Use bngApi.engineLua() in your JavaScript/Angular code:
bngApi.engineLua("extensions.mymod_feature.doThing()")To pass data:
bngApi.engineLua('extensions.mymod_feature.setValue(' + bngApi.serializeToLua(value) + ')')How do I save and load data?
Use jsonWriteFile() and jsonReadFile():
-- Save
local data = { highScore = 500, playerName = "Driver" }
jsonWriteFile("settings/mymod/save.json", data)
-- Load
local data = jsonReadFile("settings/mymod/save.json") or {}How do I find vehicles?
Get the player vehicle:
local veh = be:getPlayerVehicle(0)Get a vehicle by ID:
local veh = be:getObjectByID(id)How do I log messages?
Use log() with a level, tag, and message:
log('I', 'myMod', 'Something happened') -- Info
log('W', 'myMod', 'This looks wrong') -- Warning
log('E', 'myMod', 'Something broke') -- Error
log('D', 'myMod', 'Debug details here') -- DebugHow do I use timers?
There is no setTimeout equivalent. Use the onUpdate hook with an accumulator:
local M = {}
local timer = 0
local function onUpdate(dtReal, dtSim, dtRaw)
timer = timer + dtReal
if timer >= 5.0 then
timer = 0
-- do something every 5 seconds
end
end
M.onUpdate = onUpdate
return MShould I use classes?
Usually no — but sometimes yes. Extensions themselves should always be plain M tables. But within an extension, classes are the right tool when you need:
- Multiple instances of the same thing (e.g. managing several parcels, routes, or NPCs)
- Encapsulated state that travels together (data + methods as one unit)
- Shared behavior across instances via metatables
When NOT to use classes
For extensions, singletons, and one-off modules — just use the standard M table pattern:
local M = {}
local function functionName(args)
-- implementation
end
M.functionName = functionName
return MWhen classes make sense
When you're creating multiple instances that each hold their own state:
local M = {}
-- Class definition
local Parcel = {}
Parcel.__index = Parcel
function Parcel.new(origin, destination, weight)
return setmetatable({
origin = origin,
destination = destination,
weight = weight,
delivered = false,
}, Parcel)
end
function Parcel:markDelivered()
self.delivered = true
end
function Parcel:getInfo()
return string.format("%s → %s (%dkg)", self.origin, self.destination, self.weight)
end
-- Extension uses the class internally
local activeParcels = {}
local function createParcel(origin, dest, weight)
local p = Parcel.new(origin, dest, weight)
table.insert(activeParcels, p)
return p
end
M.createParcel = createParcel
return MThe key distinction: the extension is still a plain M table — classes live inside it for managing instances. Don't make the extension itself a class.
👉 Full guide: Lua Classes — constructors, inheritance, patterns, and real BeamNG examples.
What is the difference between GE and VE?
- GE (Game Engine) runs globally. It manages the world, UI, scenarios, and all vehicles. Extensions live in
lua/ge/extensions/. - VE (Vehicle Engine) runs per-vehicle. It handles physics, powertrain, electrics, and vehicle-specific logic. Extensions live in
lua/vehicle/extensions/.
They communicate across the boundary:
-- GE to VE
local veh = be:getPlayerVehicle(0)
veh:queueLuaCommand("myVehicleExtension.doThing()")
-- VE to GE
obj:queueGameEngineLua("extensions.mymod_feature.onVehicleData('" .. data .. "')")How do I run code on a specific vehicle?
Use queueLuaCommand() on the vehicle object:
local veh = be:getPlayerVehicle(0)
veh:queueLuaCommand("myVehicleExtension.doThing()")Or by vehicle ID:
local veh = be:getObjectByID(vehicleId)
veh:queueLuaCommand("electrics.values.headlights = 1")See Also
- Getting Started - First mod walkthrough
- Creating Extensions - Full extension reference
- Mod Scripts - Extension persistence
- Hook Catalog - All available hooks
- Common Patterns - Proven patterns
- Anti-Patterns - Mistakes to avoid
- Lua Classes - Metatables, constructors, and inheritance
Performance Optimization
How to keep your BeamNG Lua mod fast — avoiding GC pressure, caching lookups, efficient string handling, and proper timer patterns.
Creating HUD Apps
How to create UI apps (HUD widgets) for BeamNG.drive — directory structure, AngularJS directives, receiving Lua data, and styling.