$ ABD Doları
  • $ ABD Doları
  • € AVRO
  • £ GBP
  • $ Avustralya Doları
  • R$ BRL
  • İsviçre Frangı İsviçre Frangı
  • ¥ Japon Yeni
Özel bir FiveM Telefon Uygulaması Oluşturun

Özel bir FiveM Telefon Uygulaması Oluşturun

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

  1. Çalışan bir FiveM sunucusu txAdmin ve MySQL ile (oxmysql veya mysql-async).
  2. Node.js 18+ ve geliştirme PC'nizde pnpm veya npm.
  3. Bir framework yüklü: QBCore veya ESX.
  4. Ö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ı).
  5. Temel React bilgisi.

Dokümanlar

Dahili okuma (FiveMX)


Mimarlık

  1. Kaynak my_phone ile fxmanifest.lua, client, server, Ve ui paket.
  2. Kullanıcı arayüzü: Vite ile oluşturulmuş React uygulaması /ui/dist. NUI, Lua ile şu yolla konuşur: postMessage + RegisterNUICallback.
  3. Veri: MySQL tabloları şunlar için: phone_contacts, phone_messages, phone_calls.
  4. 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ışı

  1. Oyuncu bir tuşa basar veya telefon öğesini kullanır → 2) SetNuiFocus(true, true) Ve SendNUIMessage({ 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:13172 Chromium 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

  1. UI çağrıları contacts:list → sunucu satırları döndürür.
  2. “Kişi ekle” formu ekleyin → çağrı addContact.
  3. “Kişiyi sil” ekleyin → sunucu, ile siler id vatandaş sahipliği kontrolü ile.

Mesajlar (SMS)

  1. Masa phone_messages sahibi, eş, gövdeyi saklar.
  2. UI bir sohbet açar, arar messages:list Ve messages:send.
  3. 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

  1. NUI girişine asla güvenmeyin. Sunucuda türleri ve uzunluğu doğrulayın.
  2. Her sorguda sahipliği şununla kontrol edin: citizenid veya identifier.
  3. Diğer istemcilere tanımlayıcıları ifşa etmekten kaçının. Sunucu rölelerini kullanın.

Kullanıcı Deneyimi

  1. Sunucu kurallarınız gerektiriyorsa, yere düşmüş, kelepçelenmiş veya araba kullanırken telefonu iptal edin.
  2. UI'yi hızlı tutun. İyimser güncellemeler kullanın ve sunucu onayında uzlaştırın.

Performans

  1. NUI'yi boşta tutun. React'te setInterval döngülerinden kaçının. Efektler ve olaylar kullanın.
  2. Paketleri küçük tutun. Ağır ekranları tembel yükleyin. Sıkıştırılmış varlıklar gönderin.
  3. 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

  1. Kaynağı şurada başlatın: server.cfg bağımlı betiklerden önce.
ensure my_phone
  1. Oyunda, F8'e bas → çalıştır nui_devTools → aç http://localhost:13172 ve NUI sayfanı seç.
  2. Ağ sekmesini incele. Her NUI → Lua çağrısı isabet eder https://my_phone/<name> uç noktalarına.
  3. Kullanmak /myphone komut ver ve odak geçişlerini onayla.
  4. 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

  1. Commit ui/ kaynak ve ui/dist/ derle.
  2. CI'da, çalıştır pnpm --filter ui build ve yalnızca gönder dist yayınlarda.
  3. SQL geçişlerinizi sürümleyin. Yedekleme olmadan kullanıcı verilerini asla silmeyin.

Adım 10 — Sonra ekleyebileceğiniz uzantılar

  1. Bankacılık: sunucunuzun bankacılık kaynağına bağlantı; bakiye ve transferleri göster.
  2. Tweetler/Reklamlar: hız sınırlamaları ve moderasyon ile küresel akış.
  3. Pazar yeri: emanet ile listeleme.
  4. İş uygulamaları: polis\/EMS MDT kancaları.
  5. Fotoğraflar: sunucu uç noktası üzerinden ekran görüntüsü entegrasyonu, veri URL'leri değil.
  6. 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_version dır cerulean Ve ui_page işaret ediyor ui/dist/index.html.
  • CORS veya JSON hataları için F8 konsolunu kontrol edin.

Öğeler kullanılamıyor

  • QBCore: onayla QBCore.Functions.CreateUseableItem çalışır ve phone öğ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


Meta

  • Hedef resmon: Boşta ortalama ≤0.02 ms.
  • Paket bütçesi: MVP için ≤250 KB gzip'li.
  • UI FPS: 60.