Voltar para o Blog
Quest Log

IA de Inimigo em Jogos: Perseguir, Patrulhar e Atacar no Godot 4

Inimigo robótico patrulhando um corredor com cone de visão enquanto o jogador se esconde

Aprenda a programar IA de inimigo para jogo no Godot 4: máquina de estados, patrulha por waypoints, detecção do player com raycast, perseguição e ataque.

Ferramenta:Godot

IA de Inimigo em Jogos: Perseguir, Patrulhar e Atacar no Godot 4

Uma boa IA de inimigo em um jogo não precisa ser inteligente de verdade. Ela precisa ser legível: o jogador tem que entender o que o inimigo está fazendo, prever o que ele vai fazer e sentir que foi culpa dele quando for pego. O guarda de Metal Gear que anda em rotina fixa e grita quando te vê é mais divertido que qualquer rede neural, porque o jogador consegue ler e planejar em volta dele.

A boa notícia: esse tipo de inimigo cabe em três estados (patrulhar, perseguir, atacar) e um punhado de regras de transição entre eles. É o padrão máquina de estados finitos (FSM), e é a base de praticamente todo inimigo de ação que você já enfrentou, e também o ponto de partida de boa parte da IA aplicada a jogos na Godot.

Nesse tutorial eu monto um inimigo completo no Godot 4, em GDScript, visão top-down pra manter o foco na IA e não na gravidade. A mesma estrutura funciona em plataforma e em 3D, só muda o código de movimento. Todo código roda como está.

A máquina de estados: o esqueleto da IA de inimigo

Antes de qualquer detecção ou movimento, vem a estrutura. Uma máquina de estados é só isso: o inimigo está em exatamente um estado por vez, cada estado tem seu comportamento, e existem regras claras pra trocar de um pro outro.

O erro clássico de iniciante é não usar estado nenhum: vai empilhando if dentro de _physics_process até ninguém mais saber por que o inimigo treme entre andar e atacar. Com estados, cada comportamento vive isolado e as transições ficam explícitas.

A estrutura de nodes do inimigo:

Inimigo (CharacterBody2D)
├── CollisionShape2D
├── Sprite2D
├── RayCast2D            # linha de visão
└── AreaDeVisao (Area2D) # raio de percepção
    └── CollisionShape2D # um círculo grande

E o esqueleto do script:

extends CharacterBody2D

enum Estado { PATRULHAR, PERSEGUIR, ATACAR }

var estado: Estado = Estado.PATRULHAR
var player: Node2D = null

func _physics_process(delta: float) -> void:
    match estado:
        Estado.PATRULHAR:
            patrulhar(delta)
        Estado.PERSEGUIR:
            perseguir(delta)
        Estado.ATACAR:
            atacar(delta)

    move_and_slide()

func mudar_estado(novo: Estado) -> void:
    if novo == estado:
        return
    estado = novo
    # Ponto único de troca: ótimo lugar pra disparar
    # animação, som de alerta, exclamação na cabeça etc.

O match despacha pro comportamento do estado atual, e o mudar_estado() centraliza toda transição. Esse funil único parece burocracia agora, mas é ele que deixa você plugar feedback depois (o "!" em cima da cabeça do guarda) sem caçar transição espalhada pelo código.

Pra três estados, esse enum com match é o tamanho certo de solução. Existe um padrão mais robusto, com um node por estado, que vale a pena quando o inimigo passa de uns cinco ou seis comportamentos. Pra começar, isso aqui resolve e é fácil de debugar.

Patrulha: o inimigo precisa parecer vivo

Inimigo parado esperando o player é alvo de tiro. Patrulha é o que vende a ilusão de que o mundo existe sem o jogador, e a implementação mais útil é a mais simples: uma lista de pontos que o inimigo visita em loop.

No editor, crie nodes Marker2D espalhados pela fase (filhos da fase, não do inimigo) e arraste todos pro array exportado:

@export var pontos_patrulha: Array[Marker2D] = []
@export var velocidade_patrulha := 60.0
@export var tempo_de_espera := 1.5

var indice_patrulha := 0
var espera := 0.0

func patrulhar(delta: float) -> void:
    if pontos_patrulha.is_empty():
        velocity = Vector2.ZERO
        return

    # Pausa no waypoint antes de seguir pro próximo.
    if espera > 0.0:
        espera -= delta
        velocity = Vector2.ZERO
        return

    var alvo = pontos_patrulha[indice_patrulha].global_position

    if global_position.distance_to(alvo) < 8.0:
        indice_patrulha = (indice_patrulha + 1) % pontos_patrulha.size()
        espera = tempo_de_espera
        return

    velocity = global_position.direction_to(alvo) * velocidade_patrulha

Dois detalhes que fazem diferença real aqui:

A tolerância de chegada. O < 8.0 existe porque o inimigo nunca vai parar exatamente em cima do ponto. Sem tolerância, ele orbita o waypoint pra sempre, vibrando. Se o seu inimigo for rápido, aumente o valor.

A espera no waypoint. O tempo_de_espera é a diferença entre um robô de esteira e um guarda. Parar um segundo e meio em cada ponto cria a janela que o jogador usa pra passar, e stealth inteiro é desenhado em cima dessas janelas.

A patrulha com direction_to anda em linha reta, então funciona em área aberta. Se a sua fase tem labirinto de paredes, troque o movimento por NavigationAgent2D, que calcula o caminho pelo navmesh. A máquina de estados não muda nada, só a parte que gera o velocity. Esse mesmo movimento por waypoints, sem detecção nem perseguição, é o que faz os inimigos de um jogo tower defense seguirem um caminho fixo até a base.

Detecção: o player entrou no campo de visão?

Detecção boa tem duas camadas, e pular a segunda é o bug mais comum em IA de inimigo: o guarda que te enxerga através da parede.

Camada 1: alcance. A AreaDeVisao (Area2D com um círculo grande) responde "o player está perto o suficiente?". Conecte os sinais body_entered e body_exited dela no script do inimigo:

func _on_area_de_visao_body_entered(body) -> void:
    if body.is_in_group("player"):
        player = body

func _on_area_de_visao_body_exited(body) -> void:
    if body == player:
        player = null

Coloque o player no grupo player (painel Node > Groups) e confira a collision mask da Area2D pra detectar a camada dele. Guardar a referência em player evita ficar fazendo busca na árvore todo frame.

Camada 2: linha de visão. Estar no alcance não significa estar visível. O RayCast2D responde a pergunta que falta: tem parede no caminho?

@onready var raycast: RayCast2D = $RayCast2D

func enxerga_player() -> bool:
    if player == null:
        return false

    raycast.target_position = to_local(player.global_position)
    raycast.force_raycast_update()

    return raycast.get_collider() == player

A lógica: aponto o ray pro player e atualizo na hora com force_raycast_update() (sem isso, o resultado só chega no próximo passo de física). Se a primeira coisa que o ray acerta é o próprio player, a linha de visão está limpa. Se acertou outra coisa, tem obstáculo no meio. A collision mask do RayCast2D precisa incluir a camada das paredes e a do player, senão o ray atravessa parede e o teste mente.

Com as duas camadas prontas, a transição de patrulha pra perseguição é uma linha no fim de patrulhar():

    if enxerga_player():
        mudar_estado(Estado.PERSEGUIR)

Se quiser um cone de visão em vez de círculo (o guarda não tem olho na nuca), adicione um teste de ângulo antes do raycast: compare a direção até o player com a direção que o inimigo está encarando usando dot(), e só prossiga se o resultado passar de um limiar. É uma checagem barata e muda completamente o jogo de stealth.

Curso de Godot
Quer fazer seu primeiro jogo na Godot?

No curso de Godot da CursoGame.Dev você vai do zero ao primeiro jogo, com um desafio prático em cada level e suporte individual no WhatsApp quando travar.

Conhecer o curso de Godot
Suporte no WhatsAppAcesso vitalícioGarantia de 7 dias

Perseguir e atacar: a pressão e o clímax

Perseguição é o estado mais simples dos três no código, e o mais sensível no tuning:

@export var velocidade_perseguicao := 110.0
@export var alcance_de_ataque := 40.0
@export var tempo_de_memoria := 2.0

var memoria := 0.0
var ultima_posicao_vista := Vector2.ZERO

func perseguir(delta: float) -> void:
    if player and enxerga_player():
        memoria = tempo_de_memoria
        ultima_posicao_vista = player.global_position
    else:
        # Perdeu de vista: segue pra última posição conhecida.
        memoria -= delta
        if memoria <= 0.0:
            mudar_estado(Estado.PATRULHAR)
            return

    if player and global_position.distance_to(player.global_position) < alcance_de_ataque:
        mudar_estado(Estado.ATACAR)
        return

    velocity = global_position.direction_to(ultima_posicao_vista) * velocidade_perseguicao

Três decisões de design escondidas nessas linhas:

Velocidade de perseguição maior que a do player ou menor? Se for maior, todo encontro termina em combate. Se for um pouco menor, fugir vira opção real e o jogador escolhe a briga. Não é detalhe técnico, é o tipo de jogo que você está fazendo.

A memória. Sem o tempo_de_memoria, o inimigo esquece o player no exato frame em que ele dobra a esquina, e dá pra "desligar" o guarda piscando atrás de qualquer pilastra. Com memória, ele segue até a última posição vista e só desiste depois. Dois segundos já mudam a sensação de "script burro" pra "ele está me procurando".

Perseguir a última posição vista, não a atual. Repare que o velocity mira ultima_posicao_vista. Enquanto enxerga, ela é atualizada todo frame e o efeito é o mesmo. Quando perde de vista, o inimigo vai até onde o player estava, o que é exatamente o comportamento que parece natural.

O ataque fecha o ciclo. A regra de ouro: o estado de atacar não persegue. Ele para, executa e reavalia:

@export var intervalo_de_ataque := 1.0
@export var dano := 10

var recarga := 0.0

func atacar(delta: float) -> void:
    velocity = Vector2.ZERO
    recarga -= delta

    if player == null or global_position.distance_to(player.global_position) > alcance_de_ataque:
        mudar_estado(Estado.PERSEGUIR)
        return

    if recarga <= 0.0:
        recarga = intervalo_de_ataque
        if player.has_method("receber_dano"):
            player.receber_dano(dano)

O intervalo_de_ataque é o respiro do jogador. Inimigo que causa dano todo frame em contato é frustração, não desafio. Um golpe por segundo dá tempo de reagir, recuar, contra-atacar. E o has_method evita crash se o ataque acertar algo que não tem vida.

Em jogo de verdade, esse receber_dano direto vira uma animação com janela de acerto: o inimigo arma o golpe (telegraph), o jogador tem uma fração de segundo pra esquivar, e só então o dano sai. A estrutura é a mesma, o dano só passa a ser disparado por um sinal da animação em vez de imediato.

Tuning: onde a IA vira game design

Com a máquina rodando, o trabalho muda de programar pra ajustar. Os números exportados são o painel de personalidade do inimigo:

  • Raio de visão grande + memória longa = caçador implacável, bom pra terror
  • Raio curto + espera longa nos waypoints = guarda distraído, bom pra stealth
  • Perseguição mais lenta que o player + ataque forte = tanque que você contorna
  • Perseguição rápida + dano baixo = mosquito que pressiona em grupo

Pra debugar transição, um print dentro de mudar_estado() mostrando o estado novo já revela quase tudo: estado oscilando entre dois valores todo frame indica condição de entrada e saída se sobrepondo (quase sempre é a distância de ataque igual à distância de saída, resolva com uma folga entre as duas). E Debug > Visible Collision Shapes mostra a área de visão e o raycast em tempo real durante o jogo.

Fechando

Um inimigo que patrulha, detecta, persegue e ataca cabe em uma máquina de estados de três valores, dois nodes de percepção e meia dúzia de números exportados. Nada aqui é truque avançado, e é exatamente por isso que esse padrão sobrevive década após década: é fácil de ler, fácil de estender e fácil de ajustar.

Se quiser fixar, monta esse inimigo numa cena vazia e vai testando um estado por vez: primeiro a patrulha sozinha, depois a detecção com um print, depois a perseguição, por último o ataque. Quando estiver rodando, crie variações só mexendo nos exports, sem tocar no código. No dia em que três cópias do mesmo script parecerem inimigos diferentes, você entendeu o que IA de inimigo realmente é: comportamento simples com personalidade nos números.

E se patrulha e detecção forem o que mais te interessou aqui, existe um gênero inteiro construído em cima disso: em como fazer um jogo stealth eu mostro o cone de visão desenhado na tela, o medidor de suspeita e o sistema de ruído.