Wróć do bloga
Tutorial7 min czytania

System powiadomień FiveM: Toasty, alerty i ox_lib

Zbuduj dopracowany system powiadomień FiveM. Komunikaty toast, paski postępu, menu radialne oraz informacje, jak ox_lib i czołowe skrypty obsługują UI klienta.

Agency Scripts

Założyciel i główny deweloper w Agency Scripts

Dlaczego warto stworzyć własny system powiadomień?

Domyślne powiadomienia FiveM są funkcjonalne, ale brakuje im dopracowania wizualnego i elastyczności. Większość serwerów roleplay polega na generycznych wyskakujących okienkach tekstowych, które wyglądają identycznie, przez co graczom trudno odróżnić błąd od potwierdzenia sukcesu czy powiadomienia informacyjnego. Własny system powiadomień oparty na NUI daje Ci pełną kontrolę nad stylem, animacjami, pozycjonowaniem, efektami dźwiękowymi i zachowaniem kolejki. Przekształca podstawowy element interfejsu w coś dopasowanego do tożsamości wizualnej Twojego serwera i znacznie poprawia wrażenia graczy. W tym poradniku zbudujemy kompletny system powiadomień typu toast od zera przy użyciu Lua po stronie serwera i klienta, z HTML, CSS i JavaScript napędzającymi frontend NUI.

Konfiguracja warstwy NUI

Podstawą każdego niestandardowego systemu powiadomień w FiveM jest warstwa NUI (New UI). NUI pozwala renderować zawartość HTML jako nakładkę na grę, co oznacza, że masz do dyspozycji pełną moc nowoczesnych technologii webowych. Zacznij od stworzenia struktury zasobu z fxmanifest.lua, skrypt Lua po stronie klienta oraz html folder zawierający pliki twojego UI. Manifest musi zadeklarować twoją stronę NUI i zarejestrować procedury obsługi komunikatów, aby twoje skrypty Lua mogły komunikować się z frontendem.

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

Twój html/index.html plik powinien być minimalistyczny. Wystarczy kontener div, do którego powiadomienia będą dynamicznie wstrzykiwane przez JavaScript. Zachowaj prosty HTML, ponieważ każdy element powiadomienia jest tworzony i niszczony programowo. Podepnij arkusz stylów oraz skrypt i upewnij się, że body ma przezroczyste tło, aby świat gry był widoczny za powiadomieniami.

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

Projektowanie stylów CSS dla powiadomień toast

Powiadomienia toast powinny różnić się wizualnie w zależności od typu. Zdefiniuj osobne schematy kolorów dla sukces, błąd, informacje, oraz ostrzeżenie powiadomienia. Umieść kontener w prawym górnym rogu ekranu za pomocą pozycjonowania fixed i układaj powiadomienia pionowo z niewielkim odstępem między nimi. Każdy toast powinien mieć subtelny efekt glassmorphism z rozmyciem tła (backdrop blur), kolorowy lewy pasek wskazujący typ oraz płynne animacje pojawiania się i znikania. Użyj reguł CSS keyframes do wsuwania z prawej strony i zanikania po wygaśnięciu powiadomienia.

/* 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%; }
}

Tworzenie kolejki powiadomień w JavaScript

Warstwa JavaScript obsługuje komunikaty przychodzące z Lua, tworzy elementy DOM, zarządza kolejką i odpowiada za automatyczne zamykanie powiadomień. Kolejka powiadomień jest kluczowa, ponieważ nie chcesz, aby na ekranie gromadziło się jednocześnie dziesięć powiadomień. Ogranicz liczbę widocznych powiadomień do maksymalnie pięciu i kolejkuj nadmiarowe, aby pojawiały się wraz z wygasaniem wcześniejszych. Każde powiadomienie powinno mieć konfigurowalny czas trwania, a kliknięcie powinno je natychmiast zamykać. Pasek postępu na dole każdego powiadomienia toast daje graczom wizualną informację, jak długo powiadomienie pozostanie na ekranie.

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

Integracja Lua po stronie klienta

Po stronie klienta potrzebujesz funkcji wysyłającej komunikaty NUI do warstwy HTML oraz exportu, dzięki czemu inne resource'y mogą wywoływać powiadomienia bez bezpośredniej zależności od wnętrza Twojego skryptu. Ten SendNUIMessage native przekazuje dane do kontekstu przeglądarki, gdzie odbiera je twój JavaScript. Owiń to w czystą funkcję API przyjmującą tytuł, treść, typ i opcjonalny czas trwania. Zarejestruj zarówno event kliencki, jak i export, aby powiadomienia mogły być wywoływane zarówno ze skryptów serwerowych, jak i innych zasobów klienta. To podwójne podejście zapewnia maksymalną kompatybilność z dowolnym frameworkiem.

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

Wysyłanie zdarzeń po stronie serwera

Skrypt po stronie serwera zapewnia funkcje pomocnicze do wysyłania powiadomień do konkretnych graczy, wszystkich graczy lub grup graczy. To tutaj obsługujesz takie przypadki użycia, jak nadawanie ogłoszeń, wysyłanie potwierdzeń transakcji czy powiadamianie administratorów o podejrzanej aktywności. Dzięki scentralizowaniu logiki wysyłania na serwerze zachowujesz pojedynczy punkt kontroli nad doręczaniem powiadomień. Możesz tu również dodać rate limiting, aby zapobiec spamowaniu powiadomieniami przez złośliwe lub błędne skrypty klienta. Serwer powinien walidować parametry powiadomień przed ich przekazaniem, aby zapobiec NUI injection poprzez spreparowane wiadomości.

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

Dodawanie efektów dźwiękowych i dopracowanie szczegółów

Efekty dźwiękowe podnoszą powiadomienia z elementu czysto wizualnego do wielozmysłowego mechanizmu informacji zwrotnej. Gracze często mają grę w tle lub skupiają się na prowadzeniu pojazdu i mogą przegapić ciche powiadomienie toast. Krótki, subtelny dzwonek dla powiadomień o sukcesie, niski brzęczyk przy błędach i delikatny ping dla alertów informacyjnych sprawiają, że gracze rejestrują ważne informacje nawet wtedy, gdy nie patrzą na obszar powiadomień. Twoje pliki audio powinny być krótkie, poniżej 500 milisekund, a dla kompatybilności z przeglądarką używaj formatu OGG. Ustaw głośność na około 30 procent, aby dźwięk komponował się z dźwiękiem z gry, zamiast go zagłuszać. Przechowuj pliki dźwiękowe w swoim html/sounds katalogu i wstępnie załaduj je w JavaScript, aby uniknąć opóźnień w odtwarzaniu przy pierwszym powiadomieniu.

Zaawansowane funkcje i personalizacja

Gdy podstawowy system już działa, rozważ dodanie zaawansowanych funkcji, aby wyróżnić swój serwer. Trwałe powiadomienia, które pozostają na ekranie, dopóki gracz ich nie kliknie, przydają się przy ważnych alertach, takich jak połączenia telefoniczne czy liczniki więzienia. Przyciski akcji w powiadomieniach pozwalają graczom reagować bezpośrednio, na przykład akceptować prośbę o wymianę bez otwierania osobnego menu. Możesz również wdrożyć grupowanie powiadomień, w którym powtarzające się identyczne powiadomienia zwijają się w pojedynczy toast z licznikiem pokazującym liczbę wystąpień. W celu integracji z frameworkiem stwórz pliki bridge nadpisujące domyślne funkcje powiadomień w QBCore lub ESX, dzięki czemu każdy resource na serwerze automatycznie korzysta z Twojego niestandardowego systemu bez zmian w kodzie. Ten wzorzec zastępowania domyślnych funkcji frameworka własną implementacją to przejrzysty sposób na uaktualnienie UI całego serwera w jednym kroku.

Pasujący skrypt

Gotowe na twój serwer, aktualizacje w zestawie.

Gotowy, aby zacząć?

Kup skrypty w naszym sklepie lub dołącz do Discorda, aby uzyskać wsparcie, aktualizacje i sprawdzić nadchodzące nowości.