Voltar para o Blog
Quest Log

Como Criar um NPC no Godot 4: Rotina, Interação e Diálogo

Cena 2D da Godot com um NPC de vila conversando com o personagem do jogador

Aprenda a criar um NPC no Godot 4 passo a passo: patrulha por waypoints, máquina de estados, detecção do jogador com Area2D e diálogo. Com código completo.

Saber como criar NPC no Godot é um divisor de águas em qualquer projeto 2D: é o momento em que a vila deixa de ser um cenário morto e vira um lugar habitado. Neste tutorial você vai montar um NPC completo no Godot 4: ele fica parado um tempo, patrulha entre pontos do mapa, mostra um balãozinho de "!" quando o jogador se aproxima e abre uma caixa de diálogo quando o jogador aperta a tecla de interação. Tudo em GDScript com tipagem estática, usando uma máquina de estados que você poderá reaproveitar em qualquer personagem do seu jogo.

Montando a cena do NPC

A estrutura da cena é parecida com a de um jogador, com dois acréscimos: uma Area2D para detectar aproximação e os nós de interface do balão e do diálogo.

Npc (CharacterBody2D)
├── Sprite2D
├── CollisionShape2D
├── AreaInteracao (Area2D)
│   └── CollisionShape2D
├── Balao (Label)
└── CanvasLayer
    └── CaixaDialogo (PanelContainer)
        └── TextoDialogo (RichTextLabel)

Crie uma cena nova com um CharacterBody2D como raiz e renomeie para Npc. Adicione um Sprite2D com a textura do personagem e um CollisionShape2D do tamanho do corpo. A AreaInteracao recebe um CollisionShape2D próprio, com um CircleShape2D bem maior que o corpo, algo entre 60 e 90 pixels de raio: essa é a distância em que o NPC "percebe" o jogador.

O Balao é um Label simples com o texto "!" posicionado acima da cabeça do sprite. A CaixaDialogo fica dentro de um CanvasLayer para ser desenhada por cima do jogo, sem sofrer com o zoom da câmera. Dentro dela, um RichTextLabel chamado TextoDialogo exibe as falas. Deixe o balão e a caixa invisíveis por padrão, o script controla quando aparecem.

Antes de escrever código, abra Projeto, Configurações do Projeto, aba Mapa de Entrada, e crie uma ação chamada interact mapeada para a tecla E. É ela que vai disparar a conversa.

A máquina de estados: enum e match

Um NPC de vila tem pelo menos três comportamentos: parado, andando e conversando. Sem organização, isso vira uma pilha de booleanos (esta_andando, esta_falando) que conflitam entre si. A solução clássica é um enum com os estados possíveis e um match que executa só a lógica do estado atual. Se quiser entender o padrão a fundo, o tutorial de máquina de estados na Godot destrincha o conceito; aqui vamos aplicar a versão compacta, suficiente para um NPC.

Este é o esqueleto do script, com todas as variáveis que o tutorial vai usar:

extends CharacterBody2D

enum Estado { IDLE, WALK, TALK }

@export var waypoints: Array[Vector2] = []
@export var velocidade: float = 60.0
@export var tempo_parado: float = 2.0
@export var falas: Array[String] = [
    "Bem-vindo à vila, viajante!",
    "Dizem que a caverna ao norte esconde um tesouro.",
    "Mas ninguém que entrou lá voltou para confirmar."
]

var estado: Estado = Estado.IDLE
var indice_waypoint: int = 0
var indice_fala: int = 0
var tempo_espera: float = 0.0
var jogador_perto: CharacterBody2D = null

@onready var sprite: Sprite2D = $Sprite2D
@onready var balao: Label = $Balao
@onready var area_interacao: Area2D = $AreaInteracao
@onready var caixa_dialogo: PanelContainer = $CanvasLayer/CaixaDialogo
@onready var texto_dialogo: RichTextLabel = $CanvasLayer/CaixaDialogo/TextoDialogo


func _ready() -> void:
    area_interacao.body_entered.connect(_ao_jogador_entrar)
    area_interacao.body_exited.connect(_ao_jogador_sair)
    balao.hide()
    caixa_dialogo.hide()
    tempo_espera = tempo_parado


func _physics_process(delta: float) -> void:
    match estado:
        Estado.IDLE:
            _processar_idle(delta)
        Estado.WALK:
            _processar_walk()
        Estado.TALK:
            velocity = Vector2.ZERO
    move_and_slide()

Repare em dois detalhes de tipagem estática que valem para o projeto inteiro: Array[Vector2] e Array[String] são arrays tipados, então o editor acusa na hora se você tentar enfiar um valor errado neles, e jogador_perto: CharacterBody2D = null deixa explícito que a variável guarda um corpo ou nada. O match no _physics_process é o coração do NPC: cada estado tem a sua função, e o estado TALK simplesmente zera a velocity, congelando o personagem durante a conversa.

Como criar NPC no Godot que patrulha waypoints

A rotina de patrulha alterna entre IDLE e WALK. No IDLE, o NPC desconta um cronômetro; quando o tempo acaba, ele parte para o próximo waypoint. No WALK, ele caminha até o ponto atual e, ao chegar, volta para IDLE e reinicia o cronômetro.

func _processar_idle(delta: float) -> void:
    velocity = Vector2.ZERO
    tempo_espera -= delta
    if tempo_espera <= 0.0 and waypoints.size() > 1:
        estado = Estado.WALK


func _processar_walk() -> void:
    var alvo: Vector2 = waypoints[indice_waypoint]
    if global_position.distance_to(alvo) < 4.0:
        indice_waypoint = (indice_waypoint + 1) % waypoints.size()
        tempo_espera = tempo_parado
        estado = Estado.IDLE
        return
    var direcao: Vector2 = global_position.direction_to(alvo)
    velocity = velocity.move_toward(direcao * velocidade, velocidade * 8.0 * get_physics_process_delta_time())
    sprite.flip_h = direcao.x < 0.0

Alguns pontos que merecem atenção. O teste distance_to(alvo) < 4.0 usa uma margem em vez de comparar posições exatas, porque o NPC se move em passos discretos e dificilmente cai no pixel exato do waypoint; sem a margem, ele fica orbitando o ponto para sempre. O operador % faz o índice dar a volta quando chega ao fim do array, criando um circuito fechado: o NPC visita os pontos em ordem e recomeça. E o move_toward na velocity acelera o personagem gradualmente em vez de aplicar a velocidade cheia de um frame para o outro, o que deixa o movimento menos robótico.

Para definir os waypoints, selecione o NPC na cena principal e edite o array waypoints no inspetor, adicionando duas ou três posições globais do mapa, como a porta da casa e a beira do poço. Como o array está exportado, cada instância do NPC pode ter a própria rota, e você reaproveita a mesma cena para todos os moradores da vila.

Essa patrulha em linha reta assume que não há obstáculos entre os pontos. Se a sua vila tem cercas e casas no caminho, o próximo passo natural é trocar o movimento direto por pathfinding com NavigationAgent2D, mantendo exatamente a mesma máquina de estados: só muda o cálculo da direção dentro do _processar_walk.

Próximo nível
Quer aprender isso na prática?

No CursoGame.Dev você sai dos tutoriais soltos e constrói jogos publicáveis, com trilha progressiva, quests práticas e feedback real.

Conhecer a plataforma
+500 alunos4.9/5Garantia 7 dias

Detectando o jogador com Area2D

O balãozinho de "!" é um truque barato com efeito desproporcional: ele comunica "você pode falar comigo" sem nenhum texto de tutorial. A implementação usa os sinais da AreaInteracao, que já conectamos no _ready.

func _ao_jogador_entrar(corpo: Node2D) -> void:
    if corpo.is_in_group("player"):
        jogador_perto = corpo as CharacterBody2D
        balao.show()


func _ao_jogador_sair(corpo: Node2D) -> void:
    if corpo == jogador_perto:
        jogador_perto = null
        balao.hide()
        if estado == Estado.TALK:
            _encerrar_dialogo()

Para isso funcionar, o nó do jogador precisa estar num grupo chamado player: selecione ele, abra a aba Nó, seção Grupos, e adicione. O teste is_in_group("player") evita que o balão apareça quando um gato ou outro NPC cruza a área. O cast corpo as CharacterBody2D mantém a tipagem coerente com a variável.

O _ao_jogador_sair cobre um caso que muita gente esquece: se o jogador sair andando no meio da conversa, o diálogo fecha sozinho. Sem esse tratamento, o NPC fica travado em TALK para sempre, com a caixa de texto aberta e ninguém por perto para lê-la.

Com o jogador dentro da área, falta reagir à tecla E. A checagem fica no _process, usando Input.is_action_just_pressed para disparar uma única vez por aperto:

func _process(_delta: float) -> void:
    if jogador_perto == null:
        return
    if Input.is_action_just_pressed("interact"):
        if estado == Estado.TALK:
            _avancar_fala()
        else:
            _iniciar_dialogo()


func _iniciar_dialogo() -> void:
    estado = Estado.TALK
    indice_fala = 0
    balao.hide()
    sprite.flip_h = jogador_perto.global_position.x < global_position.x
    texto_dialogo.text = falas[indice_fala]
    caixa_dialogo.show()


func _avancar_fala() -> void:
    indice_fala += 1
    if indice_fala >= falas.size():
        _encerrar_dialogo()
        return
    texto_dialogo.text = falas[indice_fala]


func _encerrar_dialogo() -> void:
    caixa_dialogo.hide()
    estado = Estado.IDLE
    tempo_espera = tempo_parado
    if jogador_perto != null:
        balao.show()

A mesma tecla tem dois papéis, decididos pelo estado atual: fora do diálogo ela inicia a conversa, dentro dele ela avança para a próxima fala. É o tipo de decisão que a máquina de estados torna trivial e que sem ela viraria um emaranhado de flags.

A linha do flip_h no _iniciar_dialogo é o detalhe que dá vida à cena: o NPC vira o rosto para o jogador antes de falar. A comparação é direta, se a posição x do jogador é menor que a do NPC, o jogador está à esquerda e o sprite espelha. Ao encerrar, o NPC volta para IDLE com o cronômetro cheio, então ele espera um pouco antes de retomar a patrulha, em vez de sair andando no segundo em que a caixa fecha. E se o jogador ainda está por perto, o balão reaparece, sinalizando que dá para conversar de novo.

Teste o fluxo completo: o NPC anda entre os pontos, para, anda de novo; você se aproxima e o "!" surge; aperta E e ele congela, vira para você e mostra a primeira fala; cada E avança uma fala; na última, a caixa fecha e a rotina recomeça.

Como escalar esse NPC

O que você montou é a espinha dorsal de qualquer NPC de RPG, e cada pedaço tem um caminho claro de evolução.

O diálogo em Array[String] funciona para falas lineares, mas encosta no teto rápido: sem escolhas, sem ramificações, sem condições do tipo "só fala isso depois da missão X". Quando chegar essa hora, migre as falas para um recurso separado seguindo o guia de sistema de diálogo para jogos, que cobre a arquitetura de dados por trás de conversas ramificadas. E se quiser que o texto apareça letra por letra, como nos clássicos, o efeito typewriter no diálogo se encaixa direto no TextoDialogo deste tutorial, sem mudar nada na máquina de estados.

A própria FSM cresce pelo mesmo padrão: cada comportamento novo é um valor no enum e um ramo no match. Um NPC comerciante ganha um estado SHOP, um guarda ganha CHASE com detecção por uma segunda Area2D maior, um aldeão medroso ganha FLEE que inverte a direção do movimento. O estado TALK continua intocado, porque congelar e conversar é igual para todos.

Um último conselho de escopo: resista à tentação de dar cronograma completo de vida ao NPC logo de cara, com horário de almoço e rota noturna. Três waypoints, um balão e meia dúzia de falas já produzem a sensação de vila viva que o jogador percebe. Complexidade a mais nessa camada raramente é notada, e o tempo economizado rende muito mais investido no que o NPC diz do que em como ele anda.

Perguntas frequentes

Qual nó usar para criar um NPC no Godot 4?

Para um NPC que anda pelo cenário e colide com paredes, use CharacterBody2D como raiz, com Sprite2D e CollisionShape2D como filhos. Se o NPC fica parado para sempre, um StaticBody2D ou até um simples Node2D resolve. A Area2D entra como filha para detectar quando o jogador chega perto.

Como fazer o NPC detectar que o jogador está perto?

Adicione uma Area2D como filha do NPC com um CollisionShape2D maior que o corpo dele. Conecte os sinais body_entered e body_exited a funções do script e verifique se o corpo que entrou pertence ao grupo do jogador. Guardar a referência do jogador numa variável permite virar o sprite e iniciar o diálogo depois.

Preciso de NavigationAgent2D para o NPC patrulhar?

Não para uma patrulha simples. Se o caminho entre os waypoints é livre de obstáculos, mover com direction_to e velocity basta e é muito mais barato. O NavigationAgent2D só se torna necessário quando o NPC precisa desviar de paredes e móveis para chegar ao destino.

Como impedir que o NPC continue andando durante o diálogo?

Use uma máquina de estados com um estado TALK. Quando o diálogo abre, o script troca o estado para TALK e o match dentro de _physics_process zera a velocity nesse estado, ignorando a lógica de patrulha. Ao fechar o diálogo, o estado volta para IDLE e a rotina continua de onde parou.

Como fazer o NPC olhar para o jogador durante a conversa?

Compare a posição global do jogador com a do NPC no eixo x. Se o jogador está à esquerda, ative o flip_h do Sprite2D; se está à direita, desative. Uma linha resolve no momento em que o diálogo começa, e o efeito na sensação de vida do NPC é enorme.