Система табло FiveM: Списки игроков, пинг и профессии
Создайте чистую табличку результатов для FiveM. Количество игроков, пинг, профессии, ID и лучшие скрипты табличек для замены стандартного меню TAB со стилем.
Agency Scripts
Основатель и ведущий разработчик Agency Scripts
Зачем создавать кастомный табло результатов
Стандартный список игроков FiveM функционален, но ему не хватает полировки и плотности информации, необходимых для ролевых серверов. Кастомная таблица результатов позволяет отображать имена игроков вместе с их персонажами, должностями, ID сервера и качеством соединения, всё стилизовано под бренд вашего сервера. Помимо эстетики, кастомная таблица даёт контроль над тем, какая информация видна разным группам игроков. Обычные игроки могут видеть имена персонажей и ID, а администраторы , дополнительные поля, такие как Steam-идентификаторы и пинг. Создание собственной таблицы также позволяет добавить функции сортировки, фильтрации по должности и статистику количества игроков в реальном времени, что помогает и игрокам, и персоналу эффективно управлять сервером.
Структура ресурса и манифест
Ресурс табло относительно легковесен и состоит из клиентского скрипта для обработки горячих клавиш и сбора данных, серверного скрипта для агрегации данных игроков и страницы NUI для визуального отображения. Поддержание ресурса автономным без внешних зависимостей, кроме вашего фреймворка, облегчает установку и обслуживание. Вот манифест ресурса и структура папок:
-- 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'
Параметр ui_page директива указывает на входной файл NUI, и files блок гарантирует, что все NUI-ресурсы упакованы вместе с ресурсом. Использование lua54 включает возможности Lua 5.4, такие как целочисленное деление и побитовые операторы, которые могут быть полезны для форматирования и упаковки данных в более продвинутых реализациях табло результатов.
Сбор данных игроков на стороне сервера
Сервер отвечает за формирование полного списка игроков со всей информацией, необходимой табло. Вместо того чтобы клиент запрашивал каждого игрока отдельно, сервер собирает все данные в одну таблицу и отправляет клиенту одним пакетом. Такой подход масштабируется лучше, так как агрегация данных происходит один раз на запрос, а не на каждого игрока. В пакет включены значения пинга, информация о работе и идентификаторы игроков:
-- 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 имена и идентификаторы. Это предотвращает сбор обычными игроками информации об аккаунтах через табло результатов. GetConvar вызов получает максимальное количество игроков, чтобы табло могло отображать индикатор заполненности, например "32/64 Players".
Клиентская логика привязки клавиш и переключения
Табло должно открываться при удержании игроком определённой клавиши и закрываться при её отпускании. FiveM предоставляет RegisterKeyMapping функция для переназначаемых клавиш, что предпочтительнее жестко заданных проверок клавиш, так как позволяет игрокам настраивать управление через меню настроек игры. Используйте схему удержания для показа, где NUI открывается при нажатии клавиши и закрывается при отпускании для плавного, ненавязчивого опыта:
-- 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)
Параметр + и - префиксы в названиях команд автоматически создают пару нажатия и отпускания. Когда игрок нажимает привязанную клавишу, +scoreboard команда срабатывает. Когда они отпускают её, -scoreboard срабатывает. Обратите внимание, что SetNuiFocus вызывается с false, false потому что табло результатов только для отображения и не требует ввода с мыши. Если вам нужно кликабельное табло с сортировкой или фильтрацией, передайте true, true вместо этого и добавьте кнопку закрытия в NUI.
Отрисовка NUI с помощью HTML и CSS
Слой NUI отображает список игроков в виде стилизованной таблицы или карточек. Используйте полупрозрачный фон, который накладывается на игровой экран, не закрывая полностью вид. CSS Grid или Flexbox хорошо подходят для выравнивания столбцов данных. Цветовая кодировка пинга помогает игрокам быстро определить качество соединения: зелёный , хороший пинг, жёлтый , средний, красный , плохой. Вот основная структура 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>
Стилизуйте табло с тёмным полупрозрачным фоном и фиксированным позиционированием, чтобы оно оставалось по центру экрана. Добавьте максимальную высоту с прокруткой для списка игроков, чтобы серверы с 200 и более игроками не сдвигали табло за пределы экрана. Рассмотрите возможность добавления плавной анимации появления с помощью CSS-переходов, чтобы открытие и закрытие выглядело аккуратно, а не резким.
Добавление функции поиска и фильтрации
Для серверов с большим количеством игроков поиск и фильтрация таблицы результатов становятся необходимыми. Добавьте поле поиска сверху, которое фильтрует список игроков по имени, ID сервера или названию работы во время ввода. Реализуйте фильтр полностью на JavaScript на стороне NUI, чтобы избежать задержек с сервером. Также можно добавить кликабельные заголовки столбцов для сортировки по ID, имени, работе или пингу:
// 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 + ' показано';
}
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);
});
});
Состояние сортировки поддерживается на клиенте, поэтому переключение между возрастанием и убыванием происходит мгновенно. Индикатор дежурства рядом с названием работы даёт быстрый визуальный сигнал о том, находится ли игрок на дежурстве, что особенно полезно администраторам для проверки уровня персонала в разных отделах.
Функции админа и расширенная информация
Когда админ открывает табло результатов, он должен видеть дополнительные колонки и кнопки действий, недоступные обычным игрокам. Добавьте контекстное меню по правому клику на строках игроков, позволяющее админам кикать, банить, телепортироваться к игроку или наблюдать за ним прямо из табло. Это превращает табло из простого отображения в лёгкую админ-панель. Реализуйте проверку прав как на клиенте, так и на сервере. Клиент решает, показывать ли админский UI, а сервер независимо валидирует каждое действие админа:
-- 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)
Всегда ведите лог действий администраторов в базе данных или через Discord webhook для отчетности. Включайте идентификатор администратора, целевого игрока, выполненное действие и временную метку. Этот аудит необходим для разрешения споров между сотрудниками и обнаружения скомпрометированных админ-аккаунтов, которые могут злоупотреблять полномочиями.
Производительность и оптимизация
Табло, вызывающее падение FPS, теряет смысл. Самая распространенная ошибка производительности , обновление данных игроков каждый кадр или каждые несколько миллисекунд. Интервал автообновления не должен быть короче трех-пяти секунд, так как данные игроков, такие как пинг и статус работы, не меняются достаточно быстро для более частых обновлений. На стороне NUI избегайте уничтожения и пересоздания всего DOM при каждом обновлении. Вместо этого используйте подход дифференцирования, обновляя только изменившиеся строки, или как минимум используйте innerHTML только для контейнера списка игроков, а не для всего табло. Держите CSS простым и избегайте тяжёлых эффектов, таких как размытие или тень на каждой строке, так как они вызывают дорогие операции GPU при сотнях строк. Для очень больших серверов с 200 и более игроками реализуйте виртуальную прокрутку, которая рендерит только видимые строки плюс небольшой буфер, сохраняя количество DOM-узлов управляемым независимо от количества игроков. Тестируйте табло на полном сервере, чтобы измерить реальное влияние на производительность, так как функция, хорошо работающая с 10 игроками, может стать узким местом при 200.