Jeux Tarifs Pro Infrastructure Guides Blog Partenariat À propos

Créer un script FiveM en Lua : le tutoriel complet, de zéro à votre première ressource

Apprenez à écrire vos propres scripts FiveM en Lua : fxmanifest, client et serveur, boucles, événements réseau, commandes, natives, base de données avec oxmysql, sécurité et bonnes pratiques, avec des exemples complets.

5 min de lecture Mis à jour le 21 octobre 2026

Ce tutoriel vous apprend à écrire vos propres ressources FiveM en Lua, le langage le plus utilisé sur la plateforme. Aucune connaissance préalable de FiveM n'est nécessaire, mais quelques bases de programmation aident. Chaque notion est illustrée par un exemple complet que vous pouvez tester sur votre serveur.

Ce qu'il faut savoir avant de commencer

Une ressource FiveM tourne à deux endroits à la fois :

CôtéOù le code s'exécuteÀ quoi il sert
ClientSur l'ordinateur de chaque joueurAfficher, dessiner, lire les touches, agir sur le personnage du joueur
ServeurSur votre serveurGarder les données, décider, vérifier, parler à la base de données

Les deux côtés communiquent par des événements réseau. Règle d'or : le client peut être modifié par un tricheur, le serveur non. Tout ce qui touche à l'argent, aux objets ou aux droits se décide côté serveur.

Pour écrire votre code, utilisez un éditeur comme Visual Studio Code avec l'extension « cfxlua-vscode » qui reconnaît les fonctions de FiveM.

Étape 1 : créer la ressource

Créez un dossier mon-premier-script dans resources/, avec quatre fichiers :

fxmanifest.lua
fx_version 'cerulean'
game 'gta5'
lua54 'yes'

name 'mon-premier-script'
description 'Mon premier script FiveM'
version '1.0.0'

shared_script 'config.lua'
client_script 'client.lua'
server_script 'server.lua'
config.lua
Config = {}

Config.MessageBienvenue = 'Bienvenue sur le serveur !'
Config.CouleurMessage = { 0, 200, 255 }
client.lua
print('Le script client est chargé')
server.lua
print('Le script serveur est chargé')

Ajoutez ensure mon-premier-script au server.cfg, puis redémarrez le serveur. Le message serveur s'affiche dans la Live Console de txAdmin, le message client dans la console F8 du jeu.

Recharger sans redémarrer

Pendant le développement, tapez refresh puis ensure mon-premier-script dans la Live Console : la ressource redémarre seule, sans couper le serveur.

Étape 2 : votre première commande

RegisterCommand crée une commande que les joueurs tapent dans le chat (avec /) ou dans la console F8.

client.lua
RegisterCommand('position', function()
  local ped = PlayerPedId()                -- le personnage du joueur
  local coords = GetEntityCoords(ped)      -- sa position (vector3)
  local heading = GetEntityHeading(ped)    -- son orientation

  print(('x = %.2f, y = %.2f, z = %.2f, cap = %.2f'):format(coords.x, coords.y, coords.z, heading))

  TriggerEvent('chat:addMessage', {
    color = Config.CouleurMessage,
    args = { 'Position', ('%.2f, %.2f, %.2f'):format(coords.x, coords.y, coords.z) },
  })
end, false)

Le dernier argument, false, rend la commande accessible à tous. Avec true, seuls les joueurs qui ont le droit command.position peuvent l'utiliser (voir les permissions ACE dans le guide server.cfg FiveM).

Étape 3 : les natives

Les natives sont les fonctions du jeu GTA V exposées par FiveM : il en existe plus de 6 000. Leur liste complète, avec les paramètres, est sur docs.fivem.net/natives. Quelques natives très utilisées :

NativeCôtéRôle
PlayerPedId()ClientLe personnage du joueur
GetEntityCoords(entite)Les deuxPosition d'une entité
SetEntityCoords(entite, x, y, z)ClientTéléporter une entité
GetVehiclePedIsIn(ped, false)ClientVéhicule dans lequel se trouve le personnage
IsControlJustPressed(0, touche)ClientUne touche vient d'être pressée
GetPlayerName(source)ServeurNom d'un joueur
GetPlayerIdentifierByType(source, 'license')ServeurIdentifiant unique d'un joueur
DropPlayer(source, raison)ServeurExpulser un joueur

Exemple : soigner le joueur et lui donner un gilet pare-balles.

client.lua
RegisterCommand('soin', function()
  local ped = PlayerPedId()
  SetEntityHealth(ped, GetEntityMaxHealth(ped))
  SetPedArmour(ped, 100)
  ClearPedBloodDamage(ped)
end, true)  -- réservé aux joueurs autorisés

Étape 4 : les boucles et Wait

Beaucoup de scripts ont besoin de vérifier quelque chose en continu : une distance, une touche, un état. On utilise un thread avec CreateThread et une boucle qui fait une pause avec Wait.

client.lua
local POINT = vector3(215.76, -810.12, 30.73)  -- devant le parking central

CreateThread(function()
  while true do
    local attente = 1000                         -- loin : on vérifie une fois par seconde
    local distance = #(GetEntityCoords(PlayerPedId()) - POINT)

    if distance < 20.0 then
      attente = 0                                -- proche : on dessine à chaque image
      DrawMarker(1, POINT.x, POINT.y, POINT.z - 1.0, 0, 0, 0, 0, 0, 0, 1.5, 1.5, 0.5, 0, 200, 255, 120, false, false, 2, false, nil, nil, false)

      if distance < 1.5 then
        BeginTextCommandDisplayHelp('STRING')
        AddTextComponentSubstringPlayerName('Appuyez sur ~INPUT_CONTEXT~ pour interagir')
        EndTextCommandDisplayHelp(0, false, true, -1)

        if IsControlJustPressed(0, 38) then      -- 38 = touche E
          TriggerServerEvent('mon-premier-script:interaction')
        end
      end
    end

    Wait(attente)
  end
end)
La règle d'or des performances

Ne laissez jamais une boucle tourner avec Wait(0) quand ce n'est pas nécessaire. Ici, la boucle ne passe à chaque image (Wait(0)) que lorsque le joueur est proche du point. Loin, elle ne travaille qu'une fois par seconde. C'est la différence entre un script à 0,01 ms et un script à 0,5 ms dans resmon.

Étape 5 : les événements réseau

Le client et le serveur se parlent avec des événements.

server.lua
-- 1) Déclarer l'événement côté serveur
RegisterNetEvent('mon-premier-script:interaction', function()
  local src = source                          -- le joueur qui a déclenché l'événement
  local nom = GetPlayerName(src)
  print(('%s a interagi avec le point'):format(nom))

  -- 2) Répondre à ce joueur seulement
  TriggerClientEvent('chat:addMessage', src, {
    color = { 0, 200, 120 },
    args = { 'Serveur', 'Interaction reçue, merci ' .. nom },
  })

  -- 3) Ou prévenir tout le monde (-1 = tous les joueurs)
  TriggerClientEvent('mon-premier-script:annonce', -1, nom)
end)
client.lua
RegisterNetEvent('mon-premier-script:annonce', function(nom)
  BeginTextCommandThefeedPost('STRING')
  AddTextComponentSubstringPlayerName(nom .. ' a trouvé le point secret !')
  EndTextCommandThefeedPostTicker(false, true)
end)
FonctionSens
TriggerServerEvent('nom', ...)Client vers serveur
TriggerClientEvent('nom', joueur, ...)Serveur vers un joueur (-1 pour tous)
TriggerEvent('nom', ...)Dans le même côté (client vers client, serveur vers serveur)
RegisterNetEvent('nom', fonction)Écouter un événement qui vient du réseau
AddEventHandler('nom', fonction)Écouter un événement local
Nommez vos événements

Préfixez toujours vos événements avec le nom de la ressource (mon-premier-script:interaction). Vous évitez les collisions avec les autres ressources.

Étape 6 : sécuriser vos événements

Un tricheur peut déclencher n'importe quel TriggerServerEvent avec les valeurs de son choix. Voici un événement dangereux :

server.lua
-- À NE PAS FAIRE : le client choisit le montant
RegisterNetEvent('banque:recompense', function(montant)
  ajouterArgent(source, montant)
end)

Et sa version sûre :

server.lua
local POINT = vector3(215.76, -810.12, 30.73)
local RECOMPENSE = 250
local DELAI_MS = 10 * 60 * 1000                   -- une récompense toutes les 10 minutes
local derniereFois = {}

RegisterNetEvent('banque:recompense', function()
  local src = source

  -- 1) Le joueur est-il vraiment au bon endroit ? (position lue côté serveur, OneSync)
  local ped = GetPlayerPed(src)
  if #(GetEntityCoords(ped) - POINT) > 5.0 then
    print(('[banque] %s a déclenché la récompense à distance, ignoré'):format(GetPlayerName(src)))
    return
  end

  -- 2) A-t-il déjà été récompensé récemment ?
  local maintenant = GetGameTimer()
  if derniereFois[src] and maintenant - derniereFois[src] < DELAI_MS then return end
  derniereFois[src] = maintenant

  -- 3) Le montant est décidé ici, jamais par le client
  ajouterArgent(src, RECOMPENSE)
end)

AddEventHandler('playerDropped', function()
  derniereFois[source] = nil
end)

Les trois réflexes : vérifier la position, limiter la fréquence, décider des valeurs côté serveur.

Étape 7 : enregistrer des données avec oxmysql

Pour garder des informations entre deux connexions, utilisez la base de données avec la ressource oxmysql. Ajoutez la dépendance dans le manifeste :

fxmanifest.lua
fx_version 'cerulean'
game 'gta5'
lua54 'yes'

dependency 'oxmysql'
shared_script 'config.lua'
client_script 'client.lua'
server_scripts {
  '@oxmysql/lib/MySQL.lua',
  'server.lua',
}

Créez la table (une seule fois, dans votre outil de base de données) :

table.sql
CREATE TABLE IF NOT EXISTS temps_de_jeu (
  identifier VARCHAR(64) PRIMARY KEY,
  nom VARCHAR(64) NOT NULL,
  minutes INT NOT NULL DEFAULT 0,
  derniere_connexion TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

Puis comptez le temps de jeu de chaque joueur :

server.lua
-- Ajoute une minute à chaque joueur connecté, toutes les minutes
CreateThread(function()
  while true do
    Wait(60 * 1000)
    for _, src in ipairs(GetPlayers()) do
      local license = GetPlayerIdentifierByType(src, 'license')
      if license then
        MySQL.insert('INSERT INTO temps_de_jeu (identifier, nom, minutes) VALUES (?, ?, 1) ON DUPLICATE KEY UPDATE minutes = minutes + 1, nom = VALUES(nom)',
          { license, GetPlayerName(src) })
      end
    end
  end
end)

-- /tempsdejeu : affiche le total du joueur
RegisterCommand('tempsdejeu', function(src)
  if src == 0 then return end                        -- 0 = la console du serveur
  local license = GetPlayerIdentifierByType(src, 'license')
  local minutes = MySQL.scalar.await('SELECT minutes FROM temps_de_jeu WHERE identifier = ?', { license }) or 0
  TriggerClientEvent('chat:addMessage', src, {
    args = { 'Temps de jeu', ('%d h %02d min'):format(minutes // 60, minutes % 60) },
  })
end, false)
Fonction oxmysqlRenvoie
MySQL.query.await(sql, params)Toutes les lignes
MySQL.single.await(sql, params)La première ligne
MySQL.scalar.await(sql, params)La première valeur
MySQL.insert.await(sql, params)L'identifiant de la ligne créée
MySQL.update.await(sql, params)Le nombre de lignes modifiées
Toujours des paramètres

N'assemblez jamais une requête avec du texte venant d'un joueur ('... WHERE nom = "' .. nom .. '"'). Utilisez les ? et la liste de paramètres : c'est la protection contre les injections SQL.

Étape 8 : partager des données avec les exports

Une ressource peut offrir des fonctions aux autres avec exports.

server.lua
-- Dans "mon-premier-script"
exports('getMinutes', function(src)
  local license = GetPlayerIdentifierByType(src, 'license')
  return MySQL.scalar.await('SELECT minutes FROM temps_de_jeu WHERE identifier = ?', { license }) or 0
end)
server.lua
-- Dans une autre ressource
local minutes = exports['mon-premier-script']:getMinutes(source)
if minutes < 60 then
  print('Joueur débutant')
end

Étape 9 : les state bags

Les state bags partagent une valeur entre le serveur et les clients, synchronisée automatiquement. C'est la méthode moderne pour diffuser un état (un statut de service, une météo, un compteur).

server.lua
-- Une valeur globale, lue par tous les clients
GlobalState.horaires = 'Ouvert de 18 h à 2 h'

-- Une valeur propre à un joueur
RegisterCommand('service', function(src)
  local etat = Player(src).state
  etat:set('enService', not etat.enService, true)  -- true = répliqué aux clients
end, true)
client.lua
print(GlobalState.horaires)

AddStateBagChangeHandler('enService', nil, function(bagName, key, value)
  local joueur = GetPlayerFromStateBagName(bagName)
  if joueur == PlayerId() then
    print(value and 'Vous prenez votre service' or 'Vous quittez votre service')
  end
end)

Bonnes pratiques

Et ensuite ?


EsoterHost n'est ni approuvé, ni sponsorisé, ni affilié à Rockstar Games ou à Cfx.re. FiveM et Grand Theft Auto V sont des marques de leurs propriétaires.

Ce guide vous a-t-il aidé ? Une question ? Ouvrez un ticket

Vous ne trouvez pas votre réponse ?

Notre équipe répond à vos tickets 7j/7, directement depuis votre espace client.