π₯οΈ 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.
| Obiettivo | Dettagli |
|---|---|
| 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 |
| Prerequisiti | File 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
SendNUIMessageinvece di mandare messaggi separati. Un singolo messaggio ogni 100ms Γ¨ piΓΉ efficiente di cinque messaggi distinti. Usapointer-events: nonesul 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:
- Premi F8 in gioco per aprire la console
- Vai al tab Resources
- Trova la tua risorsa nella sezione NUI
- Click destro β Inspect
Oppure dal browser lanciando FiveM con flag:
F8 β _devTools
Problemi Comuni
| Problema | Causa | Soluzione |
|---|---|---|
| Schermo nero | SetNuiFocus non chiamato | Controlla che il focus sia attivato |
| NUI non appare | ui_page errato in fxmanifest | Controlla percorso html |
fetch fallisce | URL sbagliato | Usa https://cfx-nui-nomerisorsa/callback |
| CSS non caricato | Path errato | Controlla files {} in fxmanifest |
| NUI non risponde | Callback senza cb("ok") | Ogni callback DEVE chiamare cb() |
| Troppo lag | SendNUIMessage ogni frame | Usa 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
| Regola | PerchΓ© |
|---|---|
| Minimizza SendNUIMessage | Ogni messaggio ha overhead β batch aggiornamenti |
Usa cb() in ogni callback | FiveM aspetta la risposta; senza, il callback scade |
| SetNuiFocus(false) quando chiudi | Altrimenti mouse/tastiera bloccati sul NUI |
| Transizioni CSS animate | Sembra piΓΉ professionale |
| Non fare logica di gioco in JS | JS fa UI, Lua fa gameplay β separa i ruoli |
Usa pointer-events: none su HUD | LβHUD non deve bloccare il click sul gioco |
| Separa HTML/CSS/JS | ManutenibilitΓ e debug piΓΉ facili |
TIP
Prossimo passo: 07 - Ottimizzazione e Debug
Torna alla: Indice Generale