Complete Guide to FiveM Script Development

Master FiveM script development from beginner to advanced. Learn Lua scripting, client/server architecture, and best practices for creating professional GTA V mods.

📅 January 15, 2024⏱️ 15 min read🏷️ Tutorial

1. Introduction to FiveM Scripting

FiveM is a modification framework for Grand Theft Auto V that allows you to create custom multiplayer servers. Unlike traditional GTA V mods, FiveM enables you to build persistent online worlds with custom scripts, vehicles, maps, and gameplay mechanics.

Scripting in FiveM is primarily done using Lua, a lightweight, embeddable scripting language. FiveM scripts can run on both the client (player's computer) and server, creating a distributed architecture that enables complex multiplayer experiences.

💡 Pro Tip: Before diving into scripting, familiarize yourself with the FiveM documentation and understand the difference between client-side and server-side execution.

2. Lua Programming Fundamentals

Variables and Data Types

Lua is dynamically typed, meaning variables don't need type declarations. Here are the basic data types:

-- Variables and data types
local name = "Player"           -- String
local age = 25                  -- Number
local isAdmin = true            -- Boolean
local playerData = {            -- Table (object/array)
    name = "John Doe",
    level = 50,
    inventory = {"gun", "ammo"}
}

-- Table access
print(playerData.name)          -- "John Doe"
print(playerData["name"])       -- Alternative syntax
print(playerData.inventory[1])  -- "gun"

Functions

-- Function definition
local function calculateDamage(baseDamage, armor)
    local damage = baseDamage - (armor * 0.5)
    return math.max(damage, 0)  -- Ensure damage is never negative
end

-- Function call
local finalDamage = calculateDamage(100, 20)
print("Damage dealt:", finalDamage)  -- Output: Damage dealt: 90

Control Structures

-- If statements
local playerLevel = 45
if playerLevel >= 50 then
    print("Player is experienced")
elseif playerLevel >= 25 then
    print("Player is intermediate")
else
    print("Player is a beginner")
end

-- For loops
for i = 1, 10 do
    print("Count:", i)
end

-- While loops
local count = 0
while count < 5 do
    count = count + 1
    print("Current count:", count)
end

3. FiveM Architecture Overview

Understanding FiveM's architecture is crucial for effective script development. FiveM uses a client-server model where scripts can run on both sides.

Client-Side Scripts

  • • Run on each player's computer
  • • Handle UI, input, and visual effects
  • • Access to game natives and player data
  • • Limited security - can be modified by players
  • • File: client.lua

Server-Side Scripts

  • • Run on the server only
  • • Handle game logic, data persistence
  • • Database operations and player management
  • • Secure - cannot be modified by players
  • • File: server.lua

Communication Between Client and Server

-- Server to Client (TriggerClientEvent)
-- server.lua
TriggerClientEvent('showNotification', playerId, 'Welcome to the server!')

-- client.lua
RegisterNetEvent('showNotification')
AddEventHandler('showNotification', function(message)
    -- Show notification to player
    SetNotificationTextEntry("STRING")
    AddTextComponentString(message)
    DrawNotification(false, false)
end)

-- Client to Server (TriggerServerEvent)
-- client.lua
TriggerServerEvent('requestPlayerData', GetPlayerServerId(PlayerId()))

-- server.lua
RegisterNetEvent('requestPlayerData')
AddEventHandler('requestPlayerData', function(playerId)
    local playerData = GetPlayerData(playerId)
    TriggerClientEvent('receivePlayerData', source, playerData)
end)

4. Client-Side Scripting

Client-side scripts handle everything that happens on the player's computer. This includes UI, input handling, visual effects, and local game state.

Basic Client Script Structure

-- client.lua
local isScriptActive = true

-- Main thread - runs continuously
Citizen.CreateThread(function()
    while isScriptActive do
        -- Your main script logic here
        local playerPed = PlayerPedId()
        local playerCoords = GetEntityCoords(playerPed)
        
        -- Example: Display coordinates
        if IsControlJustPressed(0, 47) then -- G key
            print("Player coordinates:", playerCoords.x, playerCoords.y, playerCoords.z)
        end
        
        Citizen.Wait(0) -- Important: Always include a wait
    end
end)

-- Event handlers
RegisterNetEvent('myScript:showMessage')
AddEventHandler('myScript:showMessage', function(message)
    -- Handle received message
    print("Received message:", message)
end)

-- Cleanup on resource stop
AddEventHandler('onResourceStop', function(resourceName)
    if GetCurrentResourceName() == resourceName then
        isScriptActive = false
        print("Script stopped")
    end
end)

Creating Simple UI Elements

-- Simple text display
local function drawText(text, x, y, scale, color)
    SetTextFont(4)
    SetTextProportional(1)
    SetTextScale(scale, scale)
    SetTextColour(color.r or 255, color.g or 255, color.b or 255, color.a or 255)
    SetTextDropShadow(0, 0, 0, 0, 255)
    SetTextEdge(2, 0, 0, 0, 150)
    SetTextDropShadow()
    SetTextOutline()
    SetTextEntry("STRING")
    AddTextComponentString(text)
    DrawText(x, y)
end

-- Usage in main thread
Citizen.CreateThread(function()
    while true do
        drawText("FiveM Script Running", 0.5, 0.1, 0.5, {r = 255, g = 255, b = 255})
        Citizen.Wait(0)
    end
end)

5. Server-Side Scripting

Server-side scripts handle game logic, data persistence, and player management. They're essential for creating persistent multiplayer experiences.

Basic Server Script Structure

-- server.lua
local players = {}

-- Player connection handling
AddEventHandler('playerConnecting', function(playerName, setKickReason, deferrals)
    local source = source
    print("Player connecting:", playerName, "ID:", source)
    
    -- Add player to tracking
    players[source] = {
        name = playerName,
        connected = true,
        joinTime = os.time()
    }
end)

-- Player disconnection
AddEventHandler('playerDropped', function(reason)
    local source = source
    if players[source] then
        print("Player disconnected:", players[source].name, "Reason:", reason)
        players[source] = nil
    end
end)

-- Custom command example
RegisterCommand('kickplayer', function(source, args)
    if source == 0 then -- Console command
        local targetId = tonumber(args[1])
        if targetId and players[targetId] then
            DropPlayer(targetId, "Kicked by console")
            print("Kicked player:", players[targetId].name)
        end
    end
end, true) -- true = restricted to console

Player Data Management

-- Player data structure
local playerData = {}

-- Load player data (example with MySQL)
function LoadPlayerData(source)
    local identifier = GetPlayerIdentifiers(source)[1]
    
    -- Simulate database query
    local data = {
        money = 5000,
        level = 1,
        experience = 0,
        inventory = {}
    }
    
    playerData[source] = data
    TriggerClientEvent('playerDataLoaded', source, data)
end

-- Save player data
function SavePlayerData(source)
    if playerData[source] then
        -- Save to database
        print("Saving data for player:", GetPlayerName(source))
        -- MySQL.Async.execute('UPDATE players SET money = @money WHERE identifier = @id', {
        --     ['@money'] = playerData[source].money,
        --     ['@id'] = GetPlayerIdentifiers(source)[1]
        -- })
    end
end

-- Auto-save every 5 minutes
Citizen.CreateThread(function()
    while true do
        Citizen.Wait(300000) -- 5 minutes
        for playerId, _ in pairs(playerData) do
            SavePlayerData(playerId)
        end
    end
end)

6. Best Practices and Optimization

Performance Optimization

❌ Avoid: Never use Citizen.Wait(0) in server scripts. Use Citizen.Wait(1000) or higher for periodic tasks.

✅ Do: Use appropriate wait times. Client UI can use Citizen.Wait(0), but data processing should use longer intervals.

Security Best Practices

  • Server-side validation: Always validate data on the server before processing
  • Anti-cheat measures: Implement server-side checks for player actions
  • Input sanitization: Sanitize all user inputs to prevent exploits
  • Rate limiting: Limit the frequency of expensive operations

Code Organization

-- config.lua - Configuration file
Config = {}

Config.Settings = {
    MaxPlayers = 32,
    StartingMoney = 5000,
    EnableDebug = false
}

Config.Commands = {
    AdminOnly = true,
    Cooldown = 5000 -- milliseconds
}

-- utils.lua - Utility functions
Utils = {}

function Utils.GetPlayerName(source)
    return GetPlayerName(source) or "Unknown"
end

function Utils.IsPlayerAdmin(source)
    -- Check if player is admin
    return IsPlayerAceAllowed(source, "admin")
end

7. Advanced Topics

Database Integration

For persistent data, you'll need to integrate with a database. Popular choices include MySQL and MongoDB.

-- Example with oxmysql
local function CreatePlayer(identifier, data)
    MySQL.Async.execute('INSERT INTO players (identifier, money, level) VALUES (@id, @money, @level)', {
        ['@id'] = identifier,
        ['@money'] = data.money or 5000,
        ['@level'] = data.level or 1
    })
end

local function GetPlayerData(identifier)
    local result = MySQL.Sync.fetchAll('SELECT * FROM players WHERE identifier = @id', {
        ['@id'] = identifier
    })
    return result[1]
end

Custom UI with HTML/CSS/JS

For complex user interfaces, you can create custom HTML pages with CSS and JavaScript.

-- client.lua
local isUIOpen = false

-- Open custom UI
function OpenCustomUI()
    SetNuiFocus(true, true)
    SendNUIMessage({
        type = "openUI",
        data = {
            playerName = GetPlayerName(PlayerId()),
            playerMoney = 5000
        }
    })
    isUIOpen = true
end

-- Handle NUI callbacks
RegisterNUICallback('closeUI', function(data, cb)
    SetNuiFocus(false, false)
    isUIOpen = false
    cb('ok')
end)

RegisterNUICallback('updateMoney', function(data, cb)
    -- Handle money update from UI
    TriggerServerEvent('updatePlayerMoney', data.amount)
    cb('ok')
end)

8. Conclusion

FiveM script development is a powerful way to create custom multiplayer experiences in GTA V. By understanding Lua programming, FiveM's architecture, and following best practices, you can create engaging scripts that enhance the gameplay for your server's community.

Next Steps

  • • Practice with simple scripts before tackling complex systems
  • • Join the FiveM Community for support and resources
  • • Study existing scripts to understand different approaches
  • • Test your scripts thoroughly before deploying to production
  • • Keep learning and stay updated with FiveM updates

Ready to Start Scripting?

Use our AI-powered script generator to create professional FiveM scripts in seconds, or browse our collection of free templates to get started quickly.