🧵 02 — Eventi e Thread

INFO

FiveM utilizza thread virtuali cooperativi tramite il runtime Citizen e un sistema di eventi per la comunicazione tra client, server e altre risorse. Gli eventi sono il pilastro della comunicazione in qualsiasi server FiveM.

ObiettivoDettagli
Cosa imparerai- Creare e gestire thread con Citizen.CreateThread e Citizen.Wait
- Comunicare con eventi locali e di rete (TriggerEvent, TriggerServerEvent, TriggerClientEvent)
- Pattern di loop condizionali e best practices per performance
PrerequisitiConoscenze base di Lua, familiarità con l’ecosistema FiveM

Thread in FiveM

FiveM non è Lua puro: gira su Citizen, il runtime di Cfx.re. Citizen fornisce threading cooperativo tramite Citizen.CreateThread e Citizen.Wait.

flowchart LR
    A[Citizen.CreateThread] --> B[Avvia thread virtuale]
    B --> C{Esecuzione}
    C -->|Citizen.Wait(N)| D[Sospeso per N ms]
    D --> C
    C -->|Fine funzione| E[Thread termina]

Citizen.CreateThread

Crea un thread virtuale. Più thread condividono lo stesso thread OS — sono coroutine gestite dal runtime.

-- client.lua
Citizen.CreateThread(function()
    while true do
        -- Questo loop gira ogni secondo
        print("Thread attivo:", GetGameTimer())
        Citizen.Wait(1000)
    end
end)

Citizen.Wait

Sospende il thread corrente per N millisecondi. Fondamentale: senza Wait, un loop blocca tutto il client.

WARNING

Loop senza Wait bloccano il client. Un loop infinito senza Citizen.Wait fa crashare FiveM o causa timeout. Aggiungi sempre una pausa, anche minima come Wait(0).

-- Come NON fare — loop bloccante
Citizen.CreateThread(function()
    while true do
        -- SENZA Wait: FiveM si blocca, crash o timeout
    end
end)
 
-- Come fare — loop con pausa
Citizen.CreateThread(function()
    while true do
        -- Questo gira ogni 100ms (bilanciato tra performance e reattivitĂ )
        Citizen.Wait(100)
    end
end)

Pattern: loop condizionale

TIP

Usa un loop condizionale per bilanciare performance e reattività: aumenta il Wait quando il codice non serve e riducilo quando è attivo. Questo riduce il carico CPU senza sacrificare la reattività.

-- client.lua — loop solo quando serve
local mostraHUD = false
 
Citizen.CreateThread(function()
    while true do
        if mostraHUD then
            -- Disegna HUD solo se attivo
            local ped = PlayerPedId()
            local salute = GetEntityHealth(ped)
            local pos = GetEntityCoords(ped)
            print(string.format("HP: %d | Pos: %.1f, %.1f, %.1f",
                salute, pos.x, pos.y, pos.z))
            Citizen.Wait(500)
        else
            -- Se non serve, aspetta piĂą a lungo e risparmia CPU
            Citizen.Wait(1000)
        end
    end
end)

Pattern: timer singolo (SetTimeout)

-- Esegue codice UNA VOLTA dopo N ms
Citizen.SetTimeout(5000, function()
    print("Sono passati 5 secondi")
end)
 
-- Alternativa con CreateThread
Citizen.CreateThread(function()
    Citizen.Wait(5000)
    print("Sono passati 5 secondi")
end)

Eventi

Gli eventi sono il sistema di messaggistica di FiveM. Permettono la comunicazione client ↔ client, client ↔ server, e server ↔ server.

flowchart TD
    subgraph Client
        C1[TriggerEvent\nlocale]
        C2[TriggerServerEvent\n→ server]
        C3[TriggerLatentServerEvent\n→ server con buffer]
    end
    subgraph Server
        S1[TriggerEvent\nlocale]
        S2[TriggerClientEvent\n→ client specifico]
        S3[TriggerLatentClientEvent\n→ client con buffer]
    end
    C2 --> S{Server}
    S2 --> C{Client}

RegisterNetEvent

Registra un evento perché possa essere ricevuto. Sia client che server lo usano.

-- client.lua
RegisterNetEvent("myresource:riceviMessaggio")
AddEventHandler("myresource:riceviMessaggio", function(mittente, testo)
    print("Messaggio da", mittente, ":", testo)
    SetNotificationTextEntry("STRING")
    AddTextComponentSubstringPlayerName(testo)
    DrawNotification(false, false)
end)

AddEventHandler

Collega una funzione a un evento. Alternativa moderna:

RegisterNetEvent("myresource:apriMenu", function()
    -- Usando RegisterNetEvent + funzione anonima direttamente
    -- (equivale a AddEventHandler)
end)
 
-- Con AddEventHandler esplicito
RegisterNetEvent("myresource:apriMenu")
AddEventHandler("myresource:apriMenu", function()
    print("Menu aperto")
end)

TriggerEvent (locale)

Innesca un evento solo sulla macchina corrente (client o server).

-- client.lua — entrambi gli eventi sono sullo stesso client
RegisterNetEvent("mioEvento:locale")
AddEventHandler("mioEvento:locale", function(msg)
    print("Ricevuto localmente:", msg)
end)
 
TriggerEvent("mioEvento:locale", "Ciao dal client stesso")

TriggerServerEvent

Manda un evento al server dal client.

-- client.lua
RegisterCommand("compra", function(source, args)
    local item = args[1]
    local quantita = tonumber(args[2]) or 1
 
    -- Manda richiesta al server
    TriggerServerEvent("negozio:acquista", item, quantita)
end, false)
-- server.lua
RegisterNetEvent("negozio:acquista")
AddEventHandler("negozio:acquista", function(item, quantita)
    local source = source  -- IMPORTANTE: source è disponibile solo dentro l'handler
    print("Giocatore", source, "vuole", quantita, "x", item)
 
    -- Verifica e processa
    -- ...
end)

TriggerClientEvent

Manda un evento dal server a uno o piĂą client.

-- server.lua
RegisterCommand("annuncio", function(source, args)
    local messaggio = table.concat(args, " ")
 
    -- A tutti i giocatori
    TriggerClientEvent("chat:addMessage", -1, {
        color = { 255, 255, 0 },
        args = { "ANNUNCIO", messaggio }
    })
end, true) -- true = solo admin
-- server.lua — target specifico
TriggerClientEvent("giocatore:teletrasporta", targetSource, x, y, z)

TriggerLatentServerEvent / TriggerLatentClientEvent

INFO

Latent events dividono i dati in pacchetti piĂą piccoli per evitare disconnessioni su payload grandi (es. immagini, tabelle di grandi dimensioni). Usali quando un singolo evento supera qualche KB.

Per dati grandi (es. immagini, tabelle enormi). Usano buffer a pacchetti.

-- client.lua
TriggerLatentServerEvent("upload:immagine", 128000, datiImmagine)

Eventi nativi di FiveM

Evento ClientDescrizione
onClientResourceStartRisorsa client avviata
onClientResourceStopRisorsa client fermata
playerSpawnedGiocatore spawnato (dopo selezione personaggio)
Evento ServerDescrizione
playerJoiningGiocatore in fase di connessione
playerDroppedGiocatore disconnesso
onResourceStartRisorsa server avviata
onResourceStopRisorsa server fermata
rconCommandComando RCON inviato

Esempio: playerJoining

-- server.lua
AddEventHandler("playerJoining", function(playerSource, oldIds, deferredJoin)
    -- deferredJoin: permette di ritardare l'ingresso (es. per caricare dati)
    print("Giocatore", playerSource, "sta entrando...")
 
    -- Caricare dati dal database
    -- deferredJoin.defer()
    -- deferredJoin.done({ ... })
end)

Esempio: playerDropped

-- server.lua
AddEventHandler("playerDropped", function(reason)
    local source = source
    print("Giocatore", source, "ha lasciato. Motivo:", reason)
 
    -- Salvare dati nel database
    local ped = GetPlayerPed(source)
    local pos = GetEntityCoords(ped)
    exports["oxmysql"]:execute(
        "UPDATE giocatori SET ultima_posizione_x = ?, ultima_posizione_y = ?, ultima_posizione_z = ? WHERE identifier = ?",
        { pos.x, pos.y, pos.z, GetPlayerIdentifier(source) }
    )
end)

Esempio: onResourceStart

-- server.lua
AddEventHandler("onResourceStart", function(resourceName)
    if resourceName == GetCurrentResourceName() then
        print("Risorsa", resourceName, "avviata correttamente")
    end
end)
 
-- client.lua
AddEventHandler("onClientResourceStart", function(resourceName)
    if resourceName == GetCurrentResourceName() then
        print("Risorsa client avviata")
    end
end)

Esempi completi

Sistema di notifica via evento

-- notify/client.lua
RegisterNetEvent("notify:send")
AddEventHandler("notify:send", function(tipo, messaggio, durata)
    durata = durata or 3000
 
    if tipo == "success" then
        SetNotificationTextEntry("STRING")
        AddTextComponentSubstringPlayerName("âś… " .. messaggio)
        DrawNotification(true, false)
    elseif tipo == "error" then
        SetNotificationTextEntry("STRING")
        AddTextComponentSubstringPlayerName("❌ " .. messaggio)
        DrawNotification(true, false)
    elseif tipo == "info" then
        SetNotificationTextEntry("STRING")
        AddTextComponentSubstringPlayerName("ℹ️ " .. messaggio)
        DrawNotification(false, false)
    end
 
    -- Alternativa con ESX (se disponibile)
    -- ESX.ShowNotification(messaggio, tipo)
end)
-- notify/server.lua
RegisterCommand("notifica", function(source, args)
    local messaggio = table.concat(args, " ")
    TriggerClientEvent("notify:send", source, "info", messaggio)
end, false)

Timer countdown con thread

-- client.lua
local countdownAttivo = false
 
RegisterNetEvent("countdown:avvia")
AddEventHandler("countdown:avvia", function(secondi)
    if countdownAttivo then return end
    countdownAttivo = true
 
    Citizen.CreateThread(function()
        local rimasti = secondi
        while rimasti > 0 do
            print("Tempo rimasto:", rimasti, "secondi")
            TriggerEvent("chat:addMessage", {
                args = { "COUNTDOWN", tostring(rimasti) .. " secondi rimasti" }
            })
            Citizen.Wait(1000)
            rimasti = rimasti - 1
        end
 
        print("Countdown finito!")
        TriggerServerEvent("countdown:finito", source)
        countdownAttivo = false
    end)
end)

Evento con callback (promise pattern)

-- client.lua
local function richiediDatiGiocatore()
    local promise = Citizen.Promise.new()
 
    -- Crea un evento temporaneo per la risposta
    local eventName = "dati:risposta_" .. math.random(100000, 999999)
 
    RegisterNetEvent(eventName)
    AddEventHandler(eventName, function(dati)
        promise:resolve(dati)
        RemoveEventHandler(eventName)
    end)
 
    TriggerServerEvent("dati:richiedi", eventName)
 
    return promise
end
 
-- Uso con await
Citizen.CreateThread(function()
    local dati = Citizen.Await(richiediDatiGiocatore())
    print("Dati ricevuti:", json.encode(dati))
end)
-- server.lua
RegisterNetEvent("dati:richiedi")
AddEventHandler("dati:richiedi", function(eventRisposta)
    local source = source
    -- Prepara dati
    local dati = {
        nome = "Marco",
        denaro = 500,
        livello = 3
    }
    TriggerClientEvent(eventRisposta, source, dati)
end)

Cancellazione eventi

-- Rimuovere un handler specifico
local handler = AddEventHandler("evento:temporaneo", function()
    print("Questo gira solo una volta")
    RemoveEventHandler(handler)
end)
 
-- Rimuovere tutti gli handler di un evento
-- Non esiste una funzione built-in; bisogna tenerne traccia

QUESTION

Riflessione: Quanti thread stai creando nella tua risorsa? Ricorda che ogni thread, anche in idle, consuma risorse dello scheduler. Potresti consolidarne alcuni?


Best Practices

RegolaPerché
Sempre RegisterNetEvent prima di AddEventHandlerSenza, l’evento non viene registrato per la ricezione
Non abusare di TriggerServerEventOgni evento server è una richiesta di rete — limita a dati necessari
Source è disponibile solo dentro l’handlerNon salvare source in variabili globali — potrebbe cambiare
Usa eventi specificiplayer:dati meglio di player — evita collisioni
Prefissa i tuoi eventimiaRisorsa:azione — evita conflitti con altre risorse
Wait sempre nei loopSenza, client si blocca o crasha
Wait(0) vs Wait(N)Wait(0) cede allo scheduler (1 frame); Wait(N) sospende per N ms

TIP

Prossimo passo: 03 - Native Functions


Torna alla: Indice Generale