Bloga Geri Dön
Guide8 dk. okuma

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.

Eşleşen script

Sunucunuz için hazır, güncellemeler dahil.

Başlamaya Hazır mısınız?

Scriptleri mağazamızdan edinin veya destek, güncellemeler ve gelecek yeniliklere göz atmak için Discord'a katılın.