FiveM Galeri Sistemi: Araç Galerilerini Doğru Şekilde Kodlama
Gerçekçi bir FiveM galeri sistemi kurun. Test sürüşleri, showroom kurulumu, NPC ve oyuncu işletmeli seçenekler ve QBCore ile ESX için en iyi galeri scriptleri.
Agency Scripts
Agency Scripts Kurucusu ve Baş Geliştiricisi
Araç Galerisi Mimarisi
Bir galeri (car dealer) sistemi, oyuncuların araç edindiği bir kapı işlevi görerek herhangi bir FiveM roleplay sunucusundaki birincil ekonomik lokomotiflerden biridir. İyi tasarlanmış bir galeri; showroom'da araç önizlemeleri, test sürüşleri, finansman seçenekleri, takaslar ve galeri çalışanları için satış takibi gibi özellikler sunarak basit bir satın alma menüsünün çok ötesine geçer. Mimari; mevcut araçları fiyatlandırma ve kategorilerle tanımlayan bir katalog sistemine, oyuncuların satın almadan önce araçları incelemesine olanak tanıyan bir showroom ekranına, satın alma ve finansmanı yöneten bir işlem motoruna ve oyuncular tarafından işletilen galeriler için bir çalışan yönetim katmanına ayrılır. Fiyat manipülasyonunu ve yetkisiz araç spawn edilmesini önlemek için her katman server tarafından doğrulanan event'ler aracılığıyla iletişim kurar.
Araç Kataloğu ve Kategori Sistemi
Araç kataloğu, kolay gezinme için kategoriler halinde düzenlenmiş, satın alınabilecek her aracı tanımlar. Her girdi spawn adını, görünen etiketi, fiyatı, kategoriyi ve galeri gösterimi için maksimum hız ve koltuk sayısı gibi isteğe bağlı meta verileri içerir. Kataloğu hem istemcinin hem de sunucunun başvurduğu paylaşımlı bir yapılandırma dosyasında saklayın; böylece istemci oluşturma için aynı verileri kullanırken fiyat doğrulaması sunucu tarafında gerçekleşir. Yapılandırılmış bir katalog tanımı şöyledir:
Config.VehicleCatalog = {
categories = {
{id = 'sedan', label = 'Sedans', icon = 'fa-car'},
{id = 'sport', label = 'Sports Cars', icon = 'fa-flag-checkered'},
{id = 'suv', label = 'SUVs & Trucks', icon = 'fa-truck'},
{id = 'muscle', label = 'Muscle Cars', icon = 'fa-bolt'},
{id = 'motorcycle', label = 'Motorcycles', icon = 'fa-motorcycle'},
},
vehicles = {
-- Sedans
{model = 'sultan', label = 'Karin Sultan', price = 24500,
category = 'sedan', seats = 4, testDrive = true},
{model = 'schafter2', label = 'Benefactor Schafter', price = 38000,
category = 'sedan', seats = 4, testDrive = true},
-- Sports
{model = 'elegy2', label = 'Annis Elegy RH8', price = 95000,
category = 'sport', seats = 2, testDrive = true},
{model = 'comet2', label = 'Pfister Comet', price = 110000,
category = 'sport', seats = 2, testDrive = true},
-- SUVs
{model = 'baller', label = 'Gallivanter Baller', price = 55000,
category = 'suv', seats = 4, testDrive = true},
-- Muscle
{model = 'dominator', label = 'Vapid Dominator', price = 42000,
category = 'muscle', seats = 2, testDrive = true},
-- Motorcycles
{model = 'bati', label = 'Pegassi Bati 801', price = 18000,
category = 'motorcycle', seats = 2, testDrive = true},
},
}
-- Build lookup table for fast server-side price validation
Config.VehiclePrices = {}
for _, v in ipairs(Config.VehicleCatalog.vehicles) do
Config.VehiclePrices[v.model] = v.price
end
Fiyat arama tablosu Config.VehiclePrices istemcilerin manipüle edilmiş fiyatlar göndermesini önleyerek sunucuda O(1) fiyat doğrulaması sağlar. Bir satın alma işlemini gerçekleştirmeden önce model adını her zaman bu tabloya göre doğrulayın. Sunucu yöneticilerinin sunucuyu yeniden başlatmadan bir admin panel üzerinden fiyatları ayarlamasını istiyorsanız, araç fiyatlarını bir config dosyası yerine bir veritabanı tablosundan yüklemeyi düşünün.
Showroom Önizleme Sistemi
Showroom, oyuncuların satın almadan önce araçları kontrollü bir ortamda incelemelerine olanak tanır. Belirlenen bir showroom pozisyonunda bir önizleme aracı spawn edin, oyuncunun araç etrafında döndürebileceği bir kamera uygulayın ve modelin yanında istatistikleri görüntüleyin. Önizleme aracı etkileşime kapalı olmalı ve oyuncu menüyü kapattığında veya farklı bir araç seçtiğinde despawn edilmelidir. İşte client tarafı önizleme mantığı:
-- client/showroom.lua
local previewVehicle = nil
local previewCam = nil
local camAngle = 0.0
function ShowVehiclePreview(modelName)
-- Clean up previous preview
DestroyPreview()
local model = GetHashKey(modelName)
RequestModel(model)
while not HasModelLoaded(model) do Wait(10) end
local showroomPos = Config.ShowroomPosition -- vector4
previewVehicle = CreateVehicle(model, showroomPos.x, showroomPos.y,
showroomPos.z, showroomPos.w, false, false)
SetEntityInvincible(previewVehicle, true)
SetVehicleDoorsLocked(previewVehicle, 2)
FreezeEntityPosition(previewVehicle, true)
SetVehicleOnGroundProperly(previewVehicle)
SetModelAsNoLongerNeeded(model)
-- Create orbiting camera
previewCam = CreateCam('DEFAULT_SCRIPTED_CAMERA', true)
UpdateCameraPosition()
SetCamActive(previewCam, true)
RenderScriptCams(true, true, 500, true, true)
end
function UpdateCameraPosition()
if not previewCam or not previewVehicle then return end
local vehPos = GetEntityCoords(previewVehicle)
local radius = 6.0
local height = 2.0
local rad = math.rad(camAngle)
local camX = vehPos.x + radius * math.cos(rad)
local camY = vehPos.y + radius * math.sin(rad)
SetCamCoord(previewCam, camX, camY, vehPos.z + height)
PointCamAtEntity(previewCam, previewVehicle, 0.0, 0.0, 0.0, true)
end
function DestroyPreview()
if previewVehicle then
DeleteEntity(previewVehicle)
previewVehicle = nil
end
if previewCam then
SetCamActive(previewCam, false)
RenderScriptCams(false, true, 500, true, true)
DestroyCam(previewCam, false)
previewCam = nil
end
end
Showroom açıkken fare hareketi veya klavye kontrolleriyle kamera dönüşüne izin verin. Yörünge kamera yaklaşımı, oyuncuların aracın etrafında dolaşmasına gerek kalmadan aracı her açıdan incelemesini sağlar. Oyuncuların satın almadan önce istedikleri boya rengini görebilmeleri için önizlemeye araç rengi özelleştirmesi ekleyin; bu sayede satın alma sonrası pişmanlık ve destek talepleri azalır.
Satın Alma ve İşlem Gerçekleştirme
Kötüye kullanımı önlemek için araç satın alma işlemleri tamamen sunucu tarafında işlenmelidir. Sunucu, oyuncunun aracı almaya gücünün yettiğini doğrular, ödemeyi düşer, benzersiz bir plaka oluşturur, veritabanında araç kaydını açar ve satın alınan aracı spawn etmesi için istemciye bildirim gönderir. Hem tam nakit alımları hem de banka havalelerini destekleyin, isteğe bağlı olarak havale işlemleri için bankacılık sisteminizle entegre edin:
RegisterNetEvent('dealer:server:purchaseVehicle', function(modelName, paymentType)
local src = source
local Player = QBCore.Functions.GetPlayer(src)
if not Player then return end
-- Validate model exists and get price
local price = Config.VehiclePrices[modelName]
if not price then
TriggerClientEvent('QBCore:Notify', src, 'Vehicle not available', 'error')
return
end
-- Check payment
local moneyType = paymentType == 'bank' and 'bank' or 'cash'
if Player.PlayerData.money[moneyType] < price then
TriggerClientEvent('QBCore:Notify', src, 'Insufficient funds', 'error')
return
end
-- Generate unique plate
local plate = GenerateUniquePlate()
-- Process payment
Player.Functions.RemoveMoney(moneyType, price, 'vehicle-purchase-' .. modelName)
-- Create vehicle record
local vehicleHash = GetHashKey(modelName)
MySQL.insert([[
INSERT INTO player_vehicles
(citizenid, vehicle, hash, plate, garage, state, fuel, engine, body)
VALUES (?, ?, ?, ?, ?, 1, 100, 1000.0, 1000.0)
]], {
Player.PlayerData.citizenid,
modelName,
tostring(vehicleHash),
plate,
'pillboxgarage',
})
-- Log transaction
MySQL.insert([[
INSERT INTO vehicle_sales (citizenid, vehicle, plate, price, sold_at)
VALUES (?, ?, ?, ?, NOW())
]], {Player.PlayerData.citizenid, modelName, plate, price})
TriggerClientEvent('QBCore:Notify', src,
'Vehicle purchased! Plate: ' .. plate, 'success')
TriggerClientEvent('dealer:client:vehiclePurchased', src, modelName, plate)
end)
function GenerateUniquePlate()
local plate
repeat
plate = ''
local chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
for i = 1, 8 do
local idx = math.random(1, #chars)
plate = plate .. chars:sub(idx, idx)
end
local exists = MySQL.scalar.await(
'SELECT 1 FROM player_vehicles WHERE plate = ?', {plate}
)
until not exists
return plate
end
The GenerateUniquePlate function, tüm veritabanında plakanın benzersiz olmasını garanti etmek için bir yeniden deneme döngüsü kullanır. 8 alfanümerik karakterle çakışmalar istatistiksel olarak nadir olsa da bu kontrol mutlak güvenlik sağlar. Hangi araçların en popüler olduğunu ve galeri üzerinden dönen toplam nakit akışını izleyerek, yönetim incelemesi ve sunucu ekonomisi analizleri için her satışı ayrı bir tabloda kaydedin.
Test Sürüşü Sistemi
Test sürüşleri, oyuncuların bir aracı satın almadan önce deneyimlemelerine olanak tanır; bu da özellikle pahalı araçlar için son derece önemlidir. Süre dolduğunda veya izin verilen alandan çıktıklarında oyuncuyu otomatik olarak galeriye geri döndüren, zamanlayıcıya ve coğrafi sınırlara sahip geçici bir araç spawn edin. Test sürüşü aracını garajda saklanamayacak veya modifiye edilemeyecek şekilde işaretleyin, böylece oyuncuların sistemi suiistimal ederek ücretsiz araç elde etmesini engelleyin:
-- Client: test drive logic
local testDriveVehicle = nil
local testDriveTimer = 0
local testDriveActive = false
function StartTestDrive(modelName, duration)
local model = GetHashKey(modelName)
RequestModel(model)
while not HasModelLoaded(model) do Wait(10) end
local spawnPos = Config.TestDriveSpawn
testDriveVehicle = CreateVehicle(model, spawnPos.x, spawnPos.y,
spawnPos.z, spawnPos.w, true, false)
SetVehicleNumberPlateText(testDriveVehicle, 'TESTDRVE')
TaskWarpPedIntoVehicle(PlayerPedId(), testDriveVehicle, -1)
SetModelAsNoLongerNeeded(model)
testDriveTimer = duration
testDriveActive = true
-- Timer and boundary check thread
CreateThread(function()
while testDriveActive and testDriveTimer > 0 do
Wait(1000)
testDriveTimer = testDriveTimer - 1
-- Show remaining time
SendNUIMessage({
action = 'updateTestDrive',
timeLeft = testDriveTimer
})
-- Check boundary
local playerPos = GetEntityCoords(PlayerPedId())
local dealerPos = Config.DealerLocation
if #(playerPos - dealerPos) > Config.TestDriveRadius then
QBCore.Functions.Notify('Too far from dealer, returning...', 'error')
EndTestDrive()
return
end
end
if testDriveActive then
EndTestDrive()
end
end)
end
function EndTestDrive()
testDriveActive = false
if testDriveVehicle and DoesEntityExist(testDriveVehicle) then
DeleteEntity(testDriveVehicle)
testDriveVehicle = nil
end
local returnPos = Config.DealerLocation
SetEntityCoords(PlayerPedId(), returnPos.x, returnPos.y, returnPos.z)
QBCore.Functions.Notify('Test drive ended', 'info')
end
Coğrafi sınır, oyuncuların test aracını haritanın diğer ucuna sürüp terk etmesini önler. Yarıçapı, aracın geri alınabilir kalmasını sağlarken yakındaki sokaklarda anlamlı bir sürüşe izin veren makul bir mesafeye ayarlayın. "TESTDRVE" plakası, diğer oyunculara aracın geçici olduğunu gösteren görsel bir gösterge işlevi görür.
Finansman ve Ödeme Planları
Pahalı araçlar için oyuncuların peşinat ödediği ve ardından zamanla taksit ödediği bir finansman seçeneği sunun. Krediyi kalan bakiye, ödeme planı ve faiz oranıyla birlikte bir veritabanı tablosunda takip edin. Bir oyuncu ödemeleri kaçırırsa, araç haciz için işaretlenebilir. Bu özellik ekonomik derinlik katar ve lüks araçları peşin satın alma için yeterli nakit biriktirememiş oyuncular için erişilebilir hale getirir. Taksit kontrolünü, oyun içi saatle günlük olarak çalışan, ödemeleri oyuncunun banka hesabından düşen ve her tahsilatı bildiren sunucu taraflı yinelenen bir iş olarak uygulayın. Banka bakiyesi yetersizse, kaçırılan ödeme sayacını artırın ve bir uyarı gönderin. Yapılandırılabilir sayıda kaçırılan ödemeden sonra, aracı oyuncunun garajından kaldırılacağı ve bayi envanterine iade edileceği haciz işlemi için işaretleyin.
Çalışan Satışları ve Komisyon Takibi
Oyuncular tarafından işletilen galeriler: satış personelini yönetmek, performansı takip etmek ve komisyonları dağıtmak için araçlara ihtiyaç duyar. Bir galeri çalışanı bir satış gerçekleştirdiğinde, araç fiyatının yapılandırılabilir bir yüzdesini komisyon olarak kazanır. Bir veritabanı tablosunda her çalışanın satış sayısını, elde edilen toplam geliri ve kazanılan komisyonu takip edin. Galeri NUI'si üzerinden erişilebilen, kişisel istatistikleri, son satış geçmişini ve satış ekibindeki performansı karşılaştıran bir liderlik tablosunu gösteren bir çalışan paneli oluşturun. Galeri sahibi veya yönetici rütbesi: komisyon oranlarını ayarlamak, çalışan eklemek veya çıkarmak ve toplu satış raporlarını görüntülemek için bir admin paneline erişebilmelidir. Bu, galeriyi statik bir NPC etkileşiminden çıkarıp gerçek yönetim sorumlulukları ve rekabetçi teşvikler içeren dinamik bir oyuncu işletmesine dönüştürür.
Optimizasyon ve Anti-Exploit Önlemleri
Galeri script'leri, proaktif önlem gerektiren yaygın exploit vektörleriyle karşı karşıyadır. En kritik olanı, değiştirilmiş bir client'ın daha düşük bir fiyatla satın alma talebi gönderdiği fiyat manipülasyonudur. Fiyatları her zaman sunucu tarafında kataloğa göre doğrulayın ve client tarafından bildirilen değerlere asla güvenmeyin. Araçların kopyalanmasına veya veri tabanında race condition oluşmasına yol açabilecek seri satın alımları önlemek için satın alma event'lerine rate-limit uygulayın. Showroom önizlemesi için önizleme aracının şununla oluşturulduğundan emin olun: false aracın yalnızca yerel olarak var olması ve diğer oyuncular tarafından binilememesi veya çalınamaması amacıyla network parametresi için. Önizleme araçlarını şurada temizleyin: onResourceStop resource yeniden başlatıldığında sahipsiz entity'leri önlemek için handler. Birden fazla galeriye sahip sunucularda araç kataloğunu bellekte önbelleğe alın ve yalnızca bir yönetici yenileme komutunu tetiklediğinde yeniden yükleyin, böylece her NUI açılışında yapılandırma dosyasının tekrar tekrar okunmasını önleyin. Aynı oyuncunun art arda onlarca araç satın alması gibi, soruşturma gerektiren bir açık veya para kopyalama durumuna işaret edebilecek anomaliler için satın alma loglarını izleyin.