TutorialsBeginnerLua Scripting for FiveM

Lua Scripting for FiveM 2025 - Complete Programming Guide

⏱️ Estimated Time: 45-60 minutes | 🎯 Difficulty: Beginner to Intermediate | 💻 Hands-On Learning

Master Lua programming for FiveM development with this comprehensive guide. Learn everything from basic Lua syntax to advanced FiveM-specific patterns, QBCore integration, and performance optimization techniques.

What You’ll Master: Lua fundamentals, FiveM natives, client-server architecture, event systems, database integration, and QBCore-specific patterns for professional FiveM development.

Why Lua for FiveM Development?

Lua was chosen as FiveM’s scripting language for several key reasons:

Advantages of Lua in FiveM

FeatureBenefitFiveM Application
LightweightFast execution, low memory usageSmooth gameplay with many resources
EmbeddableEasy integration with C++ engineDirect access to GTA V functions
Simple SyntaxEasy to learn and readFaster development and debugging
FlexibleDynamic typing and metaprogrammingAdaptable to different server needs
CoroutinesBuilt-in threading supportHandle multiple operations efficiently

FiveM’s Lua Environment

FiveM provides several enhancements to standard Lua:

  • Native Functions: Direct access to GTA V game functions
  • Event System: Client-server communication framework
  • Coroutine Management: Automatic thread handling
  • JSON Support: Built-in JSON encoding/decoding
  • HTTP Client: Web request capabilities
  • File System Access: Read/write server files

Lua Fundamentals for FiveM

1. Basic Syntax and Variables

-- Comments in Lua start with double dashes
-- This is a single line comment
 
--[[
This is a multi-line comment
Very useful for documentation
--]]
 
-- Variables (no need to declare type)
local playerName = "John Doe"           -- String
local playerMoney = 5000                -- Number
local isAdmin = true                    -- Boolean
local playerData = nil                  -- Nil (empty value)
 
-- Numbers can be integers or floats
local playerId = 1                      -- Integer
local playerHealth = 100.0              -- Float
local temperature = -5.5                -- Negative float
 
-- Strings
local message = "Hello World"
local longMessage = [[
This is a multi-line string
Very useful for HTML or SQL
]]
local formattedMessage = string.format("Player %s has $%d", playerName, playerMoney)
 
print(formattedMessage) -- Output: Player John Doe has $5000

2. Tables (Arrays and Objects)

Tables are Lua’s primary data structure - they work as both arrays and objects:

-- Array-like table (indexed from 1, not 0!)
local weapons = {"pistol", "rifle", "shotgun"}
print(weapons[1]) -- Output: pistol (NOT weapons[0])
 
-- Object-like table (key-value pairs)
local player = {
    name = "John",
    money = 5000,
    job = "police",
    position = {x = 100.0, y = 200.0, z = 30.0}
}
 
-- Accessing table values
print(player.name)      -- Output: John
print(player["money"])  -- Output: 5000 (alternative syntax)
 
-- Adding new values
player.level = 5
player["experience"] = 1500
 
-- Nested table access
print(player.position.x) -- Output: 100.0
 
-- Table with mixed content
local mixedTable = {
    "first item",           -- Index 1
    "second item",          -- Index 2
    name = "Mixed Table",   -- Key "name"
    count = 42              -- Key "count"
}
 
print(mixedTable[1])        -- Output: first item
print(mixedTable.name)      -- Output: Mixed Table

3. Functions

Functions are first-class values in Lua and essential for FiveM development:

-- Basic function declaration
function greetPlayer(playerName)
    print("Hello, " .. playerName .. "!")
    return "Welcome to the server"
end
 
-- Function with multiple parameters and return values
function calculateDistance(x1, y1, x2, y2)
    local dx = x2 - x1
    local dy = y2 - y1
    local distance = math.sqrt(dx * dx + dy * dy)
    return distance, dx, dy  -- Multiple return values
end
 
-- Using the functions
greetPlayer("Alice")
local dist, deltaX, deltaY = calculateDistance(0, 0, 3, 4)
print("Distance: " .. dist) -- Output: Distance: 5.0
 
-- Anonymous functions (very common in FiveM)
local onPlayerJoin = function(playerId, playerName)
    print(playerName .. " joined the server!")
end
 
-- Functions can be stored in tables
local playerActions = {
    heal = function(playerId)
        -- Heal player logic
        print("Healing player " .. playerId)
    end,
    
    giveWeapon = function(playerId, weapon)
        -- Give weapon logic
        print("Giving " .. weapon .. " to player " .. playerId)
    end
}
 
-- Call functions from table
playerActions.heal(1)
playerActions.giveWeapon(1, "pistol")

4. Control Structures

-- If statements
local playerMoney = 1000
local itemPrice = 500
 
if playerMoney >= itemPrice then
    print("Player can afford the item")
    playerMoney = playerMoney - itemPrice
elseif playerMoney > 0 then
    print("Player doesn't have enough money")
else
    print("Player is broke!")
end
 
-- While loops
local countdown = 5
while countdown > 0 do
    print("Countdown: " .. countdown)
    countdown = countdown - 1
end
 
-- For loops (numeric)
for i = 1, 5 do
    print("Number: " .. i)
end
 
-- For loops (with step)
for i = 10, 1, -2 do  -- Start at 10, end at 1, step by -2
    print("Countdown: " .. i) -- Output: 10, 8, 6, 4, 2
end
 
-- For loops (iterate over tables)
local players = {"Alice", "Bob", "Charlie"}
 
-- Iterate over array values
for index, name in ipairs(players) do
    print(index .. ": " .. name)
end
 
-- Iterate over all key-value pairs
local playerData = {name = "Alice", money = 1000, job = "police"}
for key, value in pairs(playerData) do
    print(key .. " = " .. tostring(value))
end

5. Error Handling

-- Basic error handling with pcall (protected call)
local success, result = pcall(function()
    -- This might fail
    local data = JSON.decode(someJsonString)
    return data.playerName
end)
 
if success then
    print("Player name: " .. result)
else
    print("Error occurred: " .. result)
end
 
-- Custom error handling
function safeDivide(a, b)
    if b == 0 then
        error("Cannot divide by zero!")
    end
    return a / b
end
 
-- Using xpcall for more detailed error info
local function errorHandler(err)
    print("Error: " .. err)
    print("Stack trace: " .. debug.traceback())
end
 
local success, result = xpcall(function()
    return safeDivide(10, 0)
end, errorHandler)

FiveM-Specific Lua Concepts

1. Client vs Server Scripts

Understanding the difference between client and server scripts is crucial:

-- SERVER SCRIPT (server/main.lua)
-- Runs on the server, has access to all players
print("This runs on the SERVER")
 
-- Server can access all players
local players = GetPlayers()
for _, playerId in ipairs(players) do
    local playerName = GetPlayerName(playerId)
    print("Server sees player: " .. playerName)
end
 
-- Server events
RegisterServerEvent('myResource:serverEvent')
AddEventHandler('myResource:serverEvent', function(data)
    local source = source  -- Player who triggered the event
    print("Server received data from player " .. source .. ": " .. data)
end)
 
-- Trigger client event for specific player
TriggerClientEvent('myResource:clientEvent', playerId, "Hello from server!")
 
-- Trigger client event for all players
TriggerClientEvent('myResource:clientEvent', -1, "Hello everyone!")
-- CLIENT SCRIPT (client/main.lua)
-- Runs on each player's client, only sees local player
print("This runs on the CLIENT")
 
-- Client can only access local player
local playerId = PlayerId()
local playerPed = PlayerPedId()
local playerName = GetPlayerName(playerId)
 
print("Client script for player: " .. playerName)
 
-- Client events
RegisterNetEvent('myResource:clientEvent')
AddEventHandler('myResource:clientEvent', function(message)
    print("Client received: " .. message)
    
    -- Show notification to this player only
    SetNotificationTextEntry("STRING")
    AddTextComponentString(message)
    DrawNotification(false, false)
end)
 
-- Trigger server event
TriggerServerEvent('myResource:serverEvent', "Hello from client!")

2. FiveM Natives

FiveM provides access to hundreds of GTA V functions called “natives”:

-- Player and Ped natives
local playerId = PlayerId()                    -- Get local player ID
local playerPed = PlayerPedId()               -- Get local player's character
local playerName = GetPlayerName(playerId)     -- Get player name
 
-- Position natives
local x, y, z = table.unpack(GetEntityCoords(playerPed))
print(string.format("Player position: %.2f, %.2f, %.2f", x, y, z))
 
-- Set player position
SetEntityCoords(playerPed, 100.0, 200.0, 30.0, false, false, false, true)
 
-- Vehicle natives
local vehicle = GetVehiclePedIsIn(playerPed, false)
if vehicle ~= 0 then
    local speed = GetEntitySpeed(vehicle)
    local speedMph = speed * 2.236936  -- Convert m/s to mph
    print("Vehicle speed: " .. math.floor(speedMph) .. " mph")
    
    -- Vehicle modification
    SetVehicleColours(vehicle, 12, 12)  -- Set primary and secondary color
    SetVehicleNumberPlateText(vehicle, "QBCORE")
end
 
-- Weapon natives
GiveWeaponToPed(playerPed, GetHashKey("WEAPON_PISTOL"), 100, false, true)
SetPedCurrentWeaponVisible(playerPed, true, true, true, true)
 
-- UI natives
SetTextFont(4)
SetTextProportional(true)
SetTextScale(0.5, 0.5)
SetTextColour(255, 255, 255, 255)
SetTextEntry("STRING")
AddTextComponentString("Hello World!")
DrawText(0.1, 0.1)  -- Draw at 10% from left, 10% from top
 
-- Weather and time natives
SetWeatherTypeNowPersist("CLEAR")
NetworkOverrideClockTime(12, 0, 0)  -- Set time to 12:00:00

3. Threading and Coroutines

FiveM automatically manages coroutines for you:

-- CreateThread creates a new coroutine
CreateThread(function()
    while true do
        Wait(1000)  -- Wait 1 second (1000ms)
        print("This runs every second")
    end
end)
 
-- Multiple threads can run simultaneously
CreateThread(function()
    while true do
        Wait(5000)  -- Wait 5 seconds
        print("This runs every 5 seconds")
    end
end)
 
-- Thread with exit condition
CreateThread(function()
    local count = 0
    while count < 10 do
        Wait(1000)
        count = count + 1
        print("Count: " .. count)
    end
    print("Thread finished!")
end)
 
-- IMPORTANT: Always use Wait() in loops!
-- This is WRONG and will crash the server/client:
--[[
while true do
    -- No Wait() here will freeze the game!
    DoSomething()
end
--]]
 
-- This is CORRECT:
CreateThread(function()
    while true do
        Wait(0)  -- Minimum wait, allows other threads to run
        DoSomething()
    end
end)

4. Event System

Events are the backbone of FiveM client-server communication:

-- SERVER SIDE: Register and handle events
RegisterServerEvent('playerManager:updateMoney')
AddEventHandler('playerManager:updateMoney', function(amount)
    local source = source  -- Player who triggered event
    local player = QBCore.Functions.GetPlayer(source)
    
    if player then
        player.Functions.AddMoney('cash', amount)
        TriggerClientEvent('playerManager:moneyUpdated', source, player.PlayerData.money.cash)
    end
end)
 
-- SERVER SIDE: Trigger events for clients
RegisterCommand('heal', function(source, args, rawCommand)
    -- Heal specific player
    TriggerClientEvent('playerManager:healPlayer', source)
    
    -- Or heal all players
    TriggerClientEvent('playerManager:healPlayer', -1)
end, false)
 
-- CLIENT SIDE: Register and handle events
RegisterNetEvent('playerManager:moneyUpdated')
AddEventHandler('playerManager:moneyUpdated', function(newAmount)
    print("Your new money amount: $" .. newAmount)
    
    -- Update UI or show notification
    QBCore.Functions.Notify("Money updated: $" .. newAmount, "success")
end)
 
RegisterNetEvent('playerManager:healPlayer')
AddEventHandler('playerManager:healPlayer', function()
    local playerPed = PlayerPedId()
    SetEntityHealth(playerPed, GetEntityMaxHealth(playerPed))
    print("Player healed!")
end)
 
-- CLIENT SIDE: Trigger server events
RegisterCommand('requestmoney', function(source, args)
    local amount = tonumber(args[1]) or 100
    TriggerServerEvent('playerManager:updateMoney', amount)
end, false)

QBCore Lua Patterns

1. Getting QBCore Object

-- Standard QBCore initialization (CLIENT or SERVER)
local QBCore = exports['qb-core']:GetCoreObject()
 
-- Alternative method (older scripts)
local QBCore = nil
TriggerEvent('QBCore:GetObject', function(obj) QBCore = obj end)
 
-- Ensure QBCore is loaded before using
if not QBCore then
    print("Error: QBCore not found!")
    return
end

2. Player Management

-- SERVER SIDE: Working with players
RegisterServerEvent('myScript:doSomething')
AddEventHandler('myScript:doSomething', function()
    local src = source
    local Player = QBCore.Functions.GetPlayer(src)
    
    if not Player then
        print("Player not found!")
        return
    end
    
    -- Access player data
    local playerName = Player.PlayerData.charinfo.firstname .. " " .. Player.PlayerData.charinfo.lastname
    local playerJob = Player.PlayerData.job.name
    local playerMoney = Player.PlayerData.money.cash
    
    print(string.format("Player: %s | Job: %s | Money: $%d", playerName, playerJob, playerMoney))
    
    -- Modify player data
    Player.Functions.AddMoney('cash', 1000)
    Player.Functions.SetJob('police', 2)  -- Job name, grade
    Player.Functions.AddItem('water', 5)  -- Item name, amount
    
    -- Send notification
    TriggerClientEvent('QBCore:Notify', src, 'You received $1000!', 'success')
end)
 
-- CLIENT SIDE: Get local player data
local PlayerData = QBCore.Functions.GetPlayerData()
 
print("My character: " .. PlayerData.charinfo.firstname)
print("My job: " .. PlayerData.job.label)
print("My cash: $" .. PlayerData.money.cash)
 
-- Update local player data when it changes
RegisterNetEvent('QBCore:Player:SetPlayerData')
AddEventHandler('QBCore:Player:SetPlayerData', function(val)
    PlayerData = val
    print("Player data updated!")
end)

3. Job System Integration

-- SERVER SIDE: Job-specific functionality
RegisterServerEvent('police:arrestPlayer')
AddEventHandler('police:arrestPlayer', function(targetId)
    local src = source
    local Player = QBCore.Functions.GetPlayer(src)
    local TargetPlayer = QBCore.Functions.GetPlayer(targetId)
    
    if not Player or not TargetPlayer then return end
    
    -- Check if player is police
    if Player.PlayerData.job.name == 'police' and Player.PlayerData.job.onduty then
        -- Arrest logic
        TriggerClientEvent('police:arrestAnimation', targetId)
        TriggerClientEvent('QBCore:Notify', src, 'Player arrested!', 'success')
        TriggerClientEvent('QBCore:Notify', targetId, 'You have been arrested!', 'error')
    else
        TriggerClientEvent('QBCore:Notify', src, 'You are not on duty!', 'error')
    end
end)
 
-- CLIENT SIDE: Job-specific features
CreateThread(function()
    while true do
        Wait(1000)
        
        if PlayerData.job and PlayerData.job.name == 'police' and PlayerData.job.onduty then
            -- Police-only functionality
            local playerPed = PlayerPedId()
            local coords = GetEntityCoords(playerPed)
            
            -- Check for nearby criminals or create police blips
            -- This runs only for on-duty police officers
        end
    end
end)

4. Database Operations

-- SERVER SIDE: Database queries with QBCore
local QBCore = exports['qb-core']:GetCoreObject()
 
-- Simple query
RegisterServerEvent('myScript:savePlayerData')
AddEventHandler('myScript:savePlayerData', function(data)
    local src = source
    local Player = QBCore.Functions.GetPlayer(src)
    
    if not Player then return end
    
    local citizenid = Player.PlayerData.citizenid
    
    -- Insert new record
    MySQL.insert('INSERT INTO my_table (citizenid, data) VALUES (?, ?)', {
        citizenid,
        json.encode(data)
    })
    
    -- Update existing record
    MySQL.update('UPDATE my_table SET data = ? WHERE citizenid = ?', {
        json.encode(data),
        citizenid
    })
    
    -- Query with callback
    MySQL.query('SELECT * FROM my_table WHERE citizenid = ?', {citizenid}, function(result)
        if result[1] then
            local savedData = json.decode(result[1].data)
            TriggerClientEvent('myScript:dataLoaded', src, savedData)
        end
    end)
end)
 
-- Async query (modern approach)
RegisterServerEvent('myScript:getPlayerStats')
AddEventHandler('myScript:getPlayerStats', function()
    local src = source
    local Player = QBCore.Functions.GetPlayer(src)
    if not Player then return end
    
    CreateThread(function()
        local citizenid = Player.PlayerData.citizenid
        local result = MySQL.query.await('SELECT * FROM player_stats WHERE citizenid = ?', {citizenid})
        
        if result[1] then
            TriggerClientEvent('myScript:statsLoaded', src, result[1])
        else
            -- Create default stats
            MySQL.insert('INSERT INTO player_stats (citizenid, kills, deaths) VALUES (?, ?, ?)', {
                citizenid, 0, 0
            })
        end
    end)
end)

5. Item and Inventory System

-- SERVER SIDE: Item management
RegisterServerEvent('myScript:useSpecialItem')
AddEventHandler('myScript:useSpecialItem', function(itemName)
    local src = source
    local Player = QBCore.Functions.GetPlayer(src)
    if not Player then return end
    
    -- Check if player has item
    local item = Player.Functions.GetItemByName(itemName)
    if not item then
        TriggerClientEvent('QBCore:Notify', src, 'You dont have this item', 'error')
        return
    end
    
    -- Remove item
    if Player.Functions.RemoveItem(itemName, 1) then
        -- Item removed successfully, do something
        TriggerClientEvent('QBCore:Notify', src, 'Item used!', 'success')
        
        -- Add different item
        Player.Functions.AddItem('empty_bottle', 1)
        
        -- Trigger client effect
        TriggerClientEvent('myScript:itemEffect', src)
    end
end)
 
-- Register usable item
QBCore.Functions.CreateUseableItem('my_special_item', function(source, item)
    local Player = QBCore.Functions.GetPlayer(source)
    if not Player then return end
    
    -- Custom item usage logic
    TriggerClientEvent('myScript:useItem', source, item.name)
end)

Advanced Lua Patterns for FiveM

1. Callback System

-- SERVER SIDE: Create callback
QBCore.Functions.CreateCallback('myScript:getPlayerStats', function(source, cb, playerId)
    local targetPlayer = QBCore.Functions.GetPlayer(playerId)
    if targetPlayer then
        cb(targetPlayer.PlayerData)
    else
        cb(nil)
    end
end)
 
-- CLIENT SIDE: Use callback
QBCore.Functions.TriggerCallback('myScript:getPlayerStats', function(playerData)
    if playerData then
        print("Player job: " .. playerData.job.name)
        print("Player money: " .. playerData.money.cash)
    else
        print("Player not found")
    end
end, targetPlayerId)

2. Command System with Arguments

-- SERVER SIDE: Advanced command handling
QBCore.Commands.Add('givemoney', 'Give money to player', {
    {name = 'id', help = 'Player ID'},
    {name = 'amount', help = 'Amount of money'}
}, true, function(source, args)
    local src = source
    local targetId = tonumber(args[1])
    local amount = tonumber(args[2])
    
    if not targetId or not amount then
        TriggerClientEvent('QBCore:Notify', src, 'Invalid arguments', 'error')
        return
    end
    
    local TargetPlayer = QBCore.Functions.GetPlayer(targetId)
    if not TargetPlayer then
        TriggerClientEvent('QBCore:Notify', src, 'Player not found', 'error')
        return
    end
    
    TargetPlayer.Functions.AddMoney('cash', amount)
    TriggerClientEvent('QBCore:Notify', src, 'Money given!', 'success')
    TriggerClientEvent('QBCore:Notify', targetId, 'You received $' .. amount, 'success')
end, 'admin')  -- Admin permission required

3. Performance Optimization Patterns

-- Cache frequently used values
local QBCore = exports['qb-core']:GetCoreObject()
local PlayerPedId = PlayerPedId  -- Cache native functions
local GetEntityCoords = GetEntityCoords
local Wait = Wait
 
-- Efficient distance checking
local function GetDistance(coords1, coords2)
    local dx = coords1.x - coords2.x
    local dy = coords1.y - coords2.y
    local dz = coords1.z - coords2.z
    return math.sqrt(dx*dx + dy*dy + dz*dz)
end
 
-- Optimized main loop
CreateThread(function()
    local sleepTime = 1000  -- Default sleep
    
    while true do
        local playerPed = PlayerPedId()
        local playerCoords = GetEntityCoords(playerPed)
        local nearbyAction = false
        
        -- Check for nearby interactions
        for _, location in pairs(interactionLocations) do
            local distance = GetDistance(playerCoords, location.coords)
            
            if distance < 3.0 then
                nearbyAction = true
                sleepTime = 0  -- No sleep when near interaction
                
                -- Draw text or show interaction
                DrawText3D(location.coords, "[E] Interact")
                
                if IsControlJustPressed(0, 38) then -- E key
                    TriggerServerEvent('myScript:interact', location.id)
                end
                break
            end
        end
        
        -- Dynamic sleep time for performance
        if not nearbyAction then
            sleepTime = 1000  -- Sleep longer when not near anything
        end
        
        Wait(sleepTime)
    end
end)

4. Error Handling and Validation

-- Server-side validation
RegisterServerEvent('myScript:processPayment')
AddEventHandler('myScript:processPayment', function(amount, targetId)
    local src = source
    
    -- Input validation
    if type(amount) ~= 'number' or amount <= 0 then
        print(('Invalid amount from player %d: %s'):format(src, tostring(amount)))
        return
    end
    
    if type(targetId) ~= 'number' then
        print(('Invalid target ID from player %d: %s'):format(src, tostring(targetId)))
        return
    end
    
    -- Player validation
    local Player = QBCore.Functions.GetPlayer(src)
    local TargetPlayer = QBCore.Functions.GetPlayer(targetId)
    
    if not Player or not TargetPlayer then
        TriggerClientEvent('QBCore:Notify', src, 'Invalid players', 'error')
        return
    end
    
    -- Business logic with error handling
    local success, error = pcall(function()
        if Player.PlayerData.money.cash < amount then
            error('Insufficient funds')
        end
        
        Player.Functions.RemoveMoney('cash', amount)
        TargetPlayer.Functions.AddMoney('cash', amount)
        
        return true
    end)
    
    if success then
        TriggerClientEvent('QBCore:Notify', src, 'Payment sent!', 'success')
        TriggerClientEvent('QBCore:Notify', targetId, 'Payment received!', 'success')
    else
        TriggerClientEvent('QBCore:Notify', src, error, 'error')
        print(('Payment error for player %d: %s'):format(src, error))
    end
end)

Common FiveM Lua Patterns

1. Resource Communication

-- Cross-resource communication
-- From resource A to resource B
exports['qb-phone']:SendMessage(playerId, "Someone", "Hello!")
 
-- Check if resource exists before using
if GetResourceState('qb-phone') == 'started' then
    exports['qb-phone']:SendMessage(playerId, "Someone", "Hello!")
else
    print("qb-phone resource not available")
end
 
-- Export functions from your resource
-- In your resource (server/main.lua or client/main.lua)
exports('myFunction', function(parameter1, parameter2)
    return "Result: " .. parameter1 .. " + " .. parameter2
end)
 
-- Use from another resource
local result = exports['my-resource']:myFunction("Hello", "World")

2. Configuration Management

-- config.lua
Config = {}
 
Config.Locations = {
    {
        name = "Police Station",
        coords = vector3(428.23, -984.28, 29.76),
        jobs = {"police"},
        items = {"handcuffs", "radio"}
    },
    {
        name = "Hospital",
        coords = vector3(307.27, -1433.68, 29.80),
        jobs = {"ambulance"},
        items = {"medkit", "bandage"}
    }
}
 
Config.Settings = {
    enableNotifications = true,
    checkInterval = 5000,
    maxDistance = 3.0
}
 
-- Using config in main script
for _, location in pairs(Config.Locations) do
    print("Location: " .. location.name)
    
    -- Create blips, set up interactions, etc.
    CreateThread(function()
        while true do
            Wait(Config.Settings.checkInterval)
            
            local playerPed = PlayerPedId()
            local playerCoords = GetEntityCoords(playerPed)
            local distance = #(playerCoords - location.coords)
            
            if distance < Config.Settings.maxDistance then
                -- Player is near location
                if Config.Settings.enableNotifications then
                    -- Show notification
                end
            end
        end
    end)
end

3. Utility Functions

-- Useful utility functions for FiveM development
 
-- Round number to decimal places
function round(num, decimals)
    local mult = 10^(decimals or 0)
    return math.floor(num * mult + 0.5) / mult
end
 
-- Format money display
function formatMoney(amount)
    local formatted = tostring(amount)
    local k
    while true do
        formatted, k = string.gsub(formatted, "^(-?%d+)(%d%d%d)", '%1,%2')
        if k == 0 then break end
    end
    return "$" .. formatted
end
 
-- Get random table element
function getRandomTableElement(tbl)
    if #tbl == 0 then return nil end
    return tbl[math.random(#tbl)]
end
 
-- Deep copy table
function deepCopy(orig)
    local copy
    if type(orig) == 'table' then
        copy = {}
        for orig_key, orig_value in next, orig, nil do
            copy[deepCopy(orig_key)] = deepCopy(orig_value)
        end
        setmetatable(copy, deepCopy(getmetatable(orig)))
    else
        copy = orig
    end
    return copy
end
 
-- Usage examples
local money = 1234567
print(formatMoney(money)) -- Output: $1,234,567
 
local weapons = {"pistol", "rifle", "shotgun"}
local randomWeapon = getRandomTableElement(weapons)
print("Random weapon: " .. randomWeapon)

Best Practices for FiveM Lua Development

1. Code Organization

-- Good: Clear variable names and structure
local QBCore = exports['qb-core']:GetCoreObject()
 
local INTERACTION_DISTANCE = 3.0
local CHECK_INTERVAL = 1000
 
local policeStations = {
    {name = "LSPD", coords = vector3(428.23, -984.28, 29.76)},
    {name = "BCSO", coords = vector3(-449.04, 6008.14, 31.72)}
}
 
local function isPlayerNearPoliceStation(playerCoords)
    for _, station in pairs(policeStations) do
        local distance = #(playerCoords - station.coords)
        if distance <= INTERACTION_DISTANCE then
            return true, station
        end
    end
    return false, nil
end
 
-- Bad: Unclear naming and magic numbers
local a = exports['qb-core']:GetCoreObject()
local b = {
    {n = "LSPD", c = {428.23, -984.28, 29.76}},
    {n = "BCSO", c = {-449.04, 6008.14, 31.72}}
}
 
local function c(d)
    for _, e in pairs(b) do
        if #(d - vector3(e.c[1], e.c[2], e.c[3])) <= 3 then
            return true
        end
    end
    return false
end

2. Performance Tips

-- Cache frequently used functions
local PlayerPedId = PlayerPedId
local GetEntityCoords = GetEntityCoords
local Wait = Wait
 
-- Use local variables for better performance
CreateThread(function()
    while true do
        local playerPed = PlayerPedId()  -- Local variable
        local coords = GetEntityCoords(playerPed)
        
        -- Your logic here
        
        Wait(1000)
    end
end)
 
-- Avoid creating tables in loops
-- Bad:
CreateThread(function()
    while true do
        local data = {x = 1, y = 2, z = 3}  -- Creates new table every iteration
        DoSomething(data)
        Wait(1000)
    end
end)
 
-- Good:
local reusableData = {x = 1, y = 2, z = 3}  -- Create once outside loop
CreateThread(function()
    while true do
        DoSomething(reusableData)
        Wait(1000)
    end
end)

3. Security Considerations

-- Always validate server events
RegisterServerEvent('myScript:buyItem')
AddEventHandler('myScript:buyItem', function(itemName, quantity)
    local src = source
    
    -- Validate input types
    if type(itemName) ~= 'string' or type(quantity) ~= 'number' then
        print(('Invalid input from player %d'):format(src))
        return
    end
    
    -- Validate ranges
    if quantity <= 0 or quantity > 100 then
        print(('Invalid quantity from player %d: %d'):format(src, quantity))
        return
    end
    
    -- Validate allowed items
    local allowedItems = {
        'water', 'bread', 'bandage'
    }
    
    local itemAllowed = false
    for _, allowed in pairs(allowedItems) do
        if itemName == allowed then
            itemAllowed = true
            break
        end
    end
    
    if not itemAllowed then
        print(('Invalid item from player %d: %s'):format(src, itemName))
        return
    end
    
    -- Proceed with validated data
    local Player = QBCore.Functions.GetPlayer(src)
    if Player then
        -- Business logic here
    end
end)

Next Steps & Advanced Topics

Continue Your FiveM Development Journey

Professional Resources

Save Development Time: Explore FiveMX QBCore Scripts for professional, optimized Lua scripts that demonstrate advanced patterns and save weeks of development time.

🎮

Complete Solutions: Get FiveMX QBCore Server Packs with pre-written, production-ready Lua scripts for all essential server features.

Essential Documentation

Practice Projects

Try building these projects to practice your Lua skills:

  1. Simple Bank Robbery: Create a bank robbery script with timer, police alerts, and rewards
  2. Custom Job System: Build a delivery job with routes and payments
  3. Player Statistics: Track and display player stats like playtime, deaths, and money earned
  4. Custom Items: Create usable items with special effects and animations

Common Mistakes to Avoid

🚫

Performance Killers:

  • Forgetting Wait() in loops (will freeze server/client)
  • Creating objects/tables inside frequently called functions
  • Not caching native functions
  • Using while true without proper wait times
⚠️

Security Issues:

  • Trusting client-side data without validation
  • Not checking player permissions for sensitive actions
  • Exposing sensitive data to clients
  • Missing input validation on server events

Conclusion

You now have a solid foundation in Lua programming for FiveM development! Key takeaways:

What You’ve Learned

  • Lua Fundamentals: Syntax, tables, functions, and control structures
  • FiveM Patterns: Client-server architecture, events, and natives
  • QBCore Integration: Player management, jobs, items, and database operations
  • Best Practices: Performance optimization, security, and code organization

Your Next Steps

  1. Practice: Build small scripts using the patterns learned
  2. Experiment: Modify existing QBCore resources to understand their structure
  3. Join Community: Connect with other developers for help and collaboration
  4. Build Projects: Start with simple features and gradually increase complexity

Professional Development

Consider investing in professional resources to accelerate your learning and get production-ready code examples that follow industry best practices.


Ready to build amazing FiveM servers? Continue with our FiveM Development Tutorial to put your new Lua skills into practice, or explore our QBCore vs ESX comparison to understand why QBCore is the best framework for your projects.

Happy scripting! 🚀