Wróć do bloga
Tutorial12 min czytania

System radia i dyspozytorni FiveM: PMA, pma-voice i CAD

Skonfiguruj radio FiveM, dyspozytornię i systemy CAD. Kanały pma-voice, narzędzia MDT i najlepsze skrypty dla policji, EMS i mechaników.

Agency Scripts

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

Architektura systemu radia i dispatchu

System radia i dispatchu to podstawa komunikacji służb ratunkowych na serwerze roleplay FiveM. Umożliwia policji, EMS i straży pożarnej koordynację działań poprzez ustrukturyzowane kanały radiowe, odbieranie priorytetowych zgłoszeń dispatch z połączeń 911 i automatycznych systemów wykrywania oraz śledzenie lokalizacji jednostek w czasie rzeczywistym dzięki integracji z GPS. Architektura składa się z trzech połączonych systemów: frameworka kanałów radiowych zarządzającego tym, kto może rozmawiać na danej częstotliwości, kolejki dispatch przetwarzającej połączenia przychodzące i kierującej je do odpowiednich jednostek oraz warstwy śledzenia GPS wizualizującej pozycje jednostek na wspólnej mapie. W przeciwieństwie do prostej komunikacji na czacie, właściwy system radiowy dodaje immersję zmiany częstotliwości, słyszenia rozmów radiowych tylko po dostrojeniu i przestrzegania ustrukturyzowanych protokołów komunikacji, które odzwierciedlają prawdziwe służby ratunkowe. Cały system działa w oparciu o zarządzanie stanem po stronie serwera, aby zapobiec oszustwom, a NUI po stronie klienta zapewnia interfejs radia i panel dispatch.

System kanałów radiowych

Kanały radiowe porządkują komunikację, dzięki czemu różne frakcje i jednostki mogą koordynować działania bez zakłóceń. Każdy kanał ma numer częstotliwości, ograniczenia dostępu oparte na rolach zawodowych oraz listę aktualnie połączonych graczy. System obsługuje wiele typów kanałów: kanały frakcyjne do ogólnej komunikacji, kanały taktyczne do konkretnych akcji, takich jak pościg czy napad na bank, oraz kanały międzyfrakcyjne, na których policja i EMS mogą współpracować przy wspólnych incydentach. Gracze dołączają do kanału za pomocą przedmiotu radia lub interfejsu NUI, a cały głos i tekst przesyłany na tym kanale dociera tylko do graczy ustawionych na tę samą częstotliwość. Wdróż system priorytetowego nadawania, w którym dyspozytorzy i dowódcy mogą nadawać na wszystkich kanałach jednocześnie w przypadku alertów alarmowych:

Config.RadioChannels = {
    -- Police Department
    { frequency = 1, label = 'PD Main',        jobs = {'police'},          type = 'department' },
    { frequency = 2, label = 'PD Patrol',       jobs = {'police'},          type = 'department' },
    { frequency = 3, label = 'PD Tactical',     jobs = {'police'},          type = 'tactical',  maxUsers = 8 },
    { frequency = 4, label = 'PD Detectives',   jobs = {'police'},          type = 'tactical',  minRank = 3 },

    -- EMS / Fire
    { frequency = 10, label = 'EMS Main',       jobs = {'ambulance'},       type = 'department' },
    { frequency = 11, label = 'EMS Field',      jobs = {'ambulance'},       type = 'department' },
    { frequency = 12, label = 'Fire Main',      jobs = {'fire'},            type = 'department' },

    -- Inter-department
    { frequency = 20, label = 'Emergency Joint', jobs = {'police','ambulance','fire'}, type = 'inter' },
    { frequency = 21, label = 'Command Channel', jobs = {'police','ambulance','fire'}, type = 'command', minRank = 5 },

    -- Civilian (if radio item owned)
    { frequency = 50, label = 'Civilian Band',  jobs = {},                  type = 'open' },
}

-- Server-side channel state
local radioState = {} -- [frequency] = { players = {}, priority = false }

RegisterNetEvent('radio:server:joinChannel', function(frequency)
    local src = source
    local Player = QBCore.Functions.GetPlayer(src)
    if not Player then return end

    local channel = nil
    for _, ch in ipairs(Config.RadioChannels) do
        if ch.frequency == frequency then channel = ch break end
    end
    if not channel then return end

    -- Check job access
    if #channel.jobs > 0 then
        local playerJob = Player.PlayerData.job.name
        local hasAccess = false
        for _, job in ipairs(channel.jobs) do
            if job == playerJob then hasAccess = true break end
        end
        if not hasAccess then
            TriggerClientEvent('QBCore:Notify', src, 'No access to this frequency', 'error')
            return
        end
    end

    -- Check rank requirement
    if channel.minRank and Player.PlayerData.job.grade.level < channel.minRank then
        TriggerClientEvent('QBCore:Notify', src, 'Insufficient rank for this channel', 'error')
        return
    end

    -- Check max users for tactical channels
    if channel.maxUsers and radioState[frequency] then
        if #radioState[frequency].players >= channel.maxUsers then
            TriggerClientEvent('QBCore:Notify', src, 'Channel full', 'error')
            return
        end
    end

    -- Leave current channel
    LeaveCurrentChannel(src)

    -- Join new channel
    if not radioState[frequency] then
        radioState[frequency] = { players = {}, priority = false }
    end
    table.insert(radioState[frequency].players, src)

    TriggerClientEvent('radio:client:joined', src, frequency, channel.label)
    TriggerClientEvent('QBCore:Notify', src, 'Tuned to ' .. channel.label .. ' (' .. frequency .. ')', 'success')
end)

System 10-Code

Kody 10 to ustandaryzowane skróty radiowe, które dodają autentyczności komunikacji służb ratunkowych i umożliwiają szybkie aktualizacje statusu przez system dispatch. Zamiast wdrażać kody 10 jako zwykłe makra czatu, stwórz je jako funkcjonalne komendy, które aktualizują status funkcjonariusza w systemie dispatch i wywołują automatyczne odpowiedzi. Gdy funkcjonariusz zgłasza 10-80 (pościg w toku), system powinien automatycznie zaktualizować ich status na tablicy dyspozytora, utworzyć alert pościgu widoczny dla wszystkich jednostek i rozpocząć śledzenie GPS pojazdu funkcjonariusza. Gdy zgłoszą 10-97 (przybycie na miejsce), system dyspozytorski oznacza ich jako obecnych na miejscu zdarzenia dla aktywnego wezwania, na które odpowiadają. Zbuduj system kodów 10-code jako konfigurowalną tabelę, aby właściciele serwerów mogli dostosować kody do preferowanego protokołu swojego departamentu:

Config.TenCodes = {
    ['10-4']  = { label = 'Acknowledged',           action = nil,                           status = nil },
    ['10-6']  = { label = 'Busy',                    action = nil,                           status = 'busy' },
    ['10-7']  = { label = 'Out of Service',          action = 'setOffDuty',                  status = 'off_duty' },
    ['10-8']  = { label = 'In Service',              action = 'setOnDuty',                   status = 'available' },
    ['10-11'] = { label = 'Traffic Stop',            action = 'createTrafficStop',            status = 'traffic_stop' },
    ['10-15'] = { label = 'Suspect in Custody',      action = nil,                           status = 'transport' },
    ['10-20'] = { label = 'Location Request',        action = 'shareLocation',               status = nil },
    ['10-23'] = { label = 'Arrived at Scene',        action = 'markOnScene',                 status = 'on_scene' },
    ['10-32'] = { label = 'Person with Weapon',      action = 'createAlert',                 status = 'responding', priority = 'high' },
    ['10-41'] = { label = 'Beginning Tour of Duty',  action = 'clockIn',                     status = 'available' },
    ['10-42'] = { label = 'Ending Tour of Duty',     action = 'clockOut',                    status = 'off_duty' },
    ['10-71'] = { label = 'Shooting',                action = 'createAlert',                 status = 'responding', priority = 'critical' },
    ['10-78'] = { label = 'Officer Needs Assistance', action = 'panicButton',                status = 'emergency', priority = 'critical' },
    ['10-80'] = { label = 'Pursuit in Progress',     action = 'startPursuit',                status = 'pursuit', priority = 'high' },
    ['10-97'] = { label = 'Arriving on Scene',       action = 'markOnScene',                 status = 'on_scene' },
    ['10-99'] = { label = 'Officer Down',            action = 'officerDown',                 status = 'emergency', priority = 'critical' },
    ['code4'] = { label = 'No Further Assistance',   action = 'clearScene',                  status = 'available' },
}

RegisterNetEvent('radio:server:tenCode', function(code)
    local src = source
    local Player = QBCore.Functions.GetPlayer(src)
    if not Player then return end

    local codeConfig = Config.TenCodes[code]
    if not codeConfig then return end

    -- Update officer status
    if codeConfig.status then
        UpdateUnitStatus(src, codeConfig.status)
    end

    -- Execute associated action
    if codeConfig.action then
        ExecuteCodeAction(codeConfig.action, src, codeConfig)
    end

    -- Broadcast to channel
    local channel = GetPlayerChannel(src)
    if channel then
        local name = GetUnitCallsign(src) or Player.PlayerData.charinfo.firstname
        BroadcastToChannel(channel, {
            type = 'tencode',
            code = code,
            label = codeConfig.label,
            unit = name,
            priority = codeConfig.priority,
        })
    end
end)

Kolejka zgłoszeń dispatch

Kolejka dyspozytorska to centralny punkt przetwarzający wszystkie przychodzące zgłoszenia alarmowe i kierujący je do odpowiednich jednostek. Gdy obywatel zadzwoni pod 911, gdy w banku lub sklepie włączy się automatyczny alarm albo gdy policjant wezwie wsparcie, system tworzy wpis dyspozytorski z poziomem priorytetu, lokalizacją, opisem i typem wymaganej służby. Kolejka wyświetla wszystkie aktywne zgłoszenia posortowane według priorytetu na panelu NUI dispatch, pokazując czas zgłoszenia, lokalizację, status oraz przypisane jednostki. Dedykowana rola dyspozytora może ręcznie przypisywać zgłoszenia do konkretnych jednostek, a funkcjonariusze mogą przypisywać się sami, klikając zgłoszenie i oznaczając swój dojazd. Wdróż automatyczną eskalację zgłoszeń, w której nieodebrane wezwania zyskują wyższy priorytet po konfigurowalnym czasie, co daje pewność, że żadne zgłoszenie alarmowe nie zostanie zignorowane w godzinach szczytu:

local dispatchQueue = {}
local callIdCounter = 0

function CreateDispatchCall(data)
    callIdCounter = callIdCounter + 1

    local call = {
        id = callIdCounter,
        type = data.type or 'general',          -- police, ems, fire, general
        priority = data.priority or 'standard', -- low, standard, urgent, critical
        code = data.code or nil,                -- e.g., '10-71', '211' (robbery)
        title = data.title,
        description = data.description or '',
        coords = data.coords,
        street = GetStreetName(data.coords),
        blip = data.blip or nil,
        caller = data.caller or 'Unknown',
        callerId = data.callerId or nil,
        timestamp = os.time(),
        status = 'pending',                     -- pending, assigned, responding, on_scene, resolved
        assignedUnits = {},
        escalationTimer = nil,
        metadata = data.metadata or {},
    }

    -- Set escalation timer
    if call.priority ~= 'critical' then
        call.escalationTimer = os.time() + Config.EscalationTimeout
    end

    table.insert(dispatchQueue, call)

    -- Notify appropriate department
    local targetJobs = GetJobsForCallType(call.type)
    local players = QBCore.Functions.GetQBPlayers()

    for _, player in pairs(players) do
        local job = player.PlayerData.job
        if job.onduty and TableContains(targetJobs, job.name) then
            TriggerClientEvent('dispatch:client:newCall', player.PlayerData.source, call)
        end
    end

    -- Play alert sound based on priority
    if call.priority == 'critical' then
        PlayDispatchAlert('critical', targetJobs)
    end

    return call.id
end

-- Automated dispatch triggers
AddEventHandler('banking:robberyStarted', function(bankId, coords)
    CreateDispatchCall({
        type = 'police',
        priority = 'critical',
        code = '211',
        title = 'Bank Robbery in Progress',
        description = 'Silent alarm triggered at ' .. GetBankLabel(bankId),
        coords = coords,
        blip = { sprite = 500, color = 1, scale = 1.5, flash = true },
    })
end)

AddEventHandler('hospital:911call', function(callerId, coords, description, injuries)
    CreateDispatchCall({
        type = 'ems',
        priority = injuries and injuries.maxSeverity >= 80 and 'critical' or 'urgent',
        title = 'Medical Emergency',
        description = description,
        coords = coords,
        callerId = callerId,
        caller = GetPlayerName(callerId),
        blip = { sprite = 153, color = 3, scale = 1.2 },
        metadata = { injuries = injuries },
    })
end)

Śledzenie GPS i mapa jednostek

Śledzenie GPS zapewnia widoczność pozycji wszystkich aktywnych jednostek na mapie w czasie rzeczywistym, co pozwala dyspozytorom i dowódcom na podejmowanie trafnych decyzji o dysponowaniu siłami. Pozycja każdego funkcjonariusza na służbie jest wysyłana do systemu dyspozytorskiego w konfigurowalnych odstępach czasu, zazwyczaj co 5-10 sekund, i wyświetlana na wspólnym panelu mapy NUI dostępnym dla dyspozytorów i przełożonych. Mapa wyświetla blipy jednostek oznaczone kolorami według wydziału i statusu, z ikonami wskazującymi, czy jednostka jest dostępna, w drodze, na miejscu zdarzenia czy w pościgu. Dodaj funkcję śladu trasy, która rejestruje ruch jednostki z ostatnich kilku minut, aby dyspozytorzy mogli widzieć kierunek jazdy podczas pościgów. Wdróż wizualizację promienia zgłoszenia, która rysuje okrąg wokół aktywnych zgłoszeń dyspozytorskich, pokazując zalecany obszar reakcji. Gdy dyspozytor musi znaleźć najbliższą dostępną jednostkę do nowego wezwania, system może obliczyć odległości od wszystkich wolnych jednostek i zasugerować optymalne przypisanie. Zadbaj o wydajność transmisji GPS, wysyłając aktualizacje pozycji tylko wtedy, gdy jednostka przemieści się o więcej niż minimalny próg odległości, aby ograniczyć ruch sieciowy na obciążonych serwerach.

System powiadomień i panic button

System alertów obsługuje powiadomienia o wysokim priorytecie, które wymagają natychmiastowej reakcji wszystkich jednostek. Najważniejszym alertem jest panic button, aktywowany, gdy funkcjonariusz wzywa pomoc 10-78 lub 10-99, co wysyła ich pozycję GPS do wszystkich jednostek na służbie z migającym blipem i charakterystycznym dźwiękiem alarmu. Alert paniki powinien nadpisywać standardowy priorytet kolejki dyspozytorskiej i wyświetlać się wyraźnie na ekranie każdego funkcjonariusza do momentu potwierdzenia. Zaimplementuj różne poziomy alertów wyzwalające eskalujące reakcje: standardowy alert tworzy wezwanie dyspozytorskie z dźwiękiem powiadomienia, pilny alert miga na panelu dyspozytorskim i odtwarza sygnał ostrzegawczy, a krytyczny alert wyzwala pełnoekranowe powiadomienie z dźwiękiem syreny i automatycznym wyznaczaniem trasy GPS do miejsca zdarzenia. Zautomatyzowane alerty integrują się z innymi systemami serwera, dzięki czemu strefy wykrywania strzałów wyzwalają alerty po użyciu broni w miejscach publicznych, fotoradary rejestrują pojazdy przekraczające dozwoloną prędkość, a systemy alarmowe sklepów tworzą zgłoszenia dyspozytorskie po rozpoczęciu napadu. Każdy typ alertu ma konfigurowalny cooldown, aby zapobiegać spamowi z powtarzających się wyzwalaczy w tym samym obszarze.

Rola operatora dispatch

Dyspozytor to dedykowana rola, która przy stanowisku pracy zarządza przepływem zgłoszeń alarmowych do jednostek w terenie. W przeciwieństwie do funkcjonariuszy widzących uproszczone powiadomienie dispatch, dyspozytor dysponuje pełną konsolą dyspozytorską NUI z menedżerem kolejki, listą jednostek, mapą na żywo i narzędziami komunikacji. Dyspozytor może ustalać priorytety zgłoszeń, przeciągając je w kolejce, przypisywać konkretne jednostki do wezwań na podstawie odległości i dostępności, oznaczać zgłoszenia jako rozwiązane, gdy jednostki zgłoszą zakończenie działań, oraz nadawać komunikaty do wszystkich jednostek na częstotliwości departamentu. Zaimplementuj system stanowiska pracy wymagający od dyspozytora siedzenia przy wyznaczonym biurku dyspozytorskim w celu uzyskania dostępu do pełnej konsoli, co uniemożliwi obsługę systemu dispatch w terenie. Rola dyspozytora powinna mieć własną progresję stopni zawodowych, w której doświadczeni dyspozytorzy zyskują dostęp do narzędzi koordynacji międzywydziałowej i funkcji komunikatów alarmowych. Śledź wskaźniki wydajności dyspozytora, takie jak średni czas reakcji, liczba zgłoszeń obsłużonych na zmianie oraz wskaźnik rozwiązanych wezwań, aby zapewnić operatorom informację zwrotną o ich pracy, a administratorom serwera dane potrzebne do podejmowania decyzji kadrowych.

Integracja radia głosowego

W przypadku serwerów korzystających z rozwiązań voice chat, takich jak pma-voice lub mumble-voip, zintegruj system radiowy z głosem przestrzennym, aby stworzyć realistyczną komunikację radiową. Kiedy gracz nadaje na kanale radiowym, trzymając przypisany klawisz, jego głos powinien być słyszalny dla wszystkich graczy na tej samej częstotliwości bez względu na odległość, z nałożonym efektem filtra radiowego odróżniającym komunikację radiową od rozmowy twarzą w twarz. Zaimplementuj system blokady szumów, w którym gracze muszą puścić klawisz nadawania, zanim inny gracz będzie mógł mówić, symulując rzeczywiste działanie radia half-duplex. Dodaj efekty dźwiękowe radia dla naciśnięcia i puszczenia klawisza, aby zapewnić informację zwrotną audio przy rozpoczęciu i zakończeniu nadawania. Integracja głosowa powinna respektować uprawnienia dostępu do kanałów zdefiniowane w systemie radiowym, aby gracze, którzy nie dołączyli do częstotliwości przez NUI radia, nie mogli jej słuchać ani na niej nadawać, nawet jeśli jakoś poznają ID kanału mumble. W przypadku serwerów bez czatu głosowego zastosuj awaryjnie tekstowy system radiowy, w którym wiadomości wysyłane na częstotliwości pojawiają się w ostylowanym formacie czatu ze znakiem wywoławczym nadawcy i identyfikatorem kanału.

Integracja i rozszerzalność

Twój system radia i dispatchu powinien udostępniać przejrzyste API, z którego inne zasoby mogą korzystać do tworzenia zgłoszeń dispatch, wysyłania alertów i sprawdzania statusu jednostek bez konieczności znajomości wewnętrznej implementacji. Wyeksportuj funkcje takie jak CreateDispatchCall, SendAlert, GetAvailableUnits, oraz GetUnitStatus dzięki czemu dowolny resource na serwerze może zintegrować się z systemem dispatch. Gdy rozpoczyna się napad na bank, resource bankowości wywołuje export dispatch, aby utworzyć zgłoszenie. Gdy EMS odbiera zgłoszenie 911, resource szpitala przekazuje je przez ten sam system dispatch. To scentralizowane podejście zapewnia, że cała komunikacja ratunkowa przepływa przez jeden system ze spójną obsługą priorytetów, śledzeniem jednostek i logowaniem. Przechowuj historię zgłoszeń dispatch w bazie danych do wglądu administracji i generuj raporty ze zmian podsumowujące liczbę zgłoszeń, czas reakcji oraz aktywność jednostek dla każdego okresu służby. Połącz system dispatch z webhookami Discord, aby krytyczne alerty, takie jak zgłoszenia officer-down czy napady na bank, pojawiały się na dedykowanym kanale dla administracji, informując administratorów serwera nawet wtedy, gdy nie ma ich w grze.

Gotowy, aby zacząć?

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