NUI
NUI é como um resource desenha a própria interface no MTAX. Você escreve uma página web comum — HTML, CSS e JavaScript —, declara ela no manifesto, e o cliente desenha por cima do jogo. Dali em diante o seu script Lua e a sua página conversam por mensagens num sentido e por callbacks no outro.
Se você já fez interface para FiveM, é o mesmo modelo e os mesmos nomes de função: sendNuiMessage, registerNuiCallback, setNuiFocus.
Como funciona
Seção intitulada “Como funciona”O cliente roda dois browsers separados:
| Browser da interface | Browser de NUI | |
|---|---|---|
| O que desenha | O menu do MTAX, o server browser, as configurações, o console in-game | As suas páginas — uma por resource com ui_page, mais a loadscreen |
| Ponte nativa | Sim — é ela que fala com a conta, a sessão e o launcher | Nenhuma |
| DevTools | Nunca | Liberado |
| Camada | Por cima | Por baixo |
A sua página vive no browser de NUI, então ela pode ser inspecionada à vontade sem nunca expor a conta ou a sessão do jogador. Essa separação é justamente o motivo de existirem dois browsers.
Dentro do browser de NUI, cada resource ganha um frame próprio em tela cheia, na própria origem. O JavaScript de um resource não alcança a página de outro resource, e nenhum dos dois alcança o menu do MTAX.
A sua primeira interface
Seção intitulada “A sua primeira interface”Três peças: a página, a entrada no manifesto e o script.
1. A página
Seção intitulada “1. A página”<!doctype html><html><head> <meta charset="utf-8"> <style> /* A camada fica por cima do jogo — deixe o body transparente ou você pinta a tela inteira. */ html, body { margin: 0; height: 100%; background: transparent; overflow: hidden; } #painel { display: none; width: 320px; margin: 10% auto; padding: 24px; border-radius: 12px; background: rgba(10, 12, 18, 0.92); color: #fff; font-family: system-ui, sans-serif; } #painel.aberto { display: block; } </style></head><body> <div id="painel"> <h1>Olá</h1> <button id="fechar">Fechar</button> </div>
<script> // Lua -> página window.addEventListener("message", function (event) { var data = event.data || {}; if (data.type === "open") { document.getElementById("painel").classList.add("aberto"); } else if (data.type === "close") { document.getElementById("painel").classList.remove("aberto"); } });
// página -> Lua document.getElementById("fechar").addEventListener("click", function () { fetch("/close", { method: "POST", body: JSON.stringify({}) }); }); </script></body></html>2. O manifesto
Seção intitulada “2. O manifesto”-- mtaxmanifest.luaresource_name = "minha-ui"
ui_page = "client/index.html"
client_files = { "client/main.lua",}
files = { "client/index.html", "client/style.css", "client/app.js",}ui_page é o ponto de entrada. Duas regras que o servidor confere ao carregar o resource:
- ela também precisa estar listada em
files = {}; - ela não pode ser um
.lua.
Errando qualquer uma das duas, o resource é recusado com o motivo impresso no console. Todo o resto que a página carrega — CSS, JS, imagens, fontes — também precisa estar em files, porque essa lista é exatamente o que o jogador baixa.
3. O script
Seção intitulada “3. O script”-- client/main.lualocal aberto = false
addCommandHandler("ui", function() aberto = not aberto sendNuiMessage({ type = aberto and "open" or "close" }) setNuiFocus(aberto, aberto)end)
registerNuiCallback("close", function(data, cb) aberto = false setNuiFocus(false, false) cb({ ok = true })end)Isso é uma interface completa. /ui abre, o botão fecha.
A origem: nui://
Seção intitulada “A origem: nui://”A sua página é servida da memória em:
nui://<resource>/<caminho dentro do resource>Nada é gravado em claro no disco — o cliente mantém os arquivos decifrados na memória e responde o browser a partir dali.
O host é o nome do resource reduzido a [a-z0-9-], porque o host de uma URL não aceita mais nada. [HS]CPU vira nui://hs-cpu/, e o cliente escreve um aviso no log quando o nome precisou mudar. Se dois resources reduzirem para o mesmo host, o segundo ganha o sufixo -2.
Nunca escreva esse host na mão. A página pode perguntar:
var resource = GetParentResourceName(); // "hs-cpu"GetParentResourceName() é injetada em todo frame de NUI, igual ao FiveM.
Os caminhos dentro da página funcionam como você espera: um <img src="images/logo.png"> ao lado de client/index.html resolve para nui://<resource>/client/images/logo.png, e esse arquivo precisa estar em files.
Lua → página: sendNuiMessage
Seção intitulada “Lua → página: sendNuiMessage”sendNuiMessage({ type = "money", value = 1500 })A página recebe um MessageEvent de verdade:
window.addEventListener("message", function (event) { if (event.data.type === "money") { document.getElementById("money").textContent = event.data.value; }});Uma tabela é serializada para JSON por você. Uma string passa intacta, o que ajuda quando o JSON já está pronto:
sendNuiMessage(toJSON({ type = "money", value = 1500 }))Página → Lua: registerNuiCallback
Seção intitulada “Página → Lua: registerNuiCallback”A página faz um POST para um caminho da própria origem; o handler Lua com aquele nome roda, e o que ele responder volta como corpo da resposta.
registerNuiCallback("buy", function(data, cb) if getPlayerMoney() < data.price then cb({ ok = false, reason = "dinheiro insuficiente" }) return end takePlayerMoney(data.price) cb({ ok = true, left = getPlayerMoney() })end)fetch("/buy", { method: "POST", body: JSON.stringify({ item: "colete", price: 200 }),}) .then(function (r) { return r.json(); }) .then(function (resposta) { if (!resposta.ok) alert(resposta.reason); });As duas URLs abaixo chegam no mesmo callback, então use a que preferir:
| Forma | Exemplo |
|---|---|
| Nua (estilo FiveM) | fetch("/buy", …) ou fetch("nui://" + GetParentResourceName() + "/buy", …) |
| Explícita | fetch("/__nui/buy", …) |
O handler
Seção intitulada “O handler”| Parâmetro | O que é |
|---|---|
data | O corpo da requisição, decodificado de JSON para tabela. Corpo vazio ou inválido devolve uma tabela vazia, nunca nil. |
cb | Uma função que você chama uma vez com a resposta. Tabela vira JSON; chamar sem nada envia {}. |
O cb não precisa ser chamado dentro do handler — você pode guardar ele e responder de um timer, depois da ida e volta de um triggerServerEvent, quando quiser. Chamadas depois da primeira são ignoradas.
registerNuiCallback("profile", function(data, cb) -- responde depois, quando o servidor voltar pendentes[data.id] = cb triggerServerEvent("ui:profile", localPlayer, data.id)end)Dois comportamentos que vale saber:
- Nome sem callback registrado devolve 404 na hora. O
fetchrejeita em vez de pendurar para sempre. - Callback que nunca responde estoura em 10 segundos e o
fetchresolve com{}. Umcbesquecido deixa a página lenta; não trava ela.
Registrar o mesmo nome de novo substitui o handler anterior.
Foco, input e cursor
Seção intitulada “Foco, input e cursor”Por padrão a camada é desenhada mas ignora o mouse por completo — o clique atravessa para o jogo. setNuiFocus é o que muda isso.
setNuiFocus(true, true) -- a página recebe teclado e mouse, e o cursor aparecesetNuiFocus(false, false) -- de volta para o jogo| Chamada | Efeito |
|---|---|
setNuiFocus(true) | Foco e cursor — o segundo argumento assume o valor do primeiro. |
setNuiFocus(true, false) | A página recebe input, mas sem cursor na tela (menu controlado por teclado). |
setNuiFocus(false) | Libera o foco. |
Enquanto a página está com foco, o jogo para de receber aquele input — o jogador não anda nem atira através do seu menu. Quando você quer os dois ao mesmo tempo, avise:
setNuiFocus(true, true)setNuiFocusKeepInput(true) -- o jogo continua lendo o teclado tambémisNuiFocused() e isNuiFocusKeepingInput() leem o estado atual de volta, e getNuiCursorPosition() devolve o cursor em pixels de tela — ou false quando não há cursor.
Trocar a página
Seção intitulada “Trocar a página”setNuiUrl aponta a sua camada para outra página do seu próprio resource:
setNuiUrl("nui://minha-ui/client/settings.html")Ela é recusada — devolvendo false — se a URL estiver fora da origem nui:// do próprio resource. Você não consegue apontar a camada para a página de outro resource, para um site, nem para um arquivo local.
getNuiUrl() devolve a URL carregada no momento, ou false quando o resource não tem ui_page.
DevTools
Seção intitulada “DevTools”Inspecionar é liberado nas páginas de servidor, e só nelas. Dois caminhos:
setNuiDevTools(true) -- pelo scriptnui_devtools -- no console in-game (F8)Os dois alternam a mesma janela, e chamar repetidamente não empilha janelas. O DevTools abre uma janela real do sistema, então ela não aparece por cima de tela cheia exclusiva — rode o jogo em janela ou sem bordas enquanto estiver depurando.
O menu do MTAX nunca é inspecionável, por caminho nenhum.
Loadscreen
Seção intitulada “Loadscreen”Um resource que declara loadscreen = true é baixado antes de todos os outros, e o ui_page dele é exibido enquanto o jogador conecta. A flag é só true/false — o HTML continua indo em ui_page.
A página de carregamento recebe o progresso do download como mensagem, sem Lua nenhum no meio:
window.addEventListener("message", function (event) { if (event.data.type === "loadProgress") { var prontos = event.data.total - event.data.pending; barra.style.width = (100 * prontos / event.data.total) + "%"; }});A loadscreen é aposentada quando o download inicial termina. Reiniciar o resource depois não traz ela de volta.
O que a página pode e o que não pode
Seção intitulada “O que a página pode e o que não pode”Arquivos locais (nui:// do seu resource) | Pode |
data:, blob: | Pode |
Imagem, folha de estilo, fonte remota e fetch/XHR para uma API HTTP | Pode |
Navegar para um site, ou <script src="https://…"> remoto | Bloqueado |
| Ler a página de outro resource, ou o menu do MTAX | Bloqueado — origens separadas |
| Qualquer coisa sobre a conta ou a sessão MTAX do jogador | Bloqueado — o browser de NUI não tem ponte nativa nenhuma |
A depuração remota por linha de comando é removida no boot, então nem uma flag de inicialização alcança o browser da interface.
Vindo do MTA
Seção intitulada “Vindo do MTA”| MTA | MTAX |
|---|---|
createBrowser + dxDrawImage(browser, …) | ui_page = "client/index.html" no manifesto |
loadBrowserURL / getBrowserURL | setNuiUrl / getNuiUrl — dentro da própria origem |
executeBrowserJavascript | sendNuiMessage → window.addEventListener("message", …) |
setBrowserAjaxHandler | registerNuiCallback ← fetch("/nome", { method: "POST" }) |
focusBrowser / isBrowserFocused | setNuiFocus / isNuiFocused |
injectBrowserMouseDown e companhia | Nada a fazer — o engine roteia o input real enquanto a página está com foco |
toggleBrowserDevTools | setNuiDevTools, ou nui_devtools no console |
requestBrowserDomains / isBrowserDomainBlocked | Não existe: as regras acima são fixas e não scriptáveis |
resizeBrowser | A camada é sempre tela cheia; dimensione a interface com CSS |
guiCreateBrowser / guiGetBrowser | Saíram junto com o resto da família gui* do CEGUI |
Limites de relance
Seção intitulada “Limites de relance”| Mensagens na fila antes de a página existir | 64 por resource, descartando a mais antiga |
| Callback sem handler | HTTP 404 |
| Callback que nunca responde | Resolve com {} depois de 10 s |
| Tamanho da camada | Sempre a tela inteira |
| Foco | Um dono só para o cliente inteiro |
Exemplo completo
Seção intitulada “Exemplo completo”-- client/main.lualocal aberto = false
local function setAberto(valor) aberto = valor sendNuiMessage({ type = valor and "open" or "close" }) setNuiFocus(valor, valor)end
addCommandHandler("shop", function() setAberto(not aberto) end)
registerNuiCallback("buy", function(data, cb) triggerServerEvent("shop:buy", localPlayer, data.item) cb({ ok = true })end)
registerNuiCallback("close", function(_, cb) setAberto(false) cb({ ok = true })end)
-- Nunca deixe o jogador preso com foco numa página que não existe mais.addEventHandler("onClientResourceStop", resourceRoot, function() setNuiFocus(false, false)end)Veja também
Seção intitulada “Veja também”- Funções de NUI — a referência completa das dez funções desta página.
- Resources —
ui_page,fileseloadscreenno manifesto.