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 |
|---|---|---|
| Client | Sur l'ordinateur de chaque joueur | Afficher, dessiner, lire les touches, agir sur le personnage du joueur |
| Serveur | Sur votre serveur | Garder 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 :
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 = {}
Config.MessageBienvenue = 'Bienvenue sur le serveur !'
Config.CouleurMessage = { 0, 200, 255 }print('Le script client est chargé')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.
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.
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 :
| Native | Côté | Rôle |
|---|---|---|
PlayerPedId() | Client | Le personnage du joueur |
GetEntityCoords(entite) | Les deux | Position d'une entité |
SetEntityCoords(entite, x, y, z) | Client | Téléporter une entité |
GetVehiclePedIsIn(ped, false) | Client | Véhicule dans lequel se trouve le personnage |
IsControlJustPressed(0, touche) | Client | Une touche vient d'être pressée |
GetPlayerName(source) | Serveur | Nom d'un joueur |
GetPlayerIdentifierByType(source, 'license') | Serveur | Identifiant unique d'un joueur |
DropPlayer(source, raison) | Serveur | Expulser un joueur |
Exemple : soigner le joueur et lui donner un gilet pare-balles.
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.
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)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.
-- 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)RegisterNetEvent('mon-premier-script:annonce', function(nom)
BeginTextCommandThefeedPost('STRING')
AddTextComponentSubstringPlayerName(nom .. ' a trouvé le point secret !')
EndTextCommandThefeedPostTicker(false, true)
end)| Fonction | Sens |
|---|---|
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 |
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 :
-- À NE PAS FAIRE : le client choisit le montant
RegisterNetEvent('banque:recompense', function(montant)
ajouterArgent(source, montant)
end)Et sa version sûre :
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 :
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) :
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 :
-- 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 oxmysql | Renvoie |
|---|---|
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 |
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.
-- 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)-- 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).
-- 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)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
- Un préfixe par ressource pour les événements, les commandes et les exports.
- Un fichier config.lua pour tout ce qu'un administrateur voudra changer : textes, positions, montants.
lua54 'yes'dans le manifeste : Lua 5.4 est plus rapide et gère les entiers (//,<const>).localpartout : une variable globale est partagée avec tout le fichier et ralentit le code.- Vérifier côté serveur tout ce qui a de la valeur.
- Mesurer avec
resmon 1(client) etprofiler(serveur) avant de publier.
Et ensuite ?
- Piochez des exemples complets dans la bibliothèque de scripts FiveM.
- Branchez vos scripts sur un framework avec le guide Installer ESX Legacy ou QBCore.
- Gardez un serveur fluide avec le guide Optimiser un serveur FiveM.
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.
