Hedef
NUI + React kullanarak FiveM için üretime hazır bir oyun içi akıllı telefon oluşturun. Bir kaynak iskeleti oluşturacak, QBCore\/ESX olaylarını bağlayacak, MySQL'de veri kalıcı hale getirecek ve performans bütçelerine saygılı, akıcı bir UI teslim edeceksiniz.
Ön koşullar
- Çalışan bir FiveM sunucusu txAdmin ve MySQL ile (oxmysql veya mysql-async).
- Node.js 18+ ve geliştirme PC'nizde pnpm veya npm.
- Bir framework yüklü: QBCore veya ESX.
- Önerilen kütüphaneler: ox_kütüphane (geri aramalar, bildirimler), öküz_envanteri (telefon öğesi için isteğe bağlı), öküz_hedefi (dünya etkileşimleri için isteğe bağlı).
- Temel React bilgisi.
Dokümanlar
- Cfx.re NUI genel bakış – https://docs.fivem.net/docs/scripting-manual/nui-development/
- NUI geri aramalar – https://docs.fivem.net/docs/scripting-manual/nui-development/nui-callbacks/
- SendNUIMessage – https://docs.fivem.net/docs/scripting-reference/runtimes/lua/functions/SendNUIMessage/
- NUI geliştirici araçlarını hata ayıklama – https://docs.fivem.net/docs/scripting-manual/nui-development/full-screen-nui/
- QBCore sunucu fonksiyonları – https://docs.qbcore.org/qbcore-documentation/qb-core/server-function-reference
- ESX
RegisterUsableItem– https://docs.esx-framework.org/en/esx_core/es_extended/server/functions - ox_lib geri aramalar – https://overextended.dev/ox_lib
Dahili okuma (FiveMX)
- Resmon ve performans – https://fivemx.com/how-to-use-resmon-in-fivem-optimize-resources/
- Performans merkezi – https://fivemx.com/performance/
- Telefon betikleri pazarına genel bakış – https://fivemx.com/phone-scripts/
Mimarlık
- Kaynak
my_phoneilefxmanifest.lua,client,server, Veuipaket. - Kullanıcı arayüzü: Vite ile oluşturulmuş React uygulaması
/ui/dist. NUI, Lua ile şu yolla konuşur:postMessage+RegisterNUICallback. - Veri: MySQL tabloları şunlar için:
phone_contacts,phone_messages,phone_calls. - Framework bağlayıcısı: QBCore veya ESX öğe kullanılabilir işleyicisi, telefonu açar/kapatır ve sunucu geri çağrıları veri yükler/kaydeder.
Olay akışı
- Oyuncu bir tuşa basar veya telefon öğesini kullanır → 2)
SetNuiFocus(true, true)VeSendNUIMessage({ action = 'open' })→ 3) React, UI'yi gösterir → 4) UI şu yolla veri talep eder:fetch('https://my_phone/xyz')(NUI) → 5)RegisterNUICallback('xyz', ...)istemci/sunucuda çalışır → 6) Sunucu veritabanını okur/yazar → 7) Yanıt UI'ye döner → 8) Telefonu kapat ve odağı serbest bırak.
Adım 1 — Kaynağı oluştur
Klasör düzeni
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'
Adım 2 — React NUI'yi Oluşturun
İçinde bir Vite React uygulaması başlatın my_phone/ui ve şuraya derle ui/dist.
cd my_phone/ui pnpm create vite@latest . --template react-ts pnpm i
Vite yapılandırması (varlıkların şuraya yerleştiğinden emin olun: 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 köprüsü
// 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()
}
React + mesaj dinleyiciyi monte et
// 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')
})
Temel 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>
)
}
dizin.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>
UI'yi derleyin:
pnpm build
Adım 3 — İstemci: aç/kapat, NUI odak, geri çağrılar
-- 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)
İpucu: oyun konsolunda NUI geliştirici araçlarını etkinleştirmek için
nui_devTools. Açhttp://localhost:13172Chromium tarayıcınızda UI'yi incelemek için.
Adım 4 — Sunucu: DB şeması + geri çağrılar
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) );
oxmysql + ox_lib ile sunucu
-- 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)
Adım 5 — Çatı entegrasyonu (öğe + izinler)
QBCore
Kullanılabilir bir telefon öğesi ekleyin ve kullanıldığında UI'yi açıp kapatın.
-- 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
Eğer kullanıyorsanız öküz_envanteri, öğeyi orada oluşturun ve kullanılabilir işleyicilerine güvenin. Yine de aynı istemci olayını tetikleyebilirsiniz.
Adım 6 — Temel özellikler
Küçük parçalar uygulayın ve aşamalı olarak yayınlayın.
İletişim
- UI çağrıları
contacts:list→ sunucu satırları döndürür. - “Kişi ekle” formu ekleyin → çağrı
addContact. - “Kişiyi sil” ekleyin → sunucu, ile siler
idvatandaş sahipliği kontrolü ile.
Mesajlar (SMS)
- Masa
phone_messagessahibi, eş, gövdeyi saklar. - UI bir sohbet açar, arar
messages:listVemessages:send. - Sunucu mesajı ekler, isteğe bağlı olarak çevrimiçiyse eşe istemci olayı gönderir.
Sunucu taslağı
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)
İstemci alır
RegisterNetEvent('my_phone:client:messages:push', function(peer, body)
SendNUIMessage({ action = 'message:new', peer = peer, body = body })
end)
Aramalar (isteğe bağlı MVP)
- Yalnızca çağrı kayıtlarını saklayın. Gerçek ses, ses eklentinizi kullanır (pma-voice, mumble, SaltyChat) ve bu MVP'nin dışındadır.
- UI tuş takımı ekleyin → arama yapıldığında, giden bir aramayı kaydedin; cevaplandığında, gelen aramayı kaydedin. Daha sonra bir ses eklentisinin API'si ile entegre edebilirsiniz.
Adım 7 — Güvenlik, Kullanıcı Deneyimi, Performans
Güvenlik
- NUI girişine asla güvenmeyin. Sunucuda türleri ve uzunluğu doğrulayın.
- Her sorguda sahipliği şununla kontrol edin:
citizenidveyaidentifier. - Diğer istemcilere tanımlayıcıları ifşa etmekten kaçının. Sunucu rölelerini kullanın.
Kullanıcı Deneyimi
- Sunucu kurallarınız gerektiriyorsa, yere düşmüş, kelepçelenmiş veya araba kullanırken telefonu iptal edin.
- UI'yi hızlı tutun. İyimser güncellemeler kullanın ve sunucu onayında uzlaştırın.
Performans
- NUI'yi boşta tutun. React'te setInterval döngülerinden kaçının. Efektler ve olaylar kullanın.
- Paketleri küçük tutun. Ağır ekranları tembel yükleyin. Sıkıştırılmış varlıklar gönderin.
- Ortalama 0.01–0.02 ms'nin altında bütçe yapmak için Resmon kullanın. Yukarıda bağlantı verilen FiveMX kılavuzuna bakın.
Adım 8 — Test ve Hata Ayıklama
- Kaynağı şurada başlatın:
server.cfgbağımlı betiklerden önce.
ensure my_phone
- Oyunda, F8'e bas → çalıştır
nui_devTools→ açhttp://localhost:13172ve NUI sayfanı seç. - Ağ sekmesini incele. Her NUI → Lua çağrısı isabet eder
https://my_phone/<name>uç noktalarına. - Kullanmak
/myphonekomut ver ve odak geçişlerini onayla. - Resmon'u çalıştır ve telefon açık ve kapalıyken CPU'nun düşük kaldığını doğrula.
Adım 9 — Paketleme ve güncellemeler
- Commit
ui/kaynak veui/dist/derle. - CI'da, çalıştır
pnpm --filter ui buildve yalnızca gönderdistyayınlarda. - SQL geçişlerinizi sürümleyin. Yedekleme olmadan kullanıcı verilerini asla silmeyin.
Adım 10 — Sonra ekleyebileceğiniz uzantılar
- Bankacılık: sunucunuzun bankacılık kaynağına bağlantı; bakiye ve transferleri göster.
- Tweetler/Reklamlar: hız sınırlamaları ve moderasyon ile küresel akış.
- Pazar yeri: emanet ile listeleme.
- İş uygulamaları: polis\/EMS MDT kancaları.
- Fotoğraflar: sunucu uç noktası üzerinden ekran görüntüsü entegrasyonu, veri URL'leri değil.
- Ayarlar: dinamik temalar, zil sesleri, arka planlar.
Sorun giderme
Telefon duraklatma menüsünün arkasında açılıyor
Duraklatma kontrolleri sırasında devre dışı bırak ve aktif durum geri döndüğünde yeniden aç.
NUI geri çağrısı tetiklenmiyor
- Emin olmak
RegisterNUICallback('event', ...)isimler UI getirme yoluyla eşleşiyor. - Onaylamak
fx_versiondırceruleanVeui_pageişaret ediyorui/dist/index.html. - CORS veya JSON hataları için F8 konsolunu kontrol edin.
Öğeler kullanılamıyor
- QBCore: onayla
QBCore.Functions.CreateUseableItemçalışır vephoneöğe listenizde bulunur. - ESX: onayla
ESX.RegisterUsableItem('phone', ...)envanter yüklendikten sonra kaydeder.
Veritabanı hataları
- Bu kaynaktan önce oxmysql'in başlatıldığından emin olun.
- Unicode isimler için sütun boyutlarını ve kodlamalarını kontrol edin.
Referans parçacıkları (kopyala‑yapıştır)
Yardımcı
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
UI'den kişi ekle
// 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)
Ne inşa ettin
- Kişiler ve mesajlarla odaklanmış bir telefon MVP'si.
- Her iki framework'te de çalışan temiz bir NUI köprüsü.
- Güvenle genişletebileceğin bir DB katmanı.
Yayınla, performansı ölç ve yinele.
Daha fazla okuma
- FiveMX Resmon ve optimizasyon: https://fivemx.com/how-to-use-resmon-in-fivem-optimize-resources/
- Ses karşılaştırması (stack'ini seç): https://fivemx.com/fivem-voice-mumble-saltychat-pma-voice-guide/
- İlham alınacak mevcut telefonlar:
- lb‑phone v2: https://fivemx.com/lb-phone-v2/
- Quasar Smartphone: https://fivemx.com/phone-scripts/
- GCPhone: https://fivemx.com/free/gcphone/
- Z‑Phone: https://fivemx.com/free/z-phone/
Meta
- Hedef resmon: Boşta ortalama ≤0.02 ms.
- Paket bütçesi: MVP için ≤250 KB gzip'li.
- UI FPS: 60.
