نظام الإشعارات في FiveM: التنبيهات، التنبيهات المنبثقة و ox_lib
ابنِ نظام إشعارات مصقول لـ FiveM. التنبيهات، أشرطة التقدم، القوائم الدائرية وكيفية تعامل ox_lib وأفضل السكريبتات مع ردود فعل واجهة المستخدم للعميل.
Agency Scripts
المؤسس والمطور الرئيسي في Agency Scripts
لماذا بناء نظام إشعارات مخصص؟
إشعارات FiveM الافتراضية عملية لكنها تفتقر إلى الصقل البصري والمرونة. تعتمد معظم خوادم الدور على نوافذ نصية عامة تبدو متطابقة، مما يصعب على اللاعبين التمييز بين خطأ، تأكيد نجاح، أو تنبيه معلوماتي. نظام إشعارات مخصص مبني باستخدام NUI يمنحك تحكمًا كاملاً في التصميم، الرسوم المتحركة، التمركز، المؤثرات الصوتية، وسلوك الطابور. يحول عنصر واجهة المستخدم الأساسي إلى شيء يتماشى مع هوية علامتك التجارية على الخادم ويحسن تجربة اللاعب بشكل كبير. في هذا الدرس، سنبني نظام إشعارات توست كامل من الصفر باستخدام Lua على جانب الخادم والعميل، مع HTML وCSS وJavaScript لتشغيل واجهة NUI الأمامية.
إعداد طبقة NUI
أساس أي نظام إشعارات مخصص في FiveM هو طبقة NUI (واجهة المستخدم الجديدة). تتيح لك NUI عرض محتوى HTML كطبقة فوق اللعبة، مما يعني أن لديك القوة الكاملة لتقنيات الويب الحديثة تحت تصرفك. ابدأ بإنشاء هيكل المورد الخاص بك مع fxmanifest.lua ، سكريبت Lua على جانب العميل، و html المجلد الذي يحتوي على ملفات واجهة المستخدم الخاصة بك. يجب أن يعلن البيان عن صفحة NUI الخاصة بك ويسجل معالجات الرسائل حتى تتمكن سكريبتات Lua الخاصة بك من التواصل مع الواجهة الأمامية.
-- fxmanifest.lua
fx_version 'cerulean'
game 'gta5'
ui_page 'html/index.html'
files {
'html/index.html',
'html/style.css',
'html/script.js',
'html/sounds/*.ogg'
}
client_script 'client.lua'
server_script 'server.lua'
خاصتك html/index.html الملف القديم
<!-- html/index.html -->
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="notification-container"></div>
<script src="script.js"></script>
</body>
</html>
تصميم CSS لإشعارات التوست
يجب أن تكون إشعارات التوست مميزة بصريًا حسب النوع. عرّف مخططات ألوان منفصلة لـ نجاح, خطأ, معلومات، و تحذير الإشعارات. ضع الحاوية في الزاوية العلوية اليمنى من الشاشة باستخدام تحديد موقع ثابت، ورتب الإشعارات عموديًا مع وجود فجوة صغيرة بينها. يجب أن يحتوي كل إشعار على تأثير زجاجي خفيف مع تمويه الخلفية، وحد أيسر ملون للدلالة على النوع، وحركات دخول وخروج سلسة. استخدم إطارات CSS الرئيسية للانزلاق من اليمين والتلاشي عند انتهاء الإشعار.
/* html/style.css */
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: transparent; font-family: 'Segoe UI', sans-serif; overflow: hidden; }
#notification-container {
position: fixed;
top: 20px;
right: 20px;
display: flex;
flex-direction: column;
gap: 10px;
z-index: 9999;
max-width: 380px;
width: 100%;
}
.toast {
background: rgba(15, 15, 25, 0.85);
backdrop-filter: blur(12px);
border-radius: 10px;
padding: 14px 18px;
border-left: 4px solid #3b82f6;
color: #e2e8f0;
animation: slideIn 0.4s cubic-bezier(0.16, 1, 0.3, 1) forwards;
display: flex;
align-items: flex-start;
gap: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
}
.toast.success { border-left-color: #22c55e; }
.toast.error { border-left-color: #ef4444; }
.toast.warning { border-left-color: #f59e0b; }
.toast.info { border-left-color: #3b82f6; }
.toast-icon { font-size: 20px; flex-shrink: 0; margin-top: 2px; }
.toast-body { flex: 1; }
.toast-title { font-weight: 700; font-size: 14px; margin-bottom: 4px; }
.toast-msg { font-size: 13px; color: #94a3b8; line-height: 1.5; }
.toast-progress {
position: absolute;
bottom: 0; left: 0;
height: 3px;
background: currentColor;
border-radius: 0 0 0 10px;
animation: progress linear forwards;
}
@keyframes slideIn {
from { opacity: 0; transform: translateX(100px); }
to { opacity: 1; transform: translateX(0); }
}
@keyframes slideOut {
from { opacity: 1; transform: translateX(0); }
to { opacity: 0; transform: translateX(100px); }
}
@keyframes progress {
from { width: 100%; }
to { width: 0%; }
}
بناء قائمة انتظار إشعارات JavaScript
طبقة JavaScript تتعامل مع الرسائل الواردة من Lua، تنشئ عناصر DOM، تدير الطابور، وتتعامل مع الإغلاق التلقائي. طابور الإشعارات ضروري لأنك لا تريد تراكم عشر إشعارات على الشاشة في نفس الوقت. حدد الحد الأقصى للمرئي إلى خمسة، وضع أي إشعارات زائدة في الطابور لتظهر عند انتهاء صلاحية السابقة. يجب أن يكون لكل إشعار مدة قابلة للتكوين، والنقر على إشعار يجب أن يغلقه فورًا. شريط التقدم في أسفل كل إشعار يعطي اللاعبين مؤشرًا بصريًا لمدة بقاء الإشعار على الشاشة.
// html/script.js
const container = document.getElementById('notification-container');
const MAX_VISIBLE = 5;
const queue = [];
let activeCount = 0;
const icons = {
success: '✔',
error: '✖',
warning: '⚠',
info: 'ℹ'
};
const sounds = {
success: new Audio('sounds/success.ogg'),
error: new Audio('sounds/error.ogg'),
warning: new Audio('sounds/warning.ogg'),
info: new Audio('sounds/info.ogg')
};
window.addEventListener('message', (event) => {
if (event.data.type === 'showNotification') {
addNotification(event.data);
}
});
function addNotification(data) {
if (activeCount >= MAX_VISIBLE) {
queue.push(data);
return;
}
createToast(data);
}
function createToast(data) {
activeCount++;
const duration = data.duration || 5000;
const toast = document.createElement('div');
toast.className = `toast ${data.style || 'info'}`;
toast.style.position = 'relative';
toast.innerHTML = `
<div class="toast-icon">${icons[data.style] || icons.info}</div>
<div class="toast-body">
<div class="toast-title">${data.title || ''}</div>
<div class="toast-msg">${data.message}</div>
</div>
<div class="toast-progress" style="animation-duration:${duration}ms"></div>
`;
toast.addEventListener('click', () => dismissToast(toast));
container.appendChild(toast);
if (data.sound !== false && sounds[data.style]) {
sounds[data.style].currentTime = 0;
sounds[data.style].play().catch(() => {});
}
setTimeout(() => dismissToast(toast), duration);
}
function dismissToast(toast) {
if (toast.dataset.dismissed) return;
toast.dataset.dismissed = 'true';
toast.style.animation = 'slideOut 0.3s ease forwards';
setTimeout(() => {
toast.remove();
activeCount--;
if (queue.length > 0) {
createToast(queue.shift());
}
}, 300);
}
تكامل Lua على جانب العميل
على جانب العميل، تحتاج إلى دالة ترسل رسائل NUI إلى طبقة HTML الخاصة بك وتصدير حتى تتمكن الموارد الأخرى من تفعيل الإشعارات دون الاعتماد المباشر على داخلية سكربتك. ال SendNUIMessage native يدفع البيانات إلى سياق المتصفح حيث يلتقطها JavaScript الخاص بك. غلف هذا في دالة API نظيفة تقبل عنوانًا، رسالة، نوعًا، ومدة اختيارية. سجل حدث عميل وتصديرًا بحيث يمكن تشغيل الإشعارات من سكربتات الخادم وموارد العميل الأخرى. يضمن هذا النهج المزدوج أقصى توافق مع أي إطار عمل.
-- client.lua
local function ShowNotification(title, message, style, duration, sound)
SendNUIMessage({
type = 'showNotification',
title = title or '',
message = message or '',
style = style or 'info',
duration = duration or 5000,
sound = sound ~= false
})
end
-- Export for other resources
exports('ShowNotification', ShowNotification)
-- Event-based trigger from server
RegisterNetEvent('notifications:show', function(title, message, style, duration)
ShowNotification(title, message, style, duration)
end)
-- Convenience commands for testing
RegisterCommand('testnotify', function()
ShowNotification('Success', 'Your item has been saved.', 'success', 4000)
Wait(500)
ShowNotification('Error', 'Insufficient funds for this purchase.', 'error', 5000)
Wait(500)
ShowNotification('Warning', 'Your vehicle is low on fuel.', 'warning', 4000)
Wait(500)
ShowNotification('Info', 'Press E to interact with the NPC.', 'info', 3000)
end, false)
إرسال الأحداث على جانب الخادم
يوفر السكربت على جانب الخادم وظائف مساعدة لإرسال الإشعارات إلى لاعبين محددين، أو جميع اللاعبين، أو مجموعات من اللاعبين. هنا تتعامل مع حالات الاستخدام مثل بث الإعلانات، إرسال تأكيدات المعاملات، أو تنبيه المسؤولين عن نشاط مشبوه. من خلال مركزية منطق الإرسال على الخادم، تحافظ على نقطة تحكم واحدة في تسليم الإشعارات. يمكنك أيضًا إضافة تحديد معدل هنا لمنع الرسائل المزعجة الناتجة عن سكربتات العميل الخبيثة أو المعطوبة. يجب على الخادم التحقق من معلمات الإشعار قبل إعادة التوجيه لمنع حقن NUI عبر رسائل مصممة خصيصًا.
-- server.lua
local function NotifyPlayer(source, title, message, style, duration)
if not source or source <= 0 then return end
title = tostring(title or '')
message = tostring(message or '')
style = style or 'info'
TriggerClientEvent('notifications:show', source, title, message, style, duration)
end
local function NotifyAll(title, message, style, duration)
TriggerClientEvent('notifications:show', -1, title, message, style, duration)
end
exports('NotifyPlayer', NotifyPlayer)
exports('NotifyAll', NotifyAll)
-- Example: welcome notification
AddEventHandler('playerJoining', function()
local src = source
Wait(3000)
NotifyPlayer(src, 'Welcome', 'Welcome to the server! Have fun.', 'success', 6000)
end)
-- Admin broadcast command
RegisterCommand('broadcast', function(source, args)
if source > 0 and not IsPlayerAceAllowed(source, 'command.broadcast') then return end
local msg = table.concat(args, ' ')
NotifyAll('Announcement', msg, 'info', 8000)
end, true)
إضافة مؤثرات صوتية وتحسينات
المؤثرات الصوتية ترفع الإشعارات من عنصر بصري فقط إلى آلية ردود فعل متعددة الحواس. غالباً ما يكون اللاعبون لديهم اللعبة في الخلفية أو يركزون على القيادة وقد يفوتهم إشعار صامت. رنين قصير ودقيق لإشعارات النجاح، طنين منخفض للأخطاء، ونغمة رقيقة للتنبيهات المعلوماتية تضمن تسجيل اللاعبين للمعلومات المهمة حتى عندما لا ينظرون إلى منطقة الإشعارات. اجعل ملفات الصوت قصيرة، أقل من 500 مللي ثانية، واستخدم صيغة OGG لتوافق المتصفح. اضبط مستوى الصوت حوالي 30 بالمئة ليختلط مع صوت اللعبة بدلاً من أن يطغى عليه. خزّن ملفات الصوت في html/sounds الدليل وتحميلها مسبقًا في JavaScript لتجنب تأخير التشغيل في الإشعار الأول.
ميزات متقدمة وتخصيص
بمجرد أن يعمل النظام الأساسي، فكر في إضافة ميزات متقدمة لتمييز خادمك. الإشعارات المستمرة التي تبقى على الشاشة حتى ينقر اللاعب عليها صراحة مفيدة للتنبيهات المهمة مثل المكالمات الهاتفية المعلقة أو مؤقتات السجن. أزرار الإجراءات داخل الإشعارات تتيح للاعبين الرد مباشرة، مثلاً قبول طلب تجارة دون فتح قائمة منفصلة. يمكنك أيضاً تنفيذ تجميع الإشعارات حيث تتجمع الإشعارات المتكررة المتطابقة في إشعار واحد مع شارة عداد تظهر عدد المرات. للتكامل مع الأُطُر، أنشئ ملفات جسر تتجاوز وظائف الإشعارات الافتراضية في QBCore أو ESX بحيث يستخدم كل مورد على خادمك نظامك المخصص تلقائياً دون أي تغييرات في الكود. هذا النمط من استبدال الإعدادات الافتراضية للأُطُر بتنفيذك الخاص هو طريقة نظيفة لترقية واجهة المستخدم الكاملة للخادم في خطوة واحدة.