Tutorial 2026-04-28

Criar um Scoreboard Personalizado para FiveM

TDYSKY

TDYSKY

Fundador & Lead Developer na Agency Scripts

Por que construir um placar personalizado

A lista de jogadores FiveM padrão é funcional, mas carece do polimento e da densidade de informações que os servidores de roleplay precisam. Um placar personalizado permite exibir os nomes dos jogadores junto com a identidade do personagem, cargo, ID do servidor e qualidade da conexão, todos estilizados para combinar com a marca do seu servidor. Além da estética, um placar personalizado oferece controle sobre quais informações ficam visíveis para diferentes grupos de jogadores. Jogadores regulares podem ver nomes e IDs de personagens, enquanto administradores veem campos adicionais, como identificadores Steam e valores de ping. Construir o seu próprio também significa que você pode adicionar recursos como classificação, filtragem por trabalho e estatísticas de contagem de jogadores em tempo real que ajudam os jogadores e a equipe a gerenciar o servidor de maneira eficaz.

Estrutura e manifesto de recursos

Um recurso de placar é relativamente leve, consistindo em um script de cliente para manipulação de atalhos de teclado e coleta de dados, um script de servidor para agregar dados do jogador e uma página NUI para renderizar a exibição visual. Manter o recurso independente, sem dependências externas além da sua estrutura, facilita a instalação e a manutenção. Aqui está o manifesto do recurso e a estrutura de pastas:

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

description 'Custom Scoreboard System'
author 'TDYSKY'
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'

A diretiva ui_page aponta para o arquivo de entrada NUI, e o bloco files garante que todos os ativos NUI sejam agrupados com o recurso. Usar lua54 habilita recursos do Lua 5.4 como divisão inteira e operadores bit a bit, que podem ser úteis para formatação e empacotamento de dados em implementações de placar mais avançadas.

Coleta de dados do jogador no servidor

O servidor é responsável por construir a lista completa de jogadores com todas as informações que o placar precisa exibir. Em vez de fazer com que o cliente consulte cada jogador individualmente, o servidor coleta todos os dados do jogador em uma única tabela e os envia ao cliente solicitante em uma única carga. Essa abordagem é bem dimensionada porque a agregação de dados ocorre uma vez por solicitação, e não uma vez por jogador. Inclua valores de ping, informações de trabalho e identificadores de jogadores na carga útil:

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

O retorno de chamada verifica se o jogador solicitante tem permissões de administrador antes de incluir informações confidenciais, como nomes e identificadores do Steam. Isso evita que jogadores regulares coletem informações da conta através do placar. A chamada GetConvar recupera a contagem máxima de jogadores para que o placar possa exibir um indicador de capacidade como "32/64 jogadores".

Lógica de alternância e atalho de teclado do lado do cliente

O placar deve abrir quando o jogador segura uma tecla específica e fechar quando ele a solta. FiveM fornece a função RegisterKeyMapping para atalhos de teclado religáveis, que é a abordagem preferida em vez de verificações de teclas codificadas porque permite que os jogadores personalizem seus controles por meio do menu de configurações do jogo. Use um padrão hold-to-show onde o NUI abre ao pressionar a tecla e fecha ao liberar a tecla para uma experiência suave e não intrusiva:

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

Os prefixos + e - nos nomes dos comandos criam um par pressionar e soltar automaticamente. Quando o jogador pressiona a tecla vinculada, o comando +scoreboard é acionado. Quando eles o liberam, -scoreboard dispara. Observe que SetNuiFocus é chamado com false, false porque o placar é somente para exibição e não precisa de entrada do mouse. Se você deseja um placar clicável com classificação ou filtragem, passe true, true e adicione um botão Fechar no NUI.

Renderização NUI com HTML e CSS

A camada NUI renderiza a lista de jogadores como uma mesa estilizada ou layout de cartão. Use um pano de fundo semitransparente que se sobreponha à tela do jogo sem bloquear completamente a visão. CSS Grid ou Flexbox funcionam bem para alinhar as colunas de dados. Codifique os valores de ping por cores para que os jogadores possam identificar rapidamente a qualidade da conexão: verde para ping bom, amarelo para moderado e vermelho para ruim. Aqui está a estrutura principal do 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>

Estilize o placar com um fundo escuro e semitransparente e posicionamento fixo para que fique centralizado na tela. Adicione uma altura máxima com rolagem excedente para a lista de jogadores para que servidores com 200 ou mais jogadores não empurrem o placar para fora da tela. Considere adicionar uma animação de fade-in sutil usando transições CSS para fazer com que a abertura e o fechamento pareçam polidos, em vez de chocantes.

Adicionando funcionalidade de pesquisa e filtro

Para servidores com grande número de jogadores, pesquisar e filtrar o placar torna-se essencial. Adicione uma entrada de pesquisa na parte superior que filtre a lista de jogadores por nome, ID do servidor ou cargo conforme o jogador digita. Implemente o filtro inteiramente em JavaScript no lado NUI para que não haja atraso de ida e volta do servidor. Você também pode adicionar cabeçalhos de coluna clicáveis ​​para classificação por ID, nome, trabalho ou ping:

// 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 + ' shown'; } 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); }); });

O estado de classificação é mantido no lado do cliente, portanto a alternância entre ordem crescente e decrescente é instantânea. O ponto indicador de serviço próximo ao nome do trabalho fornece um sinal visual rápido para saber se um jogador está de serviço no momento, o que é especialmente útil para administradores que examinam a lista para verificar os níveis de pessoal em diferentes departamentos.

Recursos administrativos e informações estendidas

Quando um administrador abre o placar, ele deve ver colunas adicionais e botões de ação aos quais os jogadores normais não têm acesso. Adicione um menu de contexto do botão direito nas linhas de jogadores que permite aos administradores chutar, banir, teletransportar-se ou assistir a um jogador diretamente do placar. Isso transforma o placar de um simples display em um painel de administração leve. Implemente a verificação de permissão no lado do cliente e do servidor. O cliente verifica se deve renderizar os elementos da UI administrativa e o servidor valida cada ação administrativa de forma independente:

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

Sempre registre as ações do administrador em um banco de dados ou webhook do Discord para fins de responsabilização. Inclua o identificador do administrador, o jogador alvo, a ação realizada e um carimbo de data/hora. Esta trilha de auditoria é essencial para resolver disputas entre funcionários e para detectar contas de administrador comprometidas que possam estar abusando de seus poderes.

Desempenho e Otimização

Um placar que causa quedas de quadros anula seu propósito. O erro de desempenho mais comum é atualizar os dados do jogador a cada quadro ou a cada poucos milissegundos. O intervalo de atualização automática não deve ser inferior a três a cinco segundos porque os dados do jogador, como ping e status do trabalho, não mudam com rapidez suficiente para justificar atualizações mais rápidas. Do lado da NUI, evite destruir e recriar todo o DOM a cada atualização. Em vez disso, use uma abordagem de comparação que atualize apenas as linhas que foram alteradas ou, no mínimo, use innerHTML apenas no contêiner da lista de jogadores, em vez de no placar inteiro. Mantenha o CSS simples e evite efeitos pesados, como desfoque ou sombra de caixa em cada linha, pois eles acionam operações caras de composição de GPU quando multiplicados por centenas de linhas. Para servidores muito grandes com 200 ou mais jogadores, implemente a rolagem virtual que renderiza apenas as linhas visíveis mais um pequeno buffer, mantendo a contagem de nós DOM gerenciável, independentemente de quantos jogadores estão conectados. Teste seu placar em um servidor completo para medir o impacto real no desempenho, porque um recurso que parece bom para 10 jogadores pode se tornar um gargalo para 200.

Partilhar este artigo

Pronto para melhorar o teu servidor?

Explora os nossos scripts FiveM premium na loja Agency Scripts ou junta-te à nossa comunidade no Discord para suporte e atualizações.