Wróć do bloga
Tutorial8 min czytania

System scoreboard w FiveM: lista graczy, ping i prace

Zbuduj przejrzysty scoreboard FiveM. Liczba graczy, ping, prace, ID i najlepsze skrypty scoreboardu, aby stylowo zastąpić domyślne menu pod TAB.

Agency Scripts

Założyciel i główny deweloper w Agency Scripts

Dlaczego warto stworzyć własny Scoreboard

Domyślna lista graczy w FiveM jest funkcjonalna, ale brakuje jej dopracowania i ilości informacji, jakich wymagają serwery roleplay. Niestandardowy scoreboard pozwala wyświetlać nazwy graczy obok ich tożsamości IC, stanowiska pracy, ID na serwerze i jakości połączenia, a wszystko to w stylistyce dopasowanej do brandingu twojego serwera. Poza estetyką, własny scoreboard daje kontrolę nad tym, jakie informacje są widoczne dla poszczególnych grup graczy. Zwykli gracze mogą widzieć imiona postaci i ID, podczas gdy administratorzy widzą dodatkowe pola, takie jak identyfikatory Steam czy wartości pingu. Stworzenie własnego rozwiązania pozwala również dodać funkcje sortowania, filtrowania według pracy i statystyki liczby graczy w czasie rzeczywistym, co ułatwia zarządzanie serwerem zarówno graczom, jak i administracji.

Struktura zasobu i manifest

Zasób scoreboard jest stosunkowo lekki, składa się ze skryptu klienta do obsługi przypisań klawiszy i zbierania danych, skryptu serwera do agregacji danych graczy oraz strony NUI do renderowania interfejsu wizualnego. Utrzymanie zasobu jako samowystarczalnego, bez zewnętrznych zależności poza twoim frameworkiem, ułatwia jego instalację i utrzymanie. Oto manifest zasobu i struktura folderów:

-- 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'

Ten ui_page dyrektywa wskazuje na plik wejściowy NUI, a files blok zapewnia, że wszystkie zasoby NUI są dołączone do zasobu. Używanie lua54 włącza funkcje Lua 5.4, takie jak dzielenie całkowite i operatory bitowe, co może być przydatne do formatowania i pakowania danych w bardziej zaawansowanych implementacjach scoreboarda.

Zbieranie danych graczy po stronie serwera

Serwer odpowiada za zbudowanie pełnej listy graczy ze wszystkimi informacjami, które musi wyświetlić scoreboard. Zamiast odpytywania każdego gracza z osobna przez klienta, serwer zbiera wszystkie dane graczy w jedną tabelę i wysyła je do pytającego klienta w jednym payloadzie. Takie podejście dobrze się skaluje, ponieważ agregacja danych odbywa się raz na żądanie, a nie raz na gracza. Dołącz do payloadu wartości pingu, informacje o pracy oraz identyfikatory graczy:

-- 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 sprawdza, czy gracz wysyłający żądanie ma uprawnienia admina, zanim dołączy poufne informacje, takie jak nazwy Steam i identyfikatory. Zapobiega to zbieraniu informacji o kontach przez zwykłych graczy za pośrednictwem scoreboarda. Ten GetConvar wywołanie pobiera maksymalną liczbę graczy, dzięki czemu scoreboard może wyświetlać wskaźnik zapełnienia, np. "32/64 Players".

Logika skrótów klawiszowych i przełączania po stronie klienta

Scoreboard powinien otwierać się, gdy gracz trzyma określony klawisz, i zamykać, gdy go puszcza. FiveM zapewnia RegisterKeyMapping funkcja do konfigurowalnych skrótów klawiszowych, co jest preferowanym podejściem zamiast sztywno zakodowanych klawiszy, ponieważ pozwala graczom dostosować sterowanie w menu ustawień gry. Zastosuj wzorzec hold-to-show, w którym NUI otwiera się po naciśnięciu klawisza i zamyka po jego zwolnieniu, zapewniając płynne, nienachalne działanie:

-- 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)

Ten + i - prefiksy w nazwach komend automatycznie tworzą parę naciśnięcia i zwolnienia. Gdy gracz naciśnie przypisany klawisz, +scoreboard polecenie się wykonuje. Gdy go puszczą, -scoreboard zostanie wywołane. Zauważ, że SetNuiFocus jest wywoływane z false, false ponieważ scoreboard służy wyłącznie do wyświetlania i nie wymaga obsługi myszy. Jeśli chcesz klikalny scoreboard z sortowaniem lub filtrowaniem, przekaż true, true zamiast tego i dodaj przycisk zamykania w NUI.

Renderowanie NUI za pomocą HTML i CSS

Warstwa NUI renderuje listę graczy jako ostylowaną tabelę lub układ kart. Zastosuj półprzezroczyste tło, które nakłada się na ekran gry bez całkowitego zasłaniania widoku. CSS Grid lub Flexbox świetnie sprawdzają się przy wyrównywaniu kolumn danych. Oznacz wartości pingu kolorami, aby gracze mogli szybko ocenić jakość połączenia: zielony dla dobrego pingu, żółty dla umiarkowanego i czerwony dla słabego. Oto podstawowa struktura NUI:

<!-- 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>

Ostyluj scoreboard za pomocą ciemnego, półprzezroczystego tła i stałego pozycjonowania, aby pozostał wyśrodkowany na ekranie. Dodaj max-height z przewijaniem overflow dla listy graczy, aby serwery z 200 lub więcej graczami nie wypychały scoreboardu poza ekran. Rozważ dodanie subtelnej animacji pojawiania się za pomocą CSS transitions, aby otwieranie i zamykanie wyglądało profesjonalnie, a nie gwałtownie.

Dodawanie funkcji wyszukiwania i filtrowania

W przypadku serwerów z dużą liczbą graczy wyszukiwanie i filtrowanie scoreboarda staje się kluczowe. Dodaj na górze pole wyszukiwania, które filtruje listę graczy według nazwy, ID serwera lub nazwy pracy podczas wpisywania tekstu. Zaimplementuj filtr całkowicie w JavaScript po stronie NUI, aby uniknąć opóźnień związanych z zapytaniami do serwera. Możesz także dodać klikalne nagłówki kolumn do sortowania według ID, nazwy, pracy lub pingu:

// 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 + ' wyświetlonych'; } 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); }); });

Stan sortowania jest utrzymywany po stronie klienta, więc przełączanie między porządkiem rosnącym a malejącym następuje natychmiastowo. Kropka wskaźnika służby obok nazwy pracy zapewnia szybki sygnał wizualny, czy gracz jest aktualnie na służbie, co jest szczególnie przydatne dla administratorów przeglądających listę w celu sprawdzenia obsady w różnych frakcjach.

Funkcje admina i rozszerzone informacje

Gdy administrator otwiera scoreboard, powinien widzieć dodatkowe kolumny i przyciski akcji, do których zwykli gracze nie mają dostępu. Dodaj menu kontekstowe pod prawym przyciskiem myszy w wierszach graczy, które pozwala administratorom wyrzucać, banować, teleportować się do gracza lub go obserwować bezpośrednio z poziomu scoreboarda. Zmienia to scoreboard ze zwykłego widoku w lekki panel administratora. Zaimplementuj sprawdzanie uprawnień zarówno po stronie klienta, jak i serwera. Klient sprawdza, czy renderować elementy UI administratora, a serwer niezależnie waliduje każdą akcję administratora:

-- 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)

Zawsze loguj działania administratorów do bazy danych lub webhooka Discord w celach rozliczalności. Dołącz identyfikator administratora, gracza docelowego, podjęte działanie i znacznik czasu. Taki rejestr zdarzeń jest kluczowy do rozwiązywania sporów między członkami ekipy oraz do wykrywania przejętych kont administratorów, którzy mogą nadużywać swoich uprawnień.

Wydajność i optymalizacja

Scoreboard powodujący spadki klatek mija się z celem. Najczęstszym błędem wydajnościowym jest odświeżanie danych graczy w każdej klatce lub co kilka milisekund. Interwał automatycznego odświeżania nie powinien być krótszy niż od trzech do pięciu sekund, ponieważ dane gracza, takie jak ping i status pracy, nie zmieniają się na tyle szybko, aby uzasadniać częstsze aktualizacje. Po stronie NUI unikaj niszczenia i ponownego tworzenia całego DOM przy każdym odświeżeniu. Zamiast tego zastosuj podejście oparte na diffingu, które aktualizuje tylko zmienione wiersze, lub przynajmniej użyj innerHTML tylko na kontenerze listy graczy, a nie na całym scoreboardzie. Zachowaj prosty CSS i unikaj ciężkich efektów, takich jak blur lub box-shadow w każdym wierszu, ponieważ przy setkach wierszy wywołują one kosztowne operacje kompozytowania GPU. W przypadku bardzo dużych serwerów z 200 lub więcej graczami wdróż virtual scrolling, który renderuje tylko widoczne wiersze plus mały bufor, utrzymując rozsądną liczbę węzłów DOM bez względu na liczbę połączonych graczy. Przetestuj swój scoreboard na pełnym serwerze, aby zmierzyć rzeczywisty wpływ na wydajność, ponieważ funkcja, która działa dobrze przy 10 graczach, może stać się wąskim gardłem przy 200.

Gotowy, aby zacząć?

Kup skrypty w naszym sklepie lub dołącz do Discorda, aby uzyskać wsparcie, aktualizacje i sprawdzić nadchodzące nowości.