Pourquoi créer un tableau de bord personnalisé
La liste des joueurs FiveM par défaut est fonctionnelle mais manque de finition et de densité d'informations dont les serveurs de jeu de rôle ont besoin. Un tableau de bord personnalisé tu permet d'afficher les noms des joueurs ainsi que leur identité, leur titre de poste, leur identifiant de serveur et la qualité de leur connexion, le tout dans un style correspondant à l'image de marque de ton serveur. Au-delà de l'esthétique, un tableau de bord personnalisé tu permet de contrôler quelles informations sont visibles par les différents groupes de joueurs. Les joueurs réguliers peuvent voir les noms et identifiants des personnages, tandis que les administrateurs voient des champs supplémentaires tels que les identifiants Steam et les valeurs de ping. Créer le vôtre signifie également que tu peux ajouter des fonctionnalités telles que le tri, le filtrage par travail et les statistiques de nombre de joueurs en temps réel qui aident les joueurs et le personnel à gérer efficacement le serveur.
Structure des ressources et manifeste
Une ressource de tableau de bord est relativement légère, composée d'un script client pour la gestion des raccourcis clavier et la collecte de données, d'un script serveur pour l'agrégation des données des joueurs et d'une page NUI pour le rendu de l'affichage visuel. Garder la ressource autonome sans dépendances externes au-delà de ton cadre facilite son installation et sa maintenance. Voici le manifeste des ressources et la structure des dossiers :
-- 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'
Le ui_page La directive pointe vers le fichier d'entrée NUI et le files Le bloc garantit que tous les actifs NUI sont regroupés avec la ressource. En utilisant lua54 active les fonctionnalités Lua 5.4 telles que la division entière et les opérateurs au niveau du bit, qui peuvent être utiles pour le formatage et le conditionnement des données dans des implémentations de tableau de bord plus avancées.
Collecte de données du lecteur côté serveur
Le serveur est responsable de la création de la liste complète des joueurs avec toutes les informations que le tableau de bord doit afficher. Plutôt que de laisser le client interroger chaque joueur individuellement, le serveur collecte toutes les données des joueurs dans une seule table et les envoie au client demandeur en une seule charge utile. Cette approche s'adapte bien car l'agrégation des données se produit une fois par requête plutôt qu'une fois par joueur. Incluez les valeurs de ping, les informations sur la tâche et les identifiants des joueurs dans la charge utile :
-- 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)
Le rappel vérifie si le joueur demandeur dispose des autorisations d'administrateur avant d'inclure des informations sensibles telles que les noms et identifiants Steam. Cela empêche les joueurs réguliers de récolter des informations sur leur compte via le tableau de bord. Le GetConvar call récupère le nombre maximum de joueurs afin que le tableau d'affichage puisse afficher un indicateur de capacité tel que "32/64 joueurs".
Combinaison de touches côté client et logique de bascule
Le tableau de bord doit s'ouvrir lorsque le joueur détient une clé spécifique et se fermer lorsqu'il la relâche. FiveM fournit le RegisterKeyMapping fonction pour les raccourcis clavier reconfigurables, qui est l'approche préférée aux vérifications de touches codées en dur, car elle permet aux joueurs de personnaliser leurs commandes via le menu des paramètres du jeu. Utilisez un modèle de maintien pour afficher où le NUI s'ouvre lorsque tu appuyes sur une touche et se ferme lorsque tu relâches la touche pour une expérience fluide et non intrusive :
-- 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)
Le + et - les préfixes sur les noms de commandes créent automatiquement une paire de pression et de relâchement. Lorsque le joueur appuie sur la touche liée, le +scoreboard commander des feux. Quand ils le libèrent, -scoreboard les incendies. Notez que SetNuiFocus est appelé avec false, false car le tableau de bord est uniquement affiché et ne nécessite pas de saisie avec la souris. Si tu souhaites un tableau de bord cliquable avec tri ou filtrage, passez true, true à la place et ajoutez un bouton de fermeture dans le NUI.
NUI Rendu avec HTML et CSS
La couche NUI restitue la liste des joueurs sous la forme d'un tableau stylisé ou d'une disposition de carte. Utilisez une toile de fond semi-transparente qui recouvre l'écran de jeu sans bloquer complètement la vue. CSS Grid ou Flexbox fonctionne bien pour aligner les colonnes de données. Codes couleur des valeurs de ping afin que les joueurs puissent identifier rapidement la qualité de la connexion, vert pour un bon ping, jaune pour modéré et rouge pour un ping médiocre. Voici la structure de base du 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>
Stylisez le tableau de bord avec un arrière-plan sombre et semi-transparent et un positionnement fixe pour qu'il reste centré sur l'écran. Ajoutez une hauteur maximale avec défilement par débordement pour la liste des joueurs afin que les serveurs de 200 joueurs ou plus ne poussent pas le tableau de bord hors de l'écran. Pensez à ajouter une animation de fondu subtile à l'aide de transitions CSS pour que l'ouverture et la fermeture soient soignées plutôt que discordantes.
Ajout de fonctionnalités de recherche et de filtrage
Pour les serveurs avec un grand nombre de joueurs, la recherche et le filtrage du tableau de bord deviennent essentiels. Ajoutez une entrée de recherche en haut qui filtre la liste des joueurs par nom, ID de serveur ou titre de poste en fonction du type de joueur. Implémentez entièrement le filtre dans JavaScript du côté NUI afin qu'il n'y ait pas de délai aller-retour sur le serveur. Tu peux également ajouter des en-têtes de colonnes cliquables pour trier par ID, nom, tâche 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);
});
});
L'état de tri est maintenu côté client, donc le basculement entre l'ordre croissant et décroissant est instantané. Le point indicateur de service à côté du nom du poste fournit un signal visuel rapide indiquant si un joueur est actuellement en service, ce qui est particulièrement utile pour les administrateurs qui parcourent la liste pour vérifier les niveaux de personnel dans différents départements.
Fonctionnalités d'administration et informations détaillées
Lorsqu'un administrateur ouvre le tableau de bord, il devrait voir des colonnes supplémentaires et des boutons d'action auxquels les joueurs réguliers n'ont pas accès. Ajoutez un menu contextuel par clic droit sur les rangées de joueurs qui permet aux administrateurs d'expulser, de bannir, de se téléporter ou de regarder un joueur directement depuis le tableau de bord. Cela transforme le tableau de bord d'un simple affichage en un panneau d'administration léger. Implémentez la vérification des autorisations côté client et côté serveur. Le client vérifie s'il doit restituer les éléments de l'interface utilisateur d'administration et le serveur valide chaque action d'administration indépendamment :
-- 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)
Enregistrez toujours les actions de l'administrateur dans une base de données ou un webhook Discord pour en rendre compte. Incluez l'identifiant de l'administrateur, le joueur cible, l'action entreprise et un horodatage. Cette piste d'audit est essentielle pour résoudre les différends entre les membres du personnel et pour détecter les comptes administrateurs compromis qui pourraient abuser de leurs pouvoirs.
Performances et optimisation
Un tableau de bord qui provoque des chutes d’images va à l’encontre de son objectif. L'erreur de performances la plus courante consiste à actualiser les données du joueur à chaque image ou toutes les quelques millisecondes. L'intervalle d'actualisation automatique ne doit pas être inférieur à trois à cinq secondes, car les données du joueur telles que le ping et l'état des tâches ne changent pas assez rapidement pour justifier des mises à jour plus rapides. Côté NUI, évitez de détruire et de recréer l'intégralité du DOM à chaque actualisation. Utilisez plutôt une approche différentielle qui met à jour uniquement les lignes qui ont changé, ou au minimum, utilisez innerHTML uniquement sur le conteneur de la liste des joueurs plutôt que sur l'ensemble du tableau de bord. Gardez le CSS simple et évitez les effets lourds comme le flou ou l'ombre de la boîte sur chaque ligne, car ceux-ci déclenchent des opérations de composition GPU coûteuses lorsqu'ils sont multipliés sur des centaines de lignes. Pour les très grands serveurs avec 200 joueurs ou plus, implémentez un défilement virtuel qui restitue uniquement les lignes visibles plus un petit tampon, gardant ainsi le nombre de nœuds DOM gérable quel que soit le nombre de joueurs connectés. Testez ton tableau de bord sur un serveur complet pour mesurer l'impact réel sur les performances, car une fonctionnalité qui semble correcte à 10 joueurs peut devenir un goulot d'étranglement à 200.
