Bloga Geri Dön
Tutorial8 dk. okuma

FiveM Scoreboard Sistemi: Oyuncu Listesi, Ping ve Meslekler

Temiz bir FiveM scoreboard oluşturun. Oyuncu sayısı, ping, meslekler, ID'ler ve varsayılan TAB menüsünü şık bir şekilde değiştirecek en iyi scoreboard scriptleri.

Agency Scripts

Agency Scripts Kurucusu ve Baş Geliştiricisi

Neden Özel Bir Scoreboard Geliştirilmeli?

Varsayılan FiveM oyuncu listesi işlevseldir ancak roleplay sunucularının ihtiyaç duyduğu şıklıktan ve bilgi yoğunluğundan yoksundur. Özel bir scoreboard; oyuncu adlarını karakter kimlikleri, meslek unvanları, sunucu ID'leri ve bağlantı kaliteleriyle birlikte, tamamı sunucunuzun marka kimliğine uyacak şekilde biçimlendirilmiş olarak görüntülemenizi sağlar. Estetiğin ötesinde, özel bir scoreboard farklı oyuncu gruplarına hangi bilgilerin görünür olduğu konusunda size kontrol sunar. Normal oyuncular karakter adlarını ve ID'leri görebilirken, yöneticiler Steam tanımlayıcıları ve ping değerleri gibi ek alanları görebilir. Kendi listenizi oluşturmak ayrıca sıralama, mesleğe göre filtreleme ve hem oyuncuların hem de yetkililerin sunucuyu etkili bir şekilde yönetmesine yardımcı olan gerçek zamanlı oyuncu sayısı istatistikleri gibi özellikler ekleyebileceğiniz anlamına gelir.

Resource Yapısı ve Manifest

Scoreboard kaynağı nispeten hafiftir; tuş ataması yönetimi ve veri toplama için bir istemci scripti, oyuncu verilerini bir araya getirmek için bir sunucu scripti ve görsel ekranı oluşturmak için bir NUI sayfasından oluşur. Kaynağın framework'ünüzün ötesinde hiçbir harici bağımlılık olmadan bağımsız tutulması, kurulumunu ve bakımını kolaylaştırır. İşte kaynak manifestosu ve klasör yapısı:

-- fxmanifest.lua
fx_version 'cerulean'
game 'gta5'

description 'Custom Scoreboard System'
author 'Agency Scripts'
version '1.0.0'

ui_page 'html/index.html'

client_scripts {
    'client/main.lua',
}

server_scripts {
    'server/main.lua',
}

files {
    'html/index.html',
    'html/style.css',
    'html/script.js',
}

lua54 'yes'

The ui_page yönergesi NUI giriş dosyasını işaret eder ve files bloğu, tüm NUI varlıklarının kaynak ile birlikte paketlenmesini sağlar. Kullanımı: lua54 daha gelişmiş scoreboard uygulamalarında biçimlendirme ve veri paketleme için yararlı olabilecek tamsayı bölme ve bit düzeyinde işleçler gibi Lua 5.4 özelliklerini etkinleştirir.

Sunucu Taraflı Oyuncu Verisi Toplama

Sunucu, scoreboard'un göstermesi gereken tüm bilgileri içeren eksiksiz oyuncu listesini oluşturmaktan sorumludur. Client'ın her oyuncuyu tek tek sorgulaması yerine sunucu, tüm oyuncu verilerini tek bir tabloda toplar ve talep eden client'a tek bir payload halinde gönderir. Bu yaklaşım iyi ölçeklenir, çünkü veri toplama işlemi oyuncu başına bir kez yerine istek başına bir kez gerçekleşir. Payload içine ping değerlerini, meslek bilgilerini ve oyuncu tanımlayıcılarını ekleyin:

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

QBCore.Functions.CreateCallback('scoreboard:server:getPlayers', function(source, cb)
    local players = {}
    local srcPlayer = QBCore.Functions.GetPlayer(source)
    local isAdmin = srcPlayer and IsPlayerAceAllowed(source, 'admin')

    for _, playerId in ipairs(GetPlayers()) do
        local Player = QBCore.Functions.GetPlayer(tonumber(playerId))
        if Player then
            local playerData = {
                serverId = playerId,
                name = Player.PlayerData.charinfo.firstname .. ' ' ..
                       Player.PlayerData.charinfo.lastname,
                job = Player.PlayerData.job.label or 'Unemployed',
                jobGrade = Player.PlayerData.job.grade.name or '',
                onDuty = Player.PlayerData.job.onduty,
                ping = GetPlayerPing(playerId),
            }

            -- Only include sensitive data for admins
            if isAdmin then
                playerData.steamName = GetPlayerName(playerId)
                playerData.identifiers = GetPlayerIdentifiers(playerId)
            end

            table.insert(players, playerData)
        end
    end

    -- Sort by server ID
    table.sort(players, function(a, b)
        return tonumber(a.serverId) < tonumber(b.serverId)
    end)

    cb(players, #players, GetConvar('sv_maxclients', '64'))
end)

Callback, Steam isimleri ve tanımlayıcılar gibi hassas bilgileri dahil etmeden önce istekte bulunan oyuncunun admin yetkilerine sahip olup olmadığını kontrol eder. Bu, normal oyuncuların skor tablosu üzerinden hesap bilgilerini toplamasını engeller. Bu GetConvar çağrısı, scoreboard'un "32/64 Oyuncu" gibi bir kapasite göstergesi görüntüleyebilmesi için maksimum oyuncu sayısını alır.

Client Tarafı Tuş Ataması ve Açma/Kapatma Mantığı

Scoreboard, oyuncu belirli bir tuşu basılı tuttuğunda açılmalı ve bıraktığında kapanmalıdır. FiveM bunun için şu özelliği sunar: RegisterKeyMapping yeniden atanabilir tuş atamaları için function: Sabit kodlanmış tuş kontrollerine kıyasla tercih edilen yöntemdir çünkü oyuncuların kontrollerini oyun ayarları menüsünden özelleştirmelerine olanak tanır. Akıcı ve rahatsız etmeyen bir deneyim için NUI'nin tuşa basıldığında açıldığı ve tuş bırakıldığında kapandığı bir basılı tutma deseni kullanın:

-- client/main.lua
local QBCore = exports['qb-core']:GetCoreObject()
local isOpen = false
local refreshInterval = 5000  -- ms between data refreshes while open

RegisterKeyMapping('+scoreboard', 'Open Scoreboard', 'keyboard', 'HOME')

RegisterCommand('+scoreboard', function()
    if isOpen then return end
    isOpen = true
    RefreshAndShow()
end, false)

RegisterCommand('-scoreboard', function()
    if not isOpen then return end
    isOpen = false
    SetNuiFocus(false, false)
    SendNUIMessage({action = 'hide'})
end, false)

function RefreshAndShow()
    QBCore.Functions.TriggerCallback('scoreboard:server:getPlayers',
        function(players, count, maxPlayers)
            SendNUIMessage({
                action = 'show',
                players = players,
                playerCount = count,
                maxPlayers = maxPlayers,
                serverName = 'My Roleplay Server',
            })
        end
    )
end

-- Auto-refresh while scoreboard is open
CreateThread(function()
    while true do
        Wait(refreshInterval)
        if isOpen then
            RefreshAndShow()
        end
    end
end)

The + ve - komut adlarındaki ön ekler otomatik olarak bir basma ve bırakma çifti oluşturur. Oyuncu atanan tuşa bastığında, +scoreboard komutu tetiklenir. Bıraktıklarında ise -scoreboard tetiklenir. Şuna dikkat edin: SetNuiFocus şununla çağrılır: false, false çünkü scoreboard yalnızca görüntüleme amaçlıdır ve fare girdisine ihtiyaç duymaz. Sıralama veya filtreleme içeren tıklanabilir bir scoreboard istiyorsanız şunu iletin: true, true yerine ve NUI içine bir kapatma butonu ekleyin.

HTML ve CSS ile NUI İşleme

NUI katmanı oyuncu listesini stillendirilmiş bir tablo veya kart düzeni olarak işler. Görüşü tamamen engellemeden oyun ekranını kaplayan yarı saydam bir arka plan kullanın. Veri sütunlarını hizalamak için CSS Grid veya Flexbox gayet uygundur. Oyuncuların bağlantı kalitesini hızlıca tespit edebilmeleri için ping değerlerini renklendirin: iyi ping için yeşil, orta için sarı ve zayıf için kırmızı. İşte temel NUI yapısı:

<!-- html/index.html -->
<div id="scoreboard" class="hidden">
    <div class="sb-header">
        <h2 id="server-name"></h2>
        <span id="player-count"></span>
    </div>
    <div class="sb-columns">
        <span>ID</span>
        <span>Name</span>
        <span>Job</span>
        <span>Ping</span>
    </div>
    <div id="player-list"></div>
</div>

<script>
window.addEventListener('message', (event) => {
    const data = event.data;

    if (data.action === 'show') {
        const sb = document.getElementById('scoreboard');
        sb.classList.remove('hidden');
        document.getElementById('server-name').textContent = data.serverName;
        document.getElementById('player-count').textContent =
            data.playerCount + '/' + data.maxPlayers + ' Players';

        const list = document.getElementById('player-list');
        list.innerHTML = data.players.map(p => {
            const pingClass = p.ping < 80 ? 'ping-good' :
                              p.ping < 150 ? 'ping-warn' : 'ping-bad';
            return `<div class="sb-row">
                <span class="sb-id">${p.serverId}</span>
                <span class="sb-name">${p.name}</span>
                <span class="sb-job">${p.job}</span>
                <span class="sb-ping ${pingClass}">${p.ping}ms</span>
            </div>`;
        }).join('');
    }

    if (data.action === 'hide') {
        document.getElementById('scoreboard').classList.add('hidden');
    }
});
</script>

Skor tablosunu, ekranda ortalanmış kalması için koyu, yarı saydam bir arka plan ve sabit konumlandırma ile stillendirin. 200 veya daha fazla oyuncuya sahip sunucuların skor tablosunu ekran dışına itmemesi için oyuncu listesine kaydırma özellikli bir maksimum yükseklik ekleyin. Açılış ve kapanışın rahatsız edici olmak yerine akıcı hissettirmesi için CSS geçişleri kullanarak zarif bir belirme animasyonu eklemeyi değerlendirin.

Arama ve Filtreleme İşlevi Ekleme

Büyük oyuncu sayılarına sahip sunucularda skor tablosunu aramak ve filtrelemek zorunlu hale gelir. Üst kısma, oyuncu yazdıkça oyuncu listesini ada, sunucu kimliğine veya meslek unvanına göre filtreleyen bir arama alanı ekleyin. Sunucu gidiş-dönüş gecikmesi olmaması için filtreyi tamamen NUI tarafında JavaScript ile uygulayın. Ayrıca kimliğe, ada, mesleğe veya ping değerine göre sıralama yapmak için tıklanabilir sütun başlıkları ekleyebilirsiniz:

// html/script.js - Search and sort logic
let currentPlayers = [];
let sortField = 'serverId';
let sortAsc = true;

function renderPlayers(filter = '') {
    let filtered = currentPlayers;

    if (filter) {
        const term = filter.toLowerCase();
        filtered = currentPlayers.filter(p =>
            p.name.toLowerCase().includes(term) ||
            p.serverId.toString().includes(term) ||
            p.job.toLowerCase().includes(term)
        );
    }

    filtered.sort((a, b) => {
        let valA = a[sortField];
        let valB = b[sortField];
        if (typeof valA === 'string') {
            valA = valA.toLowerCase();
            valB = valB.toLowerCase();
        }
        if (valA < valB) return sortAsc ? -1 : 1;
        if (valA > valB) return sortAsc ? 1 : -1;
        return 0;
    });

    const list = document.getElementById('player-list');
    list.innerHTML = filtered.map(p => {
        const pingClass = p.ping < 80 ? 'ping-good' :
                          p.ping < 150 ? 'ping-warn' : 'ping-bad';
        const dutyDot = p.onDuty
            ? ''
            : '';
        return `
${p.serverId} ${p.name} ${dutyDot}${p.job} ${p.ping}ms
`; }).join(''); document.getElementById('filtered-count').textContent = filtered.length + ' gösteriliyor'; } document.querySelectorAll('.sb-sort').forEach(btn => { btn.addEventListener('click', () => { const field = btn.dataset.field; if (sortField === field) { sortAsc = !sortAsc; } else { sortField = field; sortAsc = true; } renderPlayers(document.getElementById('sb-search').value); }); });

Sıralama durumu istemci tarafında tutulur, böylece artan ve azalan düzen arasında geçiş anında gerçekleşir. Meslek adının yanındaki görev göstergesi noktası, bir oyuncunun şu anda görevde olup olmadığına dair hızlı bir görsel sinyal sağlar; bu da listeyi tarayarak farklı departmanlardaki personel seviyelerini kontrol eden yöneticiler için son derece kullanışlıdır.

Admin Özellikleri ve Genişletilmiş Bilgiler

Bir yetkili skor tablosunu açtığında, normal oyuncuların erişemediği ek sütunları ve işlem düğmelerini görmelidir. Oyuncu satırlarına yetkililerin bir oyuncuyu doğrudan skor tablosundan atmasına, banlamasına, yanına ışınlanmasına veya onu izlemesine olanak tanıyan bir sağ tık bağlam menüsü ekleyin. Bu, skor tablosunu basit bir göstergeden hafif bir yetkili paneline dönüştürür. Yetki kontrolünü hem istemci hem de sunucu tarafında uygulayın. İstemci yetkili arayüz öğelerinin oluşturulup oluşturulmayacağını denetler, sunucu ise her yetkili eylemini bağımsız olarak doğrular:

-- Admin action handler (server)
RegisterNetEvent('scoreboard:server:adminAction', function(targetId, action)
    local src = source
    if not IsPlayerAceAllowed(src, 'admin') then
        DropPlayer(src, 'Unauthorized admin action')
        return
    end

    local target = tonumber(targetId)
    if not target or not GetPlayerName(target) then return end

    if action == 'kick' then
        DropPlayer(target, 'Kicked by admin')
    elseif action == 'teleport' then
        local targetPed = GetPlayerPed(target)
        local coords = GetEntityCoords(targetPed)
        TriggerClientEvent('scoreboard:client:teleport', src, coords)
    elseif action == 'spectate' then
        TriggerClientEvent('scoreboard:client:spectate', src, target)
    elseif action == 'freeze' then
        local targetPed = GetPlayerPed(target)
        FreezeEntityPosition(targetPed, true)
        SetTimeout(30000, function()
            if DoesEntityExist(targetPed) then
                FreezeEntityPosition(targetPed, false)
            end
        end)
    end
end)

Hesap verebilirlik için yönetici eylemlerini her zaman bir veritabanına veya Discord webhook'una kaydedin. Yöneticinin tanımlayıcısını, hedef oyuncuyu, gerçekleştirilen eylemi ve bir zaman damgasını ekleyin. Bu denetim kaydı, yetkili ekip üyeleri arasındaki anlaşmazlıkları çözmek ve yetkilerini kötüye kullanabilecek güvenliği ihlal edilmiş yönetici hesaplarını tespit etmek için gereklidir.

Performans ve Optimizasyon

Kare düşüşlerine neden olan bir scoreboard amacına aykırıdır. En yaygın performans hatası, oyuncu verilerini her karede veya birkaç milisaniyede bir yenilemektir. Otomatik yenileme aralığı üç ila beş saniyeden daha kısa olmamalıdır; çünkü ping ve meslek durumu gibi oyuncu verileri daha hızlı güncellemeleri haklı çıkaracak kadar hızlı değişmez. NUI tarafında ise her yenilemede tüm DOM yapısını silip yeniden oluşturmaktan kaçının. Bunun yerine, yalnızca değişen satırları güncelleyen bir diffing yaklaşımı kullanın ya da en azından şunu kullanın: innerHTML tüm scoreboard yerine yalnızca oyuncu listesi kapsayıcısında. CSS'i sade tutun ve her satırda blur veya box-shadow gibi ağır efektlerden kaçının: çünkü bunlar yüzlerce satıra uygulandığında GPU için maliyetli birleştirme işlemlerini tetikler. 200 veya daha fazla oyuncuya sahip çok büyük sunucular için, bağlı oyuncu sayısından bağımsız olarak DOM düğüm sayısını yönetilebilir tutmak adına yalnızca görünür satırları ve küçük bir arabelleği işleyen sanal kaydırma (virtual scrolling) uygulayın. Gerçek performans etkisini ölçmek için scoreboard'unuzu dolu bir sunucuda test edin, çünkü 10 oyuncuyla sorunsuz çalışan bir özellik 200 oyuncuda darboğaza dönüşebilir.

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.