πŸ–₯️ 06 β€” NUI e UI

INFO

NUI (Native UI) Γ¨ il sistema di FiveM per creare interfacce grafiche con tecnologie web standard: HTML, CSS e JavaScript. L’HTML viene renderizzato sopra GTA V in un browser CEF (Chromium Embedded Framework) integrato.

ObiettivoDettagli
Cosa imparerai- Creare interfacce grafiche con HTML/CSS/JS in FiveM
- Comunicare tra Lua e JavaScript con SendNUIMessage e RegisterNUICallback
- Costruire HUD, notifiche e menu interattivi
PrerequisitiFile 02-03 completati, conoscenze base di HTML/CSS/JS

Cos’è NUI

NUI (Native UI) Γ¨ il sistema di FiveM per creare interfacce grafiche con tecnologie web standard: HTML, CSS e JavaScript. L’HTML viene renderizzato sopra GTA V in un browser CEF (Chromium Embedded Framework) integrato.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚        GTA V (gioco)         β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚  NUI (HTML/CSS/JS)     β”‚  β”‚
β”‚  β”‚  Overlay trasparente   β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚    Mouse/keyboard input       β”‚
β”‚      ↓          ↑             β”‚
β”‚  RegisterNUICallback          β”‚
β”‚      ↓          ↑             β”‚
β”‚  SendNUIMessage              β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Comunicazione Lua ↔ JS

Lua (Client)               JavaScript (NUI)
    β”‚                           β”‚
    │── SendNUIMessage ────────>β”‚  Riceve con window.addEventListener
    β”‚                           β”‚  "message"
    β”‚<─── RegisterNUICallback ──│  Invia con fetch("https://cfx-nui-...")
    β”‚                           β”‚  o $.post

Struttura Base di una Risorsa NUI

mia_nui/
β”œβ”€β”€ fxmanifest.lua
β”œβ”€β”€ client.lua
β”œβ”€β”€ html/
β”‚   β”œβ”€β”€ index.html
β”‚   β”œβ”€β”€ style.css
β”‚   └── script.js

fxmanifest.lua

fx_version "cerulean"
game "gta5"
 
client_scripts {
    "client.lua"
}
 
-- Dichiara la cartella html come NUI
ui_page "html/index.html"
 
files {
    "html/index.html",
    "html/style.css",
    "html/script.js"
}

client.lua

-- client.lua
local menuAperto = false
 
RegisterCommand("menu", function()
    menuAperto = not menuAperto
 
    if menuAperto then
        -- Mostra NUI
        SetNuiFocus(true, true) -- focus = mouse + tastiera
        SendNUIMessage({
            type = "apriMenu",
            giocatore = {
                nome = GetPlayerName(PlayerId()),
                denaro = 5000,
                livello = 3
            }
        })
    else
        -- Nascondi NUI
        SetNuiFocus(false, false)
        SendNUIMessage({ type = "chiudiMenu" })
    end
end, false)
 
-- Ricevi dati dal JS
RegisterNUICallback("chiudi", function(data, cb)
    menuAperto = false
    SetNuiFocus(false, false)
    cb("ok")
end)
 
RegisterNUICallback("acquista", function(data, cb)
    print("Giocatore vuole acquistare:", data.item, "per $", data.prezzo)
 
    -- Validazione lato server
    TriggerServerEvent("negozio:acquista", data.item, data.prezzo)
 
    cb("ok")
end)
 
> [!WARNING]
> 
> **Dimenticare `SetNuiFocus(false)` blocca il mouse e la tastiera** sul NUI, impedendo al giocatore di interagire col gioco. Chiamalo sempre in ogni punto d'uscita del menu (pulsante chiudi, tasto ESC, callback).
 
-- Chiudi con ESC
Citizen.CreateThread(function()
    while true do
        Citizen.Wait(0)
        if menuAperto and IsControlJustPressed(0, 200) then -- 200 = ESC
            menuAperto = false
            SetNuiFocus(false, false)
            SendNUIMessage({ type = "chiudiMenu" })
        end
    end
end)

html/index.html

<!DOCTYPE html>
<html lang="it">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Menu NUI</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div id="menu" class="hidden">
        <div class="menu-header">
            <h1 id="titolo">Negozio</h1>
            <h2 id="benvenuto"></h2>
        </div>
 
        <div class="menu-corpo">
            <div class="info-giocatore">
                <p>Denaro: <span id="denaro">0</span>$</p>
                <p>Livello: <span id="livello">0</span></p>
            </div>
 
            <div class="item-list">
                <!-- Gli item vengono generati dal JS -->
            </div>
        </div>
 
        <div class="menu-footer">
            <button id="chiudiBtn" class="btn">Chiudi</button>
        </div>
    </div>
 
    <script src="script.js"></script>
</body>
</html>

html/style.css

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}
 
body {
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    color: white;
    overflow: hidden;
    background: transparent;
    user-select: none;
}
 
.hidden {
    display: none !important;
}
 
#menu {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: 500px;
    background: linear-gradient(135deg, rgba(10, 10, 30, 0.95), rgba(30, 30, 60, 0.95));
    border: 1px solid rgba(255, 255, 255, 0.1);
    border-radius: 12px;
    padding: 20px;
    box-shadow: 0 0 40px rgba(0, 0, 0, 0.8);
}
 
.menu-header {
    text-align: center;
    border-bottom: 1px solid rgba(255, 255, 255, 0.1);
    padding-bottom: 15px;
    margin-bottom: 15px;
}
 
.menu-header h1 {
    font-size: 28px;
    color: #4a9eff;
    text-shadow: 0 0 10px rgba(74, 158, 255, 0.3);
}
 
.menu-header h2 {
    font-size: 14px;
    color: #888;
    margin-top: 5px;
}
 
.info-giocatore {
    display: flex;
    justify-content: space-between;
    background: rgba(255, 255, 255, 0.05);
    padding: 10px 15px;
    border-radius: 8px;
    margin-bottom: 15px;
    font-size: 14px;
}
 
.info-giocatore span {
    color: #4a9eff;
    font-weight: bold;
}
 
.item-list {
    max-height: 300px;
    overflow-y: auto;
}
 
.item-entry {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 12px 15px;
    background: rgba(255, 255, 255, 0.05);
    border-radius: 8px;
    margin-bottom: 8px;
    transition: background 0.2s;
    cursor: pointer;
}
 
.item-entry:hover {
    background: rgba(74, 158, 255, 0.15);
}
 
.item-entry .item-nome {
    font-size: 16px;
}
 
.item-entry .item-prezzo {
    font-size: 14px;
    color: #4caf50;
    font-weight: bold;
}
 
.item-entry .item-compra {
    padding: 6px 16px;
    background: #4caf50;
    border: none;
    border-radius: 6px;
    color: white;
    font-weight: bold;
    cursor: pointer;
    transition: background 0.2s;
}
 
.item-entry .item-compra:hover {
    background: #45a049;
}
 
.item-entry .item-compra:active {
    transform: scale(0.95);
}
 
.menu-footer {
    text-align: center;
    margin-top: 15px;
    padding-top: 15px;
    border-top: 1px solid rgba(255, 255, 255, 0.1);
}
 
.btn {
    padding: 10px 30px;
    background: rgba(255, 255, 255, 0.1);
    border: 1px solid rgba(255, 255, 255, 0.2);
    border-radius: 8px;
    color: white;
    font-size: 14px;
    cursor: pointer;
    transition: background 0.2s;
}
 
.btn:hover {
    background: rgba(255, 255, 255, 0.2);
}
 
/* Scrollbar personalizzata */
.item-list::-webkit-scrollbar {
    width: 6px;
}
 
.item-list::-webkit-scrollbar-track {
    background: transparent;
}
 
.item-list::-webkit-scrollbar-thumb {
    background: rgba(255, 255, 255, 0.2);
    border-radius: 3px;
}

html/script.js

// script.js
let menuAperto = false;
 
// Ascolta messaggi da Lua
window.addEventListener("message", function(event) {
    const data = event.data;
 
    switch (data.type) {
        case "apriMenu":
            apriMenu(data.giocatore);
            break;
        case "chiudiMenu":
            chiudiMenu();
            break;
        case "aggiornaDenaro":
            aggiornaDenaro(data.nuovoSaldo);
            break;
    }
});
 
function apriMenu(giocatore) {
    menuAperto = true;
    document.getElementById("menu").classList.remove("hidden");
 
    // Info giocatore
    document.getElementById("benvenuto").textContent =
        "Benvenuto, " + giocatore.nome;
    document.getElementById("denaro").textContent = giocatore.denaro.toLocaleString();
    document.getElementById("livello").textContent = giocatore.livello;
 
    // Genera lista item (esempio statico)
    const itemList = document.querySelector(".item-list");
    itemList.innerHTML = "";
 
    const items = [
        { nome: "Pistola", prezzo: 5000 },
        { nome: "Fucile a pompa", prezzo: 25000 },
        { nome: "Fucile d'assalto", prezzo: 40000 },
        { nome: "Munizioni", prezzo: 500 },
        { nome: "Giubbotto antiproiettile", prezzo: 8000 },
    ];
 
    items.forEach(function(item) {
        const entry = document.createElement("div");
        entry.className = "item-entry";
 
        entry.innerHTML = `
            <div>
                <div class="item-nome">${item.nome}</div>
                <div class="item-prezzo">$${item.prezzo.toLocaleString()}</div>
            </div>
            <button class="item-compra" data-item="${item.nome}" data-prezzo="${item.prezzo}">
                Acquista
            </button>
        `;
 
        itemList.appendChild(entry);
    });
 
    // Aggiungi event listener ai bottoni acquista
    document.querySelectorAll(".item-compra").forEach(function(btn) {
        btn.addEventListener("click", function() {
            const item = this.dataset.item;
            const prezzo = parseInt(this.dataset.prezzo);
 
            // Invia a Lua
            fetch("https://cfx-nui-mia_nui/acquista", {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify({ item: item, prezzo: prezzo })
            });
        });
    });
}
 
function chiudiMenu() {
    menuAperto = false;
    document.getElementById("menu").classList.add("hidden");
}
 
function aggiornaDenaro(nuovoSaldo) {
    document.getElementById("denaro").textContent = nuovoSaldo.toLocaleString();
}
 
// Pulsante chiudi
document.getElementById("chiudiBtn").addEventListener("click", function() {
    fetch("https://cfx-nui-mia_nui/chiudi", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({})
    });
});
 
// Chiudi con ESC dal browser
document.addEventListener("keydown", function(event) {
    if (event.key === "Escape" && menuAperto) {
        fetch("https://cfx-nui-mia_nui/chiudi", {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({})
        });
    }
});

Esempio: HUD (Heads-Up Display)

TIP

Per l’HUD, raggruppa i dati in un unico SendNUIMessage invece di mandare messaggi separati. Un singolo messaggio ogni 100ms Γ¨ piΓΉ efficiente di cinque messaggi distinti. Usa pointer-events: none sul CSS per evitare che l’HUD blocchi i click sul gioco.

client.lua (frammento HUD)

-- client.lua β€” HUD che si aggiorna ogni frame
Citizen.CreateThread(function()
    while true do
        Citizen.Wait(100) -- aggiorna ogni 100ms (10 fps) per risparmiare CPU
 
        local ped = PlayerPedId()
        local salute = GetEntityHealth(ped) - 100 -- GTA V usa 100-200
        local maxSalute = GetEntityMaxHealth(ped) - 100
        local armatura = GetPedArmour(ped)
 
        local inVeicolo = IsPedInAnyVehicle(ped, false)
        local velocita = 0
        local carburante = 0
 
        if inVeicolo then
            local veicolo = GetVehiclePedIsIn(ped, false)
            velocita = math.floor(GetEntitySpeed(veicolo) * 3.6) -- m/s β†’ km/h
            carburante = GetVehicleFuelLevel(veicolo)
        end
 
        SendNUIMessage({
            type = "updateHUD",
            salute = salute,
            maxSalute = maxSalute,
            armatura = armatura,
            inVeicolo = inVeicolo,
            velocita = velocita,
            carburante = carburante
        })
    end
end)

html/hud-style.css (aggiunte per HUD)

/* HUD fisso in basso a destra */
#hud {
    position: absolute;
    bottom: 30px;
    right: 30px;
    background: rgba(0, 0, 0, 0.6);
    border: 1px solid rgba(255, 255, 255, 0.1);
    border-radius: 10px;
    padding: 15px 20px;
    min-width: 200px;
    pointer-events: none; /* non blocca il click sul gioco */
}
 
.hud-row {
    display: flex;
    justify-content: space-between;
    margin: 4px 0;
    font-size: 13px;
}
 
.hud-bar {
    width: 120px;
    height: 8px;
    background: rgba(255, 255, 255, 0.15);
    border-radius: 4px;
    overflow: hidden;
}
 
.hud-bar-fill {
    height: 100%;
    border-radius: 4px;
    transition: width 0.3s ease;
}
 
.hp-fill { background: linear-gradient(90deg, #e74c3c, #2ecc71); }
.armour-fill { background: linear-gradient(90deg, #3498db, #85c1e9); }
.fuel-fill { background: linear-gradient(90deg, #f39c12, #f1c40f); }
 
#hud-speed {
    font-size: 24px;
    font-weight: bold;
    color: #4a9eff;
    text-align: right;
}

Esempio: Notifiche Custom

client.lua

-- client.lua
RegisterNetEvent("nui:notifica")
AddEventHandler("nui:notifica", function(messaggio, tipo)
    SendNUIMessage({
        type = "notifica",
        messaggio = messaggio,
        tipo = tipo or "info", -- info, success, error, warning
        durata = 4000
    })
end)
 
-- Test
RegisterCommand("testnotifica", function()
    TriggerEvent("nui:notifica", "Questo Γ¨ un test!", "success")
end, false)

script.js

window.addEventListener("message", function(event) {
    const data = event.data;
 
    switch (data.type) {
        case "notifica":
            mostraNotifica(data.messaggio, data.tipo, data.durata);
            break;
    }
});
 
function mostraNotifica(messaggio, tipo, durata) {
    const container = document.getElementById("notifiche-container") ||
        creaContainerNotifiche();
 
    const notifica = document.createElement("div");
    notifica.className = "notifica notifica-" + tipo;
    notifica.textContent = messaggio;
 
    container.appendChild(notifica);
 
    setTimeout(function() {
        notifica.classList.add("notifica-uscita");
        setTimeout(function() {
            if (notifica.parentNode) {
                notifica.parentNode.removeChild(notifica);
            }
        }, 300);
    }, durata || 4000);
}
 
function creaContainerNotifiche() {
    const container = document.createElement("div");
    container.id = "notifiche-container";
    container.style.cssText = `
        position: absolute;
        top: 20px;
        right: 20px;
        z-index: 1000;
        display: flex;
        flex-direction: column;
        gap: 8px;
    `;
    document.body.appendChild(container);
    return container;
}

style.css (aggiunte notifiche)

.notifica {
    padding: 12px 20px;
    border-radius: 8px;
    font-size: 14px;
    font-weight: 500;
    animation: notificaEntrata 0.3s ease-out;
    min-width: 250px;
    text-align: center;
}
 
.notifica-info {
    background: rgba(52, 152, 219, 0.9);
    border: 1px solid rgba(52, 152, 219, 0.5);
}
 
.notifica-success {
    background: rgba(46, 204, 113, 0.9);
    border: 1px solid rgba(46, 204, 113, 0.5);
}
 
.notifica-error {
    background: rgba(231, 76, 60, 0.9);
    border: 1px solid rgba(231, 76, 60, 0.5);
}
 
.notifica-warning {
    background: rgba(241, 196, 15, 0.9);
    border: 1px solid rgba(241, 196, 15, 0.5);
    color: #333;
}
 
.notifica-uscita {
    animation: notificaUscita 0.3s ease-in forwards;
}
 
@keyframes notificaEntrata {
    from { transform: translateX(100%); opacity: 0; }
    to { transform: translateX(0); opacity: 1; }
}
 
@keyframes notificaUscita {
    from { transform: translateX(0); opacity: 1; }
    to { transform: translateX(100%); opacity: 0; }
}

Debug NUI

Strumenti Chrome DevTools

FiveM include Chrome DevTools per NUI. Per aprirli:

  1. Premi F8 in gioco per aprire la console
  2. Vai al tab Resources
  3. Trova la tua risorsa nella sezione NUI
  4. Click destro β†’ Inspect

Oppure dal browser lanciando FiveM con flag:

F8 β†’ _devTools

Problemi Comuni

ProblemaCausaSoluzione
Schermo neroSetNuiFocus non chiamatoControlla che il focus sia attivato
NUI non appareui_page errato in fxmanifestControlla percorso html
fetch fallisceURL sbagliatoUsa https://cfx-nui-nomerisorsa/callback
CSS non caricatoPath erratoControlla files {} in fxmanifest
NUI non rispondeCallback senza cb("ok")Ogni callback DEVE chiamare cb()
Troppo lagSendNUIMessage ogni frameUsa Wait(50) o aggiorna solo quando necessario

QUESTION

Riflessione: Il tuo NUI gestisce correttamente la chiusura da tutte le vie possibili (pulsante, ESC, callback)? Ogni percorso deve chiamare SetNuiFocus(false) per non lasciare il giocatore bloccato.


Best Practices

RegolaPerchΓ©
Minimizza SendNUIMessageOgni messaggio ha overhead β€” batch aggiornamenti
Usa cb() in ogni callbackFiveM aspetta la risposta; senza, il callback scade
SetNuiFocus(false) quando chiudiAltrimenti mouse/tastiera bloccati sul NUI
Transizioni CSS animateSembra piΓΉ professionale
Non fare logica di gioco in JSJS fa UI, Lua fa gameplay β€” separa i ruoli
Usa pointer-events: none su HUDL’HUD non deve bloccare il click sul gioco
Separa HTML/CSS/JSManutenibilitΓ  e debug piΓΉ facili

TIP

Prossimo passo: 07 - Ottimizzazione e Debug


Torna alla: Indice Generale