>
Tutorial 2026-02-26

Criar Sistemas de Diálogo Interativo com NPCs para FiveM

TDYSKY

TDYSKY

Fundador & Lead Developer na Agency Scripts

Por que o diálogo com NPCs é importante

Os sistemas de diálogo NPC transformam peds estáticos em personagens vivos que dão personalidade ao seu servidor e guiam os jogadores através das atividades. Sem diálogo, os NPCs são pouco mais que alvos de interação que abrem menus. Com um sistema de diálogo adequado, eles se tornam entregadores de missões, contadores de histórias, donos de lojas com atitude e informantes que reagem de maneira diferente com base na reputação ou no trabalho do jogador. Um sistema de diálogo cria oportunidades para conhecimentos específicos do servidor, histórias ramificadas e escolhas do jogador que afetam interações futuras. Pense em como é muito mais envolvente quando um jogador se aproxima de um NPC mecânico que o cumprimenta pelo nome, comenta sobre as condições de seu carro e oferece serviços diferentes dependendo se ele é um cliente antigo ou um visitante de primeira viagem. Esse nível de imersão mantém os jogadores investidos em seu servidor e lhes dá motivos para explorar além dos loops padrão.

Estrutura de dados da árvore de diálogo

A base de qualquer sistema de diálogo é a estrutura de dados que define o fluxo da conversa. Uma árvore de diálogo consiste em nós, onde cada nó contém o texto do NPC e uma lista de opções de resposta do jogador que se conectam a outros nós. Essa estrutura em árvore oferece suporte a conversas ramificadas, caminhos condicionais baseados no estado do jogador e loops que retornam a pontos anteriores da conversa. Projete seus nós de diálogo para serem orientados por dados, para que a equipe do servidor possa criar novas conversas sem tocar no código Lua. Aqui está um formato prático de árvore de diálogo:

Config.Dialogues = {
    ['mechanic_greeting'] = {
        npcName = 'Tony the Mechanic',
        nodes = {
            ['start'] = {
                text = "Hey there! Car giving you trouble, or are you just here for a tune-up?",
                animation = 'WORLD_HUMAN_WELDING',
                options = {
                    {
                        label = "I need repairs",
                        next = 'repairs',
                        condition = function(player)
                            return IsPlayerInVehicle(player)
                        end
                    },
                    {
                        label = "What services do you offer?",
                        next = 'services'
                    },
                    {
                        label = "Just browsing, thanks",
                        next = 'goodbye'
                    },
                }
            },
            ['repairs'] = {
                text = "Let me take a look... Yeah, your engine's seen better days. I can fix it up for $500. What do you say?",
                options = {
                    { label = "Fix it up", next = 'repair_accept', action = 'repair_vehicle' },
                    { label = "Too expensive", next = 'haggle' },
                    { label = "Never mind", next = 'goodbye' },
                }
            },
            ['haggle'] = {
                text = "Look, parts aren't cheap. But since you seem like a decent person, I can do $350. Final offer.",
                options = {
                    { label = "Deal!", next = 'repair_accept', action = 'repair_vehicle_discount' },
                    { label = "I'll pass", next = 'goodbye' },
                }
            },
            ['services'] = {
                text = "I do repairs, custom paint jobs, performance tuning, and tire changes. What catches your eye?",
                options = {
                    { label = "Tell me about tuning", next = 'tuning_info' },
                    { label = "Back to start", next = 'start' },
                }
            },
            ['repair_accept'] = {
                text = "Alright, give me a minute... Done! She's running smooth now. Take care of her out there.",
                options = {
                    { label = "Thanks, Tony!", next = 'end' },
                }
            },
            ['goodbye'] = {
                text = "No worries. Come back anytime you need help with your ride!",
                options = {
                    { label = "See you around", next = 'end' },
                }
            },
        }
    },
}

A função condition nas opções permite mostrar ou ocultar dinamicamente as escolhas com base no estado atual do jogador. Uma opção de reparo só aparece quando o jogador chega em um veículo, as opções relacionadas à missão só aparecem quando o jogador atinge o estágio certo e as opções VIP podem ser restritas a jogadores com certas permissões. O campo action aciona funções do lado do servidor quando uma opção específica é selecionada, conectando as opções de diálogo aos resultados do jogo.

Gerando e Gerenciando NPCs

Os NPCs de diálogo precisam ser gerados de forma confiável, colocados em locais consistentes e mantidos imunes ao caos que o mundo do GTA pode lançar sobre eles. Ao gerar um NPC de diálogo, você precisa solicitar o modelo, criar o ped, defini-lo como uma entidade de missão para que os sistemas de limpeza do GTA não o excluam, congelar sua posição para que ele não se afaste e torná-lo invencível para que os jogadores não possam matar seu doador de missão. Use um gerenciador de NPC centralizado que gera peds quando os jogadores estão próximos e os desaparece quando nenhum jogador está ao alcance para economizar memória em servidores ocupados:

local spawnedNPCs = {}

function SpawnDialogueNPC(npcId, config)
    if spawnedNPCs[npcId] then return end

    local model = GetHashKey(config.model)
    RequestModel(model)
    while not HasModelLoaded(model) do Wait(10) end

    local ped = CreatePed(0, model, config.coords.x, config.coords.y,
        config.coords.z, config.heading, false, true)

    SetEntityInvincible(ped, true)
    SetBlockingOfNonTemporaryEvents(ped, true)
    FreezeEntityPosition(ped, true)
    SetPedFleeAttributes(ped, 0, false)
    SetPedCombatAttributes(ped, 46, true)
    SetPedCanRagdoll(ped, false)
    SetEntityAsMissionEntity(ped, true, true)
    SetModelAsNoLongerNeeded(model)

    -- Play idle animation if configured
    if config.scenario then
        TaskStartScenarioInPlace(ped, config.scenario, 0, true)
    end

    spawnedNPCs[npcId] = { entity = ped, config = config }
    return ped
end

A combinação de SetBlockingOfNonTemporaryEvents e FreezeEntityPosition garante que eventos ambientais como explosões próximas, perseguições policiais ou jogadores agressivos não façam com que seu NPC fuja, revide ou faça uma boneca de pano. Sem essas proteções, os jogadores poderiam encontrar um entregador de missões caído no chão se contorcendo após ser atropelado por um carro que passava, quebrando completamente a imersão que você trabalhou para criar.

Sistema de câmera cinematográfica

O trabalho da câmera durante conversas de diálogo eleva a experiência de uma interação de menu a um momento cinematográfico. Quando um diálogo começar, crie uma câmera que focalize o rosto do NPC com um leve deslocamento, usando a profundidade de campo para desfocar o fundo e chamar a atenção para a conversa. Alterne entre os ângulos da câmera conforme a conversa avança, cortando para o jogador quando ele faz uma escolha e voltando para o NPC quando ele responde. O sistema de câmera nativo do GTA oferece controle total sobre posição, rotação, campo de visão e profundidade de campo:

local dialogueCam = nil

function StartDialogueCamera(npcPed)
    local npcCoords = GetEntityCoords(npcPed)
    local npcHeading = GetEntityHeading(npcPed)
    local playerPed = PlayerPedId()

    -- Calculate camera position offset from NPC face
    local angleRad = math.rad(npcHeading + 160)
    local camX = npcCoords.x + (math.sin(angleRad) * 1.5)
    local camY = npcCoords.y + (math.cos(angleRad) * 1.5)
    local camZ = npcCoords.z + 0.6

    dialogueCam = CreateCam('DEFAULT_SCRIPTED_CAMERA', true)
    SetCamCoord(dialogueCam, camX, camY, camZ)
    PointCamAtPedBone(dialogueCam, npcPed, 31086, 0.0, 0.0, 0.1, true)  -- Head bone

    -- Depth of field for cinematic look
    SetCamNearDof(dialogueCam, 0.5)
    SetCamFarDof(dialogueCam, 3.5)
    SetCamDofStrength(dialogueCam, 0.6)
    SetCamUseShallowDofMode(dialogueCam, true)

    SetCamFov(dialogueCam, 40.0)  -- Tighter shot
    SetCamActive(dialogueCam, true)
    RenderScriptCams(true, true, 800, true, false)

    -- Disable player controls during dialogue
    SetPlayerControl(PlayerId(), false, 0)

    -- Make player face NPC
    TaskTurnPedToFaceEntity(playerPed, npcPed, 1000)
end

function StopDialogueCamera()
    if dialogueCam then
        RenderScriptCams(false, true, 500, true, false)
        DestroyCam(dialogueCam, true)
        dialogueCam = nil
        SetPlayerControl(PlayerId(), true, 0)
    end
end

O nativo PointCamAtPedBone é particularmente poderoso porque trava o foco da câmera na cabeça do NPC, independentemente de qualquer leve movimento de animação, mantendo o enquadramento consistente durante toda a conversa. As durações de transição em RenderScriptCams criam fades de câmera suaves em vez de cortes bruscos, e você deve experimentar valores entre 500ms e 1000ms para encontrar a sensação certa para o ritmo do seu servidor.

Sistema de exibição de legendas

Um sistema de legendas apresenta o texto do diálogo do NPC de uma forma visualmente atraente na parte inferior da tela, imitando como os jogos baseados em histórias exibem a conversa. Em vez de descartar todo o bloco de texto de uma vez, implemente um efeito de máquina de escrever que revele os caracteres um de cada vez, criando a ilusão de que o NPC está falando ativamente. Use NUI para exibição de legendas porque oferece controle CSS total sobre fontes, cores, animações e posicionamento. Envie o texto de cada nó de diálogo para o quadro NUI quando ele se tornar ativo, junto com o nome do NPC e quaisquer tags de emoção que devam afetar o estilo de exibição:

// Subtitle display JavaScript (html/subtitles.js)
let typewriterTimeout = null;

window.addEventListener('message', (event) => {
    const data = event.data;

    if (data.action === 'showDialogue') {
        clearTimeout(typewriterTimeout);
        const container = document.getElementById('subtitle-container');
        const nameEl = document.getElementById('npc-name');
        const textEl = document.getElementById('dialogue-text');
        const optionsEl = document.getElementById('dialogue-options');

        container.style.display = 'block';
        nameEl.textContent = data.npcName;
        textEl.textContent = '';
        optionsEl.innerHTML = '';

        // Typewriter effect
        let charIndex = 0;
        const fullText = data.text;

        function typeNext() {
            if (charIndex < fullText.length) {
                textEl.textContent += fullText[charIndex];
                charIndex++;
                typewriterTimeout = setTimeout(typeNext, 30);
            } else {
                // Show options after text completes
                showOptions(data.options);
            }
        }
        typeNext();
    }

    if (data.action === 'hideDialogue') {
        document.getElementById('subtitle-container').style.display = 'none';
    }
});

function showOptions(options) {
    const optionsEl = document.getElementById('dialogue-options');
    options.forEach((opt, index) => {
        const btn = document.createElement('button');
        btn.className = 'dialogue-option';
        btn.innerHTML = `${index + 1} ${opt.label}`;
        btn.onclick = () => {
            fetch(`https://${GetParentResourceName()}/selectOption`, {
                method: 'POST',
                body: JSON.stringify({ index: index })
            });
        };
        optionsEl.appendChild(btn);
    });
}

Estilize o contêiner de legenda com um fundo escuro semitransparente, cantos arredondados e uma borda gradiente sutil que corresponda ao tema do seu servidor. Posicione-o na parte inferior central da tela com preenchimento suficiente para que não se sobreponha ao minimapa ou outros elementos do HUD. Adicione atalhos de teclado para que os jogadores possam pressionar teclas numéricas para selecionar opções rapidamente sem clicar, o que parece mais natural durante o fluxo da conversa.

Integração do sistema Quest

Os sistemas de diálogo tornam-se verdadeiramente poderosos quando conectados a uma estrutura de busca. O campo action da árvore de diálogo nas opções de resposta fornece o ponto de gancho onde a lógica da missão é executada. Quando um jogador aceita uma missão por meio de diálogo, o manipulador de ação deve criar uma entrada de missão no registro de missões do jogador, definir quaisquer pontos de passagem ou objetivos necessários e acompanhar o progresso por meio de interações de diálogo subsequentes. Armazene o progresso da missão no banco de dados por jogador para que eles possam se desconectar e continuar de onde pararam. Projete missões como máquinas de estado onde cada estado corresponde a um nó de diálogo e um conjunto de objetivos que devem ser concluídos antes que o próximo diálogo esteja disponível:

-- Server-side quest actions triggered by dialogue choices
local QuestActions = {
    ['accept_delivery_job'] = function(src, npcId)
        local Player = QBCore.Functions.GetPlayer(src)
        local citizenid = Player.PlayerData.citizenid

        -- Create quest entry
        MySQL.insert(
            'INSERT INTO player_quests (citizenid, quest_id, stage, started_at) VALUES (?, ?, ?, NOW())',
            {citizenid, 'tony_delivery_1', 'pickup'}
        )

        -- Set waypoint for pickup location
        TriggerClientEvent('quest:client:setWaypoint', src, {
            coords = vector3(482.5, -1311.2, 29.2),
            blipSprite = 501,
            blipColor = 5,
            label = 'Package Pickup'
        })

        TriggerClientEvent('QBCore:Notify', src, 'Quest started: Special Delivery', 'success')
    end,

    ['complete_delivery'] = function(src, npcId)
        local Player = QBCore.Functions.GetPlayer(src)
        local citizenid = Player.PlayerData.citizenid

        MySQL.update(
            'UPDATE player_quests SET stage = ?, completed_at = NOW() WHERE citizenid = ? AND quest_id = ?',
            {'completed', citizenid, 'tony_delivery_1'}
        )

        Player.Functions.AddMoney('cash', 1500, 'quest-delivery-reward')
        TriggerClientEvent('QBCore:Notify', src, 'Quest complete! Reward: $1,500', 'success')
    end,
}

Use o estágio de missão para modificar dinamicamente quais nós de diálogo estão disponíveis. Quando um jogador retorna para Tony após completar a entrega, o sistema de diálogo verifica o estágio da missão e apresenta um diálogo de conclusão com recompensa em vez da saudação inicial. Isso cria um fluxo natural de conversa onde os NPCs reconhecem o progresso do jogador e reagem de acordo, fazendo com que o mundo pareça responsivo e vivo.

Animações e Expressões de NPCs

NPCs estáticos que ficam perfeitamente parados enquanto falam parecem robóticos e quebram a imersão. Adicione suporte de animação ao seu sistema de diálogo para que os NPCs gesticulem, emocionem e reajam durante as conversas. GTA V possui uma enorme biblioteca de dicionários de animação que cobrem gestos, expressões faciais e linguagem corporal que você pode acionar em pontos específicos do diálogo. Atribua animações no nível do nó para que cada linha de diálogo possa ter seu próprio gesto de acompanhamento. Quando o NPC der boas notícias, reproduza uma alegre animação de aceno de mão. Quando eles discutirem algo sério, use uma postura severa de braços cruzados. Para momentos ociosos entre as respostas do jogador, faça um loop de uma animação de pensamento ou espera. Você também pode usar animações faciais nativas como SetFacialIdleAnimOverride para alterar a expressão de repouso do NPC para combinar com o clima da conversa, fazendo-o parecer feliz, zangado, assustado ou confuso. Combine animações corporais e faciais para obter performances mais convincentes e sempre teste as animações no jogo porque alguns dicionários de animação parecem diferentes em diferentes modelos de ped, e o que funciona em um ped masculino pode ser cortado ou parecer estranho em uma ped feminina.

Desempenho e Melhores Práticas

Um sistema de diálogo que gera dezenas de NPCs no mapa precisa de um gerenciamento cuidadoso de desempenho. Gere NPCs apenas quando os jogadores estiverem a uma distância de renderização, normalmente de 50 a 100 metros, e os desapareça quando nenhum jogador estiver por perto. Use um único thread para gerenciar todas as distâncias de spawn do NPC em vez de criar threads separados por NPC, pois isso reduz drasticamente a sobrecarga no lado do cliente. Armazene em cache os dados da árvore de diálogo no início do recurso, em vez de ler arquivos durante as conversas. Mantenha as transições da câmera de diálogo suaves, mas não crie e destrua câmeras excessivamente, pois as operações da câmera têm um custo de desempenho mensurável. Quando vários jogadores interagem com o mesmo NPC simultaneamente, cada jogador deve obter sua própria instância de diálogo que é executada de forma independente, o que significa que o estado do diálogo deve ser armazenado por jogador e não na entidade NPC. Limpe todos os recursos de diálogo quando o jogador se desconecta ou se afasta no meio da conversa, destruindo a câmera, liberando o foco do NUI e restaurando os controles do jogador. Teste seu sistema de diálogo com o pior cenário em mente: o jogo do jogador trava no meio do diálogo ou ele pressiona F4 enquanto uma câmera está ativa. Seu sistema deve detectar esses casos e fazer uma limpeza adequada para evitar câmeras persistentes ou controles bloqueados na reconexão.

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.