$ USD
  • $ USD
  • € EUR
  • Britse pond (£ GBP)
  • $ AUD
  • R$ BRL
  • CHF CHF
  • ¥ JPY
Ontwikkel een aangepaste FiveM-telefoonapp

Ontwikkel een aangepaste FiveM-telefoonapp

Doel
Maak een productieklare in-game smartphone voor FiveM met behulp van NUI + React. Je zult een resource opzetten, QBCore/ESX events aansluiten, gegevens in MySQL opslaan, en een soepele UI leveren die voldoet aan prestatiebudgetten.


Voorwaarden

  1. Een draaiende FiveM server met txAdmin en MySQL (oxmysql or mysql-async).
  2. Node.js 18+ en pnpm of npm op je ontwikkel-pc.
  3. Eén framework geïnstalleerd: QBCore of ESX.
  4. Aanbevolen bibliotheken: ox_lib (callbacks, meldingen), ox_inventory (optioneel voor telefoonitem), ox_target (optioneel voor wereldinteracties).
  5. Basis React-kennis.

Documentatie

Interne uitlezing (FiveMX)


Architectuur

  1. Resource my_phone met fxmanifest.lua, client, server, en ui bundel.
  2. UI: React-app gebouwd met Vite in /ui/dist. NUI communiceert met Lua via postMessage + RegisterNUICallback.
  3. Data: MySQL-tabellen voor phone_contacts, phone_messages, phone_calls.
  4. Framework lijm: QBCore of ESX item bruikbare handler schakelt de telefoon in, en server-callbacks laden/slaan gegevens op.

Gebeurtenisstroom

  1. Speler drukt op een toets of gebruikt het telefoonitem → 2) SetNuiFocus(true, true) En SendNUIMessage({ action = 'open' }) → 3) React toont UI → 4) UI vraagt gegevens op via fetch('https://my_phone/xyz') (NUI) → 5) RegisterNUICallback('xyz', ...) wordt uitgevoerd op client/server → 6) Server leest/schrijft DB → 7) Antwoord keert terug naar UI → 8) Sluit telefoon en geef focus vrij.

Stap 1 — De resource opzetten

Mapindeling

resources/
 [local]/
 my_phone/
 fxmanifest.lua
 client/
 main.lua
 server/
 main.lua
 ui/
 index.html
 src/
 main.tsx
 App.tsx
 api.ts
 styles.css

fxmanifest.lua

fx_version 'cerulean'
game 'gta5'

ui_page 'ui/dist/index.html'

files { 'ui/dist/**' }

client_scripts { 'client/main.lua' }
server_scripts {
 '@oxmysql/lib/MySQL.lua',
 'server/main.lua'
}

lua54 'yes'

Stap 2 — Maak de React NUI

Initialiseer een Vite React-app in my_phone/ui en bouw naar ui/dist.

cd my_phone/ui
pnpm create vite@latest . --template react-ts
pnpm i

Vite configuratie (zorg dat assets in dist)

// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
 plugins: [react()],
 build: { outDir: 'dist', emptyOutDir: true },
 base: ''
})

NUI brug

// src/api.ts
export async function nui<T>(event: string, data?: unknown): Promise<T> {
 const res = await fetch(`https://my_phone/${event}`, {
 method: 'POST',
 headers: { 'Content-Type': 'application/json' },
 body: JSON.stringify(data ?? {})
 })
 return await res.json()
}

Mount React + berichtenluisteraar

// src/main.tsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'

const root = ReactDOM.createRoot(document.getElementById('root')!)
root.render(<App />)

window.addEventListener('message', (e) => {
 if (e.data?.action === 'open') document.body.classList.add('open')
 if (e.data?.action === 'close') document.body.classList.remove('open')
})

Basis UI

// src/App.tsx
import { useEffect, useState } from 'react'
import { nui } from './api'

type Contact = { id: number; name: string; number: string }

export default function App() {
 const [contacts, setContacts] = useState<Contact[]>([])
 const [visible, setVisible] = useState(false)

 useEffect(() => {
 const handler = (e: MessageEvent) => {
 if (e.data?.action === 'open') {
 setVisible(true)
 nui<Contact[]>('contacts:list').then(setContacts)
 }
 if (e.data?.action === 'close') setVisible(false)
 }
 window.addEventListener('message', handler)
 return () => window.removeEventListener('message', handler)
 }, [])

 if (!visible) return null
 return (
 <div className="phone">
 <header>Phone</header>
 <section>
 {contacts.map(c => (
 <div key={c.id} className="row">
 <div>{c.name}</div>
 <div>{c.number}</div>
 </div>
 ))}
 </section>
 <footer>
 <button onClick={() => nui('ui:close')}>Close</button>
 </footer>
 </div>
 )
}

index.html

<!doctype html>
<html>
 <head>
 <meta charset="utf-8" />
 <meta name="viewport" content="width=device-width, initial-scale=1" />
 <title>my_phone</title>
 <link rel="stylesheet" href="/src/styles.css" />
 </head>
 <body>
 <div id="root"></div>
 <script type="module" src="/src/main.tsx"></script>
 </body>
</html>

Bouw de UI:

pnpm build

Stap 3 — Client: open/sluit, NUI focus, callbacks

-- client/main.lua
local open = false

local function openPhone()
 if open then return end
 open = true
 SetNuiFocus(true, true)
 SendNUIMessage({ action = 'open' })
end

local function closePhone()
 if not open then return end
 open = false
 SetNuiFocus(false, false)
 SendNUIMessage({ action = 'close' })
end

-- Keybind (F1 example)
RegisterCommand('myphone', function()
 if open then closePhone() else openPhone() end
end)
RegisterKeyMapping('myphone', 'Toggle Phone', 'keyboard', 'F1')

-- NUI → game callbacks
RegisterNUICallback('ui:close', function(_, cb)
 closePhone()
 cb({ ok = true })
end)

-- list contacts asks the server
RegisterNUICallback('contacts:list', function(_, cb)
 lib.callback('my_phone:server:getContacts', false, function(rows)
 cb(rows)
 end)
end)

Tip: schakel NUI ontwikkeltools in spelconsole in met nui_devTools. Open http://localhost:13172 in uw Chromium-browser om de UI te inspecteren.


Stap 4 — Server: DB-schema + callbacks

SQL (MySQL)

CREATE TABLE IF NOT EXISTS phone_contacts (
 id INT AUTO_INCREMENT PRIMARY KEY,
 citizenid VARCHAR(64) NOT NULL,
 name VARCHAR(64) NOT NULL,
 number VARCHAR(32) NOT NULL,
 INDEX(citizenid)
);

CREATE TABLE IF NOT EXISTS phone_messages (
 id BIGINT AUTO_INCREMENT PRIMARY KEY,
 owner VARCHAR(64) NOT NULL,
 peer VARCHAR(64) NOT NULL,
 body TEXT NOT NULL,
 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
 INDEX(owner), INDEX(peer)
);

Server met oxmysql + ox_lib

-- server/main.lua
local QBCore = exports['qb-core'] and exports['qb-core']:GetCoreObject()
ESX = ESX or nil

if not QBCore then
 TriggerEvent('esx:getSharedObject', function(obj) ESX = obj end)
end

-- Load contacts for the logged-in character
lib.callback.register('my_phone:server:getContacts', function(source)
 local citizenid
 if QBCore then
 local Player = QBCore.Functions.GetPlayer(source)
 citizenid = Player and Player.PlayerData.citizenid
 else
 local xPlayer = ESX.GetPlayerFromId(source)
 citizenid = xPlayer and xPlayer.identifier
 end
 if not citizenid then return {} end

 local rows = MySQL.query.await('SELECT id, name, number FROM phone_contacts WHERE citizenid = ?', { citizenid })
 return rows or {}
end)

-- Save a contact
lib.callback.register('my_phone:server:addContact', function(source, contact)
 if type(contact) ~= 'table' then return { ok = false } end
 local name, number = contact.name, contact.number
 if not name or not number then return { ok = false } end

 local citizenid
 if QBCore then
 local Player = QBCore.Functions.GetPlayer(source)
 citizenid = Player and Player.PlayerData.citizenid
 else
 local xPlayer = ESX.GetPlayerFromId(source)
 citizenid = xPlayer and xPlayer.identifier
 end
 if not citizenid then return { ok = false } end

 MySQL.insert.await('INSERT INTO phone_contacts (citizenid, name, number) VALUES (?, ?, ?)', { citizenid, name, number })
 return { ok = true }
end)

Stap 5 — Framework-integratie (item + rechten)

QBCore

Voeg een bruikbaar telefoonitem toe en schakel de UI in wanneer gebruikt.

-- server/main.lua (QBCore only)
if QBCore then
 QBCore.Functions.CreateUseableItem('phone', function(src, item)
 TriggerClientEvent('my_phone:client:toggle', src)
 end)
end
-- client/main.lua
RegisterNetEvent('my_phone:client:toggle', function()
 if IsPauseMenuActive() then return end
 if IsPedInAnyVehicle(PlayerPedId(), false) then -- optional rule
 -- show a notification via ox_lib
 lib.notify({ title = 'Phone', description = 'No phone while driving.', type = 'error' })
 return
 end
 if IsNuiFocused() then ExecuteCommand('myphone') else ExecuteCommand('myphone') end
end)

ESX

-- server/main.lua (ESX only)
if ESX and not QBCore then
 ESX.RegisterUsableItem('phone', function(playerId)
 TriggerClientEvent('my_phone:client:toggle', playerId)
 end)
end

Als u ox_inventory, maak het item daar aan en vertrouw op de bruikbare handlers. U kunt nog steeds hetzelfde client-event triggeren.


Stap 6 — Kernfuncties

Implementeer kleine onderdelen en lever incrementeel.

Contacten

  1. UI-gesprekken contacts:list → server retourneert rijen.
  2. Voeg “Contact toevoegen”-formulier toe → oproep addContact.
  3. Voeg “Contact verwijderen” toe → server verwijdert op basis van id met controle op burger-eigendom.

Berichten (SMS)

  1. Tabel phone_messages slaat eigenaar, peer, body op.
  2. UI opent een chat, roept messages:list En messages:send.
  3. Server voegt bericht in, zendt optioneel client-event naar peer indien online.

Server-schets

lib.callback.register('my_phone:server:messages:list', function(source, peer)
 local cid = GetCitizenId(source)
 return MySQL.query.await('SELECT * FROM phone_messages WHERE owner=? AND peer=? ORDER BY id DESC LIMIT 200', { cid, peer }) or {}
end)

RegisterNetEvent('my_phone:server:messages:send', function(peer, body)
 local src = source
 local cid = GetCitizenId(src)
 if type(body) ~= 'string' or #body == 0 or #body > 500 then return end
 MySQL.insert.await('INSERT INTO phone_messages (owner, peer, body) VALUES (?, ?, ?)', { cid, peer, body })
 TriggerClientEvent('my_phone:client:messages:push', src, peer, body)
 -- optional: find target player by phone number and push live event
end)

Client ontvangt

RegisterNetEvent('my_phone:client:messages:push', function(peer, body)
 SendNUIMessage({ action = 'message:new', peer = peer, body = body })
end)

Oproepen (optionele MVP)

  • Sla alleen oproeplogboeken op. Echte audio gebruikt je voice-plugin (pma-voice, mumble, SaltyChat) en valt buiten deze MVP.
  • Voeg UI toetsenbord toe → bij draaien, log een uitgaande oproep; bij beantwoorden, log inkomende. Je kunt later integreren met de API van een voice-plugin.

Stap 7 — Beveiliging, UX, prestaties

Beveiliging

  1. Vertrouw nooit NUI-invoer. Valideer typen en lengte op de server.
  2. Controleer eigendom bij elke query met citizenid of identifier.
  3. Vermijd het blootstellen van identifiers aan andere clients. Gebruik server relays.

UX

  1. Annuleer telefoon terwijl neergeslagen, geboeid of rijdend, als je serverregels dit vereisen.
  2. Houd UI snel. Gebruik optimistische updates en reconcilieer op server-ack.

Prestaties

  1. Houd NUI inactief. Vermijd setInterval loops in React. Gebruik effects en events.
  2. Houd bundels klein. Laad zware schermen lui. Verzend gecomprimeerde assets.
  3. Gebruik Resmon om gemiddeld onder 0,01–0,02 ms te budgetteren. Zie de FiveMX-gids hierboven gelinkt.

Stap 8 — Testen en debuggen

  1. Start resource in server.cfg vóór afhankelijke scripts.
ensure my_phone
  1. In het spel, druk op F8 → voer uit nui_devTools → open http://localhost:13172 en kies je NUI-pagina.
  2. Inspecteer het netwerktabblad. Elke NUI → Lua-aanroep raakt https://my_phone/<name> eindpunten.
  3. Gebruik /myphone commando en bevestig dat focus wisselt.
  4. Voer Resmon uit en verifieer dat de CPU laag blijft terwijl de telefoon open en gesloten is.

Stap 9 — Verpakken en updates

  1. Commit ui/ bron en ui/dist/ build.
  2. In CI, voer uit pnpm --filter ui build en verzend alleen dist in releases.
  3. Versiebeheer je SQL-migraties. Verwijder nooit gebruikersgegevens zonder back-ups.

Stap 10 — Extensies die je vervolgens kunt toevoegen

  1. Bankieren: link naar de bankbron van je server; toon saldo en overschrijvingen.
  2. Tweets/Advertenties: wereldwijde feed met snelheidslimieten en moderatie.
  3. Marketplace: aanbiedingen met escrow.
  4. Sollicitaties: politie/EMS MDT-haken.
  5. Foto's: screenshot-integratie via server-eindpunt, geen data-URL's.
  6. Instellingen: dynamische thema's, ringtones, achtergronden.

Probleemoplossing

Telefoon opent achter pauzemenu
Uitschakelen tijdens pauzecontroles en opnieuw inschakelen wanneer actief terugkeert.

NUI callback wordt niet geactiveerd

  • Zorg ervoor dat RegisterNUICallback('event', ...) namen komen overeen met het UI-ophaalpad.
  • Bevestig fx_version is cerulean En ui_page wijst naar ui/dist/index.html.
  • Controleer de F8-console op CORS- of JSON-fouten.

Items niet bruikbaar

  • QBCore: bevestigen QBCore.Functions.CreateUseableItem draait en phone bestaat in je itemlijst.
  • ESX: bevestig ESX.RegisterUsableItem('phone', ...) registreert na het laden van inventaris.

Databasefouten

  • Zorg ervoor dat oxmysql is gestart vóór deze resource.
  • Controleer kolomgroottes en coderingen voor Unicode-namen.

Referentie snippets (kopieer‑plak)

Helper

function GetCitizenId(source)
 if QBCore then
 local P = QBCore.Functions.GetPlayer(source)
 return P and P.PlayerData.citizenid
 else
 local xP = ESX.GetPlayerFromId(source)
 return xP and xP.identifier
 end
end

Contact toevoegen vanuit UI

// UI
async function addContact(name: string, number: string) {
 const res = await nui<{ ok: boolean }>('contacts:add', { name, number })
 if (res.ok) {
 const next = await nui<any[]>('contacts:list')
 // update state
 }
}
-- client
RegisterNUICallback('contacts:add', function(data, cb)
 lib.callback('my_phone:server:addContact', false, function(resp)
 cb(resp)
 end, data)
end)

Wat je hebt gebouwd

  • Een gerichte telefoon-MVP met contacten en berichten.
  • Een schone NUI-brug die op beide frameworks werkt.
  • Een DB-laag die je veilig kunt uitbreiden.

Verzenden, prestaties meten en itereren.


Verder lezen


Meta

  • Doel resmon: ≤0.02 ms gemiddeld in rust.
  • Bundelbudget: ≤250 KB gezipt voor MVP.
  • UI FPS: 60.