FiveM NUI con React e Vite: configurazione, callback e sviluppo browser

Costruisci una NUI FiveM con React e Vite: ui_page e file in fxmanifest, base './', SendNUIMessage, RegisterNUICallback, SetNuiFocus e sviluppo nel browser.

L'HTML semplice funziona per una piccola NUI, ma un'interfaccia vera è più facile con React. La parte che confonde le persone non è React, è la colla: il manifest, i percorsi di output della build, e come i messaggi vanno avanti e indietro.

Questo articolo configura una NUI React e Vite che si carica in FiveM, parla a Lua in entrambe le direzioni, e può essere costruita in una scheda del browser normale.

Layout del progetto

Metti l'interfaccia in una cartella web dentro la risorsa e creala con Vite:

bash
cd my_script
npm create vite@latest web -- --template react
cd web
npm install

Il risultato:

text
my_script/
  fxmanifest.lua
  client/main.lua
  web/
    package.json
    vite.config.js
    src/
    dist/        (created by the build)

vite.config: base './'

FiveM carica la tua pagina da un indirizzo nui://, non dalla radice di un server web. I percorsi di asset predefiniti di Vite iniziano con /, che poi non puntano a nulla e ti lasciano con una pagina vuota. Rendili relativi:

js
// web/vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  base: './',
  build: { outDir: 'dist' },
})

Poi costruisci:

bash
npm run build

fxmanifest.lua

Punta ui_page all'index.html costruito e spedisci l'intera cartella dist al giocatore con files:

lua
fx_version 'cerulean'
game 'gta5'

ui_page 'web/dist/index.html'

files {
    'web/dist/index.html',
    'web/dist/**',
}

client_script 'client/main.lua'

Solo i file elencati in files vengono inviati ai giocatori. Se un'immagine o un font manca in gioco, il glob del file non l'ha incluso. Tieni node_modules e src fuori dall'elenco: i giocatori non ne hanno bisogno.

Lua all'interfaccia: SendNUIMessage

Lo script del client invia un messaggio e dà il focus all'interfaccia:

lua
local open = false

local function setOpen(state)
    open = state
    SetNuiFocus(state, state)
    SendNUIMessage({ action = 'setVisible', visible = state, data = { name = 'Mic' } })
end

RegisterCommand('myui', function()
    setOpen(not open)
end, false)

SetNuiFocus(hasFocus, hasCursor) dà la tastiera e il mouse alla pagina. Dai sempre indietro, o il cursore rimane bloccato: vedi cursore bloccato NUI focus.

In React, ascolta il messaggio:

jsx
import { useEffect, useState } from 'react'

export default function App() {
  const [visible, setVisible] = useState(false)
  const [name, setName] = useState('')

  useEffect(() => {
    const onMessage = (event) => {
      const { action, visible, data } = event.data
      if (action === 'setVisible') {
        setVisible(visible)
        if (data) setName(data.name)
      }
    }
    window.addEventListener('message', onMessage)
    return () => window.removeEventListener('message', onMessage)
  }, [])

  if (!visible) return null
  return <div className="panel">Hello {name}</div>
}

Una pagina che non rende nulla quando è nascosta è il modello usuale, poiché la pagina NUI stessa rimane caricata.

Interfaccia a Lua: fetch e RegisterNUICallback

Per l'altra direzione, la pagina POSTs a un URL sulla tua risorsa. GetParentResourceName() è una funzione che FiveM fornisce alla pagina e restituisce il nome della risorsa:

js
export async function nuiPost(eventName, data = {}) {
  const resource = window.GetParentResourceName ? window.GetParentResourceName() : 'my_script'
  const resp = await fetch(`https://${resource}/${eventName}`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json; charset=UTF-8' },
    body: JSON.stringify(data),
  })
  return resp.json()
}
jsx
<button onClick={() => nuiPost('close')}>Close</button>

In Lua, gestiscilo. Il callback deve essere chiamato, o la richiesta rimane in sospeso:

lua
RegisterNUICallback('close', function(data, cb)
    setOpen(false)   -- gives focus back
    cb({ ok = true })
end)

data è il corpo JSON decodificato e cb risponde al fetch. Tutto ciò che passi a cb torna come risposta.

Sviluppa nel browser

Ricostruire e riavviare la risorsa per ogni cambio CSS è lento. Esegui Vite e lavora in una scheda del browser normale:

bash
npm run dev

Fuori dal gioco, window.GetParentResourceName non esiste e nessuno ti invia messaggi, quindi simula entrambi:

js
export const isBrowser = !window.invokeNative

// in main.jsx, in dev only
if (isBrowser) {
  setTimeout(() => {
    window.postMessage({ action: 'setVisible', visible: true, data: { name: 'Mic' } }, '*')
  }, 500)
}

La funzione nuiPost sopra già ritorna a un nome di risorsa predefinito. Nel browser, la richiesta semplicemente fallisce, quindi catturala o salta la chiamata quando isBrowser è vero. Usa uno sfondo scuro o a scacchi nello sviluppo, poiché il vero gioco mostra il mondo dietro la pagina.

Iterazione dentro il gioco

Per un controllo finale nel gioco, costruisci e riavvia solo la risorsa:

bash
npm run build

quindi ensure my_script nella console del server. Con vite build --watch, i file si ricostruiscono ogni volta che salvi, e riavvi solo la risorsa.

Problemi comuni

  • Pagina vuota. Mancante base: './', o files non include web/dist/**.
  • Nulla accade al clic. Il nome RegisterNUICallback non corrisponde all'URL del fetch, o cb non viene mai chiamato.
  • Cursore bloccato dopo la chiusura. Mancante SetNuiFocus(false, false) su ogni percorso di chiusura, incluso un riavvio della risorsa.
  • Font e immagini mancanti nel gioco. Sono referenziati da percorso assoluto, o non sono in dist.
  • Il messaggio non arriva mai. La pagina non è stata ancora caricata, o i nomi action differiscono tra Lua e il listener.

Elenco di controllo

Sintomo Soluzione
NUI vuota nel gioco, funziona nel browser base: './' in vite.config, quindi npm run build
File mancanti per i giocatori files { 'web/dist/**' } in fxmanifest.lua
Il pulsante non fa nulla Abbina il nome RegisterNUICallback e chiama cb
Impossibile raggiungere il gioco dalla pagina fetch a https://${GetParentResourceName()}/name
Il cursore non torna SetNuiFocus(false, false) quando chiude
Sviluppo senza il gioco Esegui vite, simula postMessage e GetParentResourceName

Risposte rapide

Perché la mia NUI React è una pagina vuota in FiveM?

Di solito la build usa percorsi di asset assoluti. Imposta base: './' in vite.config, ricostruisci, e assicurati che files in fxmanifest.lua includa l'intera cartella web/dist.

Come l'interfaccia invia dati al gioco?

Con un POST fetch a https://${GetParentResourceName()}/eventName, gestito in Lua da RegisterNUICallback('eventName', ...), che deve chiamare il suo callback.

Posso sviluppare la NUI senza avviare FiveM?

Sì. Esegui vite nel browser, rileva che sei fuori dal gioco, e invia messaggi falsi con window.postMessage per simulare ciò che Lua invierebbe.

Script senza questo problema

Mic PhoneUn telefono pieghevole che si apre in un tablet e arriva sul telefono vero del giocatore.Vedi script →Clothing DesignerDisegna vestiti dentro FiveM — pennello, livelli, import di immagini e IA — e poi indossali.Vedi script →Arcade MachinesSette giochi arcade giocabili in cabinati veri, con classifiche e scommesse.Vedi script →

Continua a leggere