Tutorial 2026-04-15

Desenvolvimento do Sistema de Rádio e Dispatch em FiveM

TDYSKY

TDYSKY

Fundador & Lead Developer na Agency Scripts

Arquitetura do sistema de rádio e despacho

Um sistema de rádio e despacho é a espinha dorsal da comunicação dos serviços de emergência em um servidor de roleplay FiveM. Ele permite que a polícia, o EMS e os bombeiros coordenem respostas por meio de canais de rádio estruturados, recebam alertas de despacho priorizados de chamadas para o 911 e sistemas de detecção automatizados e rastreiem a localização das unidades em tempo real por meio da integração GPS. A arquitetura consiste em três sistemas interconectados: a estrutura do canal de rádio que gerencia quem pode falar em qual frequência, a fila de despacho que processa as chamadas recebidas e as encaminha para as unidades apropriadas, e a camada de rastreamento GPS que visualiza as posições das unidades em um mapa compartilhado. Ao contrário da comunicação simples baseada em chat, um sistema de rádio adequado adiciona a imersão de frequências de comutação, ouvindo a vibração do rádio apenas quando sintonizado e seguindo protocolos de comunicação estruturados que refletem os serviços de emergência do mundo real. Todo o sistema funciona através do gerenciamento de estado do lado do servidor para evitar trapaças, com o NUI do lado do cliente fornecendo a interface de rádio e o painel de despacho.

Sistema de canais de rádio

Os canais de rádio organizam a comunicação para que diferentes departamentos e unidades possam coordenar-se sem interferências. Cada canal possui um número de frequência, restrições de acesso baseadas em funções e uma lista de jogadores conectados no momento. O sistema suporta vários tipos de canais: canais departamentais para comunicação geral, canais táticos para operações específicas, como perseguição ou resposta a assalto a banco, e canais interdepartamentais onde a polícia e o EMS podem coordenar incidentes compartilhados. Os jogadores ingressam em um canal por meio de um item de rádio ou interface NUI, e toda voz ou texto transmitido nesse canal atinge apenas os jogadores sintonizados na mesma frequência. Implemente um sistema de alto-falantes prioritários onde os operadores de despacho e os comandantes possam transmitir para todos os canais simultaneamente para alertas de emergência:

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)

Sistema de 10 códigos

Os dez códigos são taquigrafias de rádio padronizadas que adicionam autenticidade à comunicação do serviço de emergência e permitem atualizações rápidas de status por meio do sistema de despacho. Em vez de implementar códigos 10 como meras macros de bate-papo, construa-os como comandos funcionais que atualizam o status do oficial no sistema de despacho e acionam respostas automatizadas. Quando um policial liga para 10-80 (perseguição em andamento), o sistema deve atualizar automaticamente seu status no painel de despacho, criar um alerta de perseguição visível para todas as unidades e iniciar o rastreamento GPS do veículo do policial. Quando eles chamam 10-97 (chegando ao local), o sistema de despacho os marca como presentes para a chamada ativa à qual estão respondendo. Crie o sistema de 10 códigos como uma tabela configurável para que os proprietários de servidores possam personalizar os códigos para corresponder ao protocolo preferido de seu departamento:

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)

Fila de chamadas de despacho

A fila de despacho é o hub central que processa todas as chamadas de emergência recebidas e as encaminha para as unidades apropriadas. Quando um civil liga para o 911, quando um alarme automático é acionado em um banco ou loja, ou quando um policial solicita reforços, o sistema cria uma entrada de despacho com nível de prioridade, localização, descrição e tipo de serviço necessário. A fila exibe todas as chamadas ativas classificadas por prioridade no painel NUI de despacho, mostrando o horário da chamada, localização, status e quais unidades estão atribuídas. Uma função de operador de despacho dedicada pode atribuir chamadas manualmente a unidades específicas, ou os oficiais podem auto-atribuir clicando em uma chamada e marcando-se como respondendo. Implemente o escalonamento automático de chamadas onde as chamadas não atendidas aumentam de prioridade após um tempo limite configurável, garantindo que nenhuma emergência seja ignorada durante períodos de maior movimento:

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)

Rastreamento GPS e Mapa de Unidade

O rastreamento por GPS fornece visibilidade em tempo real de onde todas as unidades ativas estão posicionadas no mapa, permitindo que despachantes e comandantes tomem decisões informadas sobre atribuição. A posição de cada oficial de serviço é transmitida ao sistema de despacho em um intervalo configurável, normalmente a cada 5 a 10 segundos, e exibida em um painel de mapa NUI compartilhado, acessível aos operadores de despacho e supervisores. O mapa mostra mensagens de unidade codificadas por cores por departamento e status, com ícones indicando se estão disponíveis, respondendo, no local ou em perseguição. Inclui um recurso de trilha que rastreia o movimento da unidade nos últimos minutos para que os despachantes possam ver a direção da viagem durante as perseguições. Implemente a visualização do raio de chamada que desenha um círculo ao redor das chamadas de despacho ativas, mostrando a área de resposta recomendada. Quando um despachante precisa encontrar a unidade disponível mais próxima de uma nova chamada, o sistema pode calcular distâncias de todas as unidades disponíveis e sugerir a atribuição ideal. Mantenha a transmissão GPS eficiente enviando atualizações de posição apenas quando a unidade se moveu além do limite mínimo de distância para reduzir o tráfego de rede em servidores ocupados.

Sistema de alerta e botão de pânico

O sistema de alerta lida com notificações de alta prioridade que exigem atenção imediata de todas as unidades. O alerta mais crítico é o botão de pânico, ativado quando um policial liga para 10-78 ou 10-99, que envia sua localização GPS para todas as unidades em serviço com um sinal intermitente e um som de alarme distinto. O alerta de pânico deve substituir a prioridade normal da fila de despacho e ser exibido com destaque na tela de cada policial até ser reconhecido. Implemente diferentes níveis de alerta que acionam respostas crescentes: um alerta padrão cria uma chamada de despacho com um som de notificação, um alerta urgente pisca no painel de despacho e reproduz um tom de aviso, e um alerta crítico aciona uma notificação em tela inteira com som de sirene e roteamento GPS automático para o local do alerta. Os alertas automatizados integram-se a outros sistemas de servidor para que as zonas de detecção de tiros acionem alertas quando as armas são disparadas em áreas públicas, os radares sinalizam os veículos que excedem os limites de velocidade e os sistemas de alarme das lojas criam chamadas de despacho quando os roubos começam. Cada tipo de alerta tem tempos de espera configuráveis ​​para evitar que spam seja acionado repetidamente na mesma área.

Função de Operador de Despacho

O operador de despacho é uma função dedicada que fica em uma estação de trabalho e gerencia o fluxo de chamadas de emergência para as unidades de campo. Ao contrário dos oficiais que veem uma notificação de despacho simplificada, o operador tem um console de despacho NUI completo com gerenciador de filas, lista de unidades, mapa ao vivo e ferramentas de comunicação. A operadora pode priorizar as chamadas arrastando-as para a fila, atribuir unidades específicas às chamadas com base na proximidade e disponibilidade, marcar as chamadas como resolvidas quando as unidades relatam que foram liberadas e transmitir mensagens para todas as unidades em uma frequência de departamento. Implemente um sistema de estação de trabalho onde os operadores de despacho devam estar sentados em um balcão de despacho designado para acessar o console completo, evitando que operem o sistema de despacho enquanto estiverem em campo. A função de operador deve ter a sua própria progressão de nível de trabalho, onde despachantes experientes obtêm acesso a ferramentas de coordenação interdepartamentais e capacidades de transmissão de emergência. Acompanhe as métricas de desempenho de despacho, como tempo médio de resposta, chamadas tratadas por turno e taxa de resolução de chamadas, para fornecer aos operadores feedback sobre seu desempenho e fornecer aos administradores de servidores dados para decisões de pessoal.

Integração de rádio de voz

Para servidores que usam soluções de bate-papo por voz, como pma-voice ou mumble-voip, integre o sistema de rádio com voz de proximidade para criar uma comunicação de rádio realista. Quando um jogador transmite em um canal de rádio pressionando uma tecla, sua voz deve ser ouvida por todos os jogadores na mesma frequência, independentemente da proximidade física, com um efeito de filtro de rádio aplicado para distinguir a comunicação por rádio da conversa cara a cara. Implemente um sistema de silenciador onde os jogadores devem liberar sua tecla de transmissão antes que outro jogador possa falar, simulando o comportamento real do rádio half-duplex. Adicione efeitos sonoros de rádio para aumentar e diminuir a tecla para fornecer feedback de áudio quando alguém inicia e interrompe a transmissão. A integração de voz deve respeitar os controles de acesso aos canais definidos no sistema de rádio, para que os jogadores que não aderiram a uma frequência através do rádio NUI não possam ouvir ou transmitir nela, mesmo que de alguma forma conheçam o ID do canal mumble. Para servidores sem bate-papo por voz, utilize um sistema de rádio baseado em texto, onde as mensagens enviadas em uma frequência aparecem em um formato de bate-papo estilizado com o indicativo do remetente e o identificador do canal.

Integração e Extensibilidade

Seu sistema de rádio e despacho deve fornecer uma API limpa que outros recursos possam usar para criar chamadas de despacho, enviar alertas e consultar o status da unidade sem a necessidade de entender a implementação interna. Exporte funções como CreateDispatchCall, SendAlert, GetAvailableUnits e GetUnitStatus para que qualquer recurso no servidor possa ser integrado ao sistema de despacho. Quando um assalto a banco começa, o recurso bancário chama a exportação de despacho para criar uma chamada. Quando o EMS recebe uma chamada para o 911, o recurso do hospital a encaminha através do mesmo sistema de despacho. Essa abordagem centralizada garante que todas as comunicações de emergência fluam através de um único sistema com tratamento de prioridades, rastreamento de unidades e registro consistentes. Armazene o histórico de chamadas de despacho no banco de dados para revisão administrativa e gere relatórios de turno que resumem o volume de chamadas, os tempos de resposta e a atividade da unidade para cada período de serviço. Conecte o sistema de despacho aos webhooks do Discord para que alertas críticos, como chamadas de oficiais inativos ou assaltos a bancos, apareçam em um canal de equipe dedicado, mantendo os administradores do servidor informados mesmo quando não estão no jogo.

Partilhar este artigo

Pronto para melhorar o teu servidor?

Explora os nossos scripts FiveM premium na loja Agency Scripts ou junta-te à nossa comunidade no Discord para suporte e atualizações.