العودة إلى المدونة
Tutorial8 دقيقة قراءة

نظام لوحة نتائج FiveM: قوائم اللاعبين، البينغ والوظائف

ابنِ لوحة نتائج نظيفة لـ FiveM. عدد اللاعبين، البينغ، الوظائف، المعرفات وأفضل سكريبتات لوحة النتائج لاستبدال قائمة TAB الافتراضية بأناقة.

Agency Scripts

المؤسس والمطور الرئيسي في Agency Scripts

لماذا بناء لوحة نتائج مخصصة

قائمة اللاعبين الافتراضية في FiveM عملية لكنها تفتقر إلى التلميع وكثافة المعلومات التي تحتاجها خوادم اللعب التمثيلي. تسمح لوحة النتائج المخصصة بعرض أسماء اللاعبين إلى جانب هويتهم داخل الشخصية، المسمى الوظيفي، معرف الخادم، وجودة الاتصال، جميعها مصممة لتتناسب مع علامة خادمك التجارية. إلى جانب الجمالية، تمنحك لوحة النتائج المخصصة تحكمًا في المعلومات المرئية لمجموعات اللاعبين المختلفة. قد يرى اللاعبون العاديون أسماء الشخصيات والمعرفات، بينما يرى المسؤولون حقولًا إضافية مثل معرفات 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)

تتحقق الدالة الراجعة مما إذا كان اللاعب الذي يطلب لديه أذونات المسؤول قبل تضمين معلومات حساسة مثل أسماء 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 لجعل الفتح والإغلاق يبدو مصقولًا بدلاً من مفاجئ.

إضافة وظيفة البحث والتصفية

لخوادم ذات أعداد لاعبين كبيرة، يصبح البحث وتصفية لوحة النتائج أمرًا ضروريًا. أضف حقل بحث في الأعلى يقوم بتصفية قائمة اللاعبين حسب الاسم، معرف الخادم، أو لقب الوظيفة أثناء كتابة اللاعب. نفذ الفلتر بالكامل بجافا سكريبت على جانب NUI بحيث لا يكون هناك تأخير في الرحلة إلى الخادم. يمكنك أيضًا إضافة رؤوس أعمدة قابلة للنقر للفرز حسب المعرف، الاسم، الوظيفة، أو البينغ:

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

يتم الحفاظ على حالة الفرز على جانب العميل بحيث يكون التبديل بين الترتيب التصاعدي والتنازلي فوريًا. تشير نقطة مؤشر الخدمة بجانب اسم الوظيفة إلى ما إذا كان اللاعب في الخدمة حاليًا، وهو مفيد بشكل خاص للمسؤولين الذين يفحصون القائمة للتحقق من مستويات التوظيف عبر الأقسام المختلفة.

ميزات المشرف ومعلومات موسعة

عندما يفتح المسؤول لوحة النتائج، يجب أن يرى أعمدة إضافية وأزرار إجراءات لا يمكن للاعبين العاديين الوصول إليها. أضف قائمة سياق بزر الفأرة الأيمن على صفوف اللاعبين تتيح للمسؤولين طرد، حظر، الانتقال إلى، أو مراقبة لاعب مباشرة من لوحة النتائج. يحول هذا لوحة النتائج من عرض بسيط إلى لوحة إدارة خفيفة الوزن. نفذ فحص الأذونات على جانب العميل والخادم. يتحقق العميل مما إذا كان يجب عرض عناصر واجهة الإدارة، ويتحقق الخادم من كل إجراء إداري بشكل مستقل:

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

سجل دائمًا إجراءات المسؤولين في قاعدة بيانات أو webhook على Discord للمساءلة. ضمن معرف المسؤول، اللاعب المستهدف، الإجراء المتخذ، والطابع الزمني. هذا السجل التدقيقي ضروري لحل النزاعات بين أعضاء الطاقم وللكشف عن حسابات المسؤولين المخترقة التي قد تسيء استخدام صلاحياتها.

الأداء والتحسين

لوحة النتائج التي تسبب انخفاض الإطارات تهدم هدفها. الخطأ الأكثر شيوعاً في الأداء هو تحديث بيانات اللاعب كل إطار أو كل بضعة ميلي ثانية. يجب ألا تقل فترة التحديث التلقائي عن ثلاث إلى خمس ثوانٍ لأن بيانات اللاعب مثل البينغ وحالة الوظيفة لا تتغير بسرعة كافية لتبرير تحديثات أسرع. على جانب NUI، تجنب تدمير وإعادة إنشاء DOM بالكامل في كل تحديث. بدلاً من ذلك، استخدم نهج التفريق الذي يحدث فقط الصفوف التي تغيرت، أو على الأقل، استخدم innerHTML على حاوية قائمة اللاعبين فقط بدلاً من لوحة النتائج بأكملها. اجعل CSS بسيطًا وتجنب التأثيرات الثقيلة مثل التمويه أو ظل الصندوق على كل صف لأنها تؤدي إلى عمليات تجميع GPU مكلفة عند تكرارها عبر مئات الصفوف. للخوادم الكبيرة جدًا التي تضم 200 لاعب أو أكثر، نفذ التمرير الافتراضي الذي يعرض فقط الصفوف المرئية بالإضافة إلى مخزن صغير، مما يحافظ على عدد عقد DOM قابل للإدارة بغض النظر عن عدد اللاعبين المتصلين. اختبر لوحة النتائج على خادم كامل لقياس التأثير الفعلي على الأداء، لأن ميزة تبدو جيدة مع 10 لاعبين قد تصبح عنق زجاجة عند 200.

جاهز للبدء؟

احصل على السكربتات من متجرنا، أو انضم إلى Discord للدعم والتحديثات ونظرة على ما هو قادم.