Pular para o conteúdo

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.

O cliente roda dois browsers separados:

Browser da interfaceBrowser de NUI
O que desenhaO menu do MTAX, o server browser, as configurações, o console in-gameAs suas páginas — uma por resource com ui_page, mais a loadscreen
Ponte nativaSim — é ela que fala com a conta, a sessão e o launcherNenhuma
DevToolsNuncaLiberado
CamadaPor cimaPor 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.

Três peças: a página, a entrada no manifesto e o script.

client/index.html
<!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>
-- mtaxmanifest.lua
resource_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.

-- client/main.lua
local 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 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.

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 }))

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:

FormaExemplo
Nua (estilo FiveM)fetch("/buy", …) ou fetch("nui://" + GetParentResourceName() + "/buy", …)
Explícitafetch("/__nui/buy", …)
ParâmetroO que é
dataO corpo da requisição, decodificado de JSON para tabela. Corpo vazio ou inválido devolve uma tabela vazia, nunca nil.
cbUma 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 fetch rejeita em vez de pendurar para sempre.
  • Callback que nunca responde estoura em 10 segundos e o fetch resolve com {}. Um cb esquecido deixa a página lenta; não trava ela.

Registrar o mesmo nome de novo substitui o handler anterior.

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 aparece
setNuiFocus(false, false) -- de volta para o jogo
ChamadaEfeito
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ém

isNuiFocused() e isNuiFocusKeepingInput() leem o estado atual de volta, e getNuiCursorPosition() devolve o cursor em pixels de tela — ou false quando não há cursor.

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.

Inspecionar é liberado nas páginas de servidor, e só nelas. Dois caminhos:

setNuiDevTools(true) -- pelo script
nui_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.

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.

Arquivos locais (nui:// do seu resource)Pode
data:, blob:Pode
Imagem, folha de estilo, fonte remota e fetch/XHR para uma API HTTPPode
Navegar para um site, ou <script src="https://…"> remotoBloqueado
Ler a página de outro resource, ou o menu do MTAXBloqueado — origens separadas
Qualquer coisa sobre a conta ou a sessão MTAX do jogadorBloqueado — 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.

MTAMTAX
createBrowser + dxDrawImage(browser, …)ui_page = "client/index.html" no manifesto
loadBrowserURL / getBrowserURLsetNuiUrl / getNuiUrl — dentro da própria origem
executeBrowserJavascriptsendNuiMessage → window.addEventListener("message", …)
setBrowserAjaxHandlerregisterNuiCallback ← fetch("/nome", { method: "POST" })
focusBrowser / isBrowserFocusedsetNuiFocus / isNuiFocused
injectBrowserMouseDown e companhiaNada a fazer — o engine roteia o input real enquanto a página está com foco
toggleBrowserDevToolssetNuiDevTools, ou nui_devtools no console
requestBrowserDomains / isBrowserDomainBlockedNão existe: as regras acima são fixas e não scriptáveis
resizeBrowserA camada é sempre tela cheia; dimensione a interface com CSS
guiCreateBrowser / guiGetBrowserSaíram junto com o resto da família gui* do CEGUI
Mensagens na fila antes de a página existir64 por resource, descartando a mais antiga
Callback sem handlerHTTP 404
Callback que nunca respondeResolve com {} depois de 10 s
Tamanho da camadaSempre a tela inteira
FocoUm dono só para o cliente inteiro
-- client/main.lua
local 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)
  • Funções de NUI — a referência completa das dez funções desta página.
  • Resources — ui_page, files e loadscreen no manifesto.