Godot do Zero: Inimigos simples com patrulha e detecção do jogador

Capítulo 9

Tempo estimado de leitura: 9 minutos

+ Exercício

Objetivo do inimigo: patrulhar, detectar e reagir

Neste capítulo, você vai criar um inimigo 2D simples, mas completo o suficiente para uso real: ele patrulha entre dois pontos, vira a direção corretamente, respeita colisões com paredes/limites e detecta o jogador para reagir (perseguir por alguns segundos ou causar dano ao encostar). A ideia é separar bem: movimento (patrulha/perseguição), detecção (visão) e parâmetros (variáveis exportadas), para instanciar vários inimigos sem duplicar lógica.

Estrutura da cena do inimigo

Nós recomendados

Crie uma cena Enemy.tscn com a seguinte estrutura (nomes sugeridos):

  • Enemy (CharacterBody2D)
  • Sprite (Sprite2D ou AnimatedSprite2D, se já tiver animações prontas)
  • CollisionShape2D (colisor do corpo)
  • Vision (Area2D)
    • VisionShape (CollisionShape2D) — formato do “cone”/área de visão
  • AttackHitbox (Area2D)
    • AttackShape (CollisionShape2D) — área pequena ao redor do inimigo para contato/ataque
  • WallRay (RayCast2D) — opcional, para virar ao detectar parede à frente
  • FloorRay (RayCast2D) — opcional, para virar ao detectar “fim do chão” (plataforma)
  • ChaseTimer (Timer) — controla por quanto tempo ele persegue após ver o jogador

Você pode usar Vision para detectar o jogador e os Rays para melhorar a patrulha (virar antes de bater). Se seu jogo é top-down, o FloorRay pode ser dispensável.

Configuração rápida das Areas

  • Vision (Area2D): use um CollisionShape2D grande (círculo/retângulo) para representar alcance de visão. Para um “cone”, você pode usar um polígono (CollisionPolygon2D) se preferir.
  • AttackHitbox (Area2D): shape pequeno, próximo ao corpo, para detectar contato com o jogador.

Importante: o jogador precisa estar em um grupo (por exemplo player) para o inimigo identificá-lo sem acoplamento. No nó do jogador, em Groups, adicione player.

Parâmetros do inimigo com variáveis exportadas

Variáveis exportadas permitem ajustar cada instância no editor (velocidade, alcance, tempo de perseguição, dano etc.) sem criar scripts diferentes.

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

Baixar o aplicativo

extends CharacterBody2D

enum State { PATROL, CHASE }

@export var patrol_speed: float = 60.0
@export var chase_speed: float = 110.0
@export var gravity: float = 900.0

@export var patrol_distance: float = 160.0 # distância entre ponto A e B
@export var start_direction: int = 1 # 1 direita, -1 esquerda

@export var chase_duration: float = 2.0
@export var contact_damage: int = 1

@export var use_wall_ray: bool = true
@export var use_floor_ray: bool = false

var state: State = State.PATROL
var dir: int = 1
var start_x: float
var target: Node2D = null

@onready var vision: Area2D = $Vision
@onready var attack_hitbox: Area2D = $AttackHitbox
@onready var wall_ray: RayCast2D = $WallRay
@onready var floor_ray: RayCast2D = $FloorRay
@onready var chase_timer: Timer = $ChaseTimer
@onready var sprite: Node = $Sprite

Por que isso é útil? Você cria um único Enemy.tscn e, ao instanciar na fase, ajusta patrol_distance, patrol_speed e chase_duration para variar comportamento sem duplicar nada.

Patrulha entre dois pontos (A e B)

Definindo os limites da patrulha

Uma forma simples é guardar a posição inicial e patrulhar em torno dela. Assim, você não precisa colocar dois marcadores na cena (embora isso também funcione).

func _ready() -> void:
	start_x = global_position.x
	dir = signi(start_direction)
	chase_timer.wait_time = chase_duration
	chase_timer.one_shot = true

	vision.body_entered.connect(_on_vision_body_entered)
	vision.body_exited.connect(_on_vision_body_exited)
	attack_hitbox.body_entered.connect(_on_attack_body_entered)
	chase_timer.timeout.connect(_on_chase_timeout)

	_update_facing()

Agora, no estado de patrulha, o inimigo anda e inverte ao atingir o limite:

func _physics_process(delta: float) -> void:
	_apply_gravity(delta)

	match state:
		State.PATROL:
			_patrol(delta)
		State.CHASE:
			_chase(delta)

	move_and_slide()
	_post_move_turn_checks()
func _patrol(delta: float) -> void:
	velocity.x = dir * patrol_speed

	var left_limit := start_x - patrol_distance * 0.5
	var right_limit := start_x + patrol_distance * 0.5

	if global_position.x <= left_limit:
		dir = 1
		_update_facing()
	elif global_position.x >= right_limit:
		dir = -1
		_update_facing()

Virar ao encostar em paredes/limites

Mesmo com limites A/B, é comum o inimigo bater numa parede antes de chegar ao limite. Você pode virar de duas maneiras:

  • Após colisão: detectar se bateu em parede durante o movimento.
  • Antes da colisão: usar RayCast2D apontando para frente.

Abordagem prática: use RayCast quando disponível e, como fallback, vire se estiver colidindo com parede.

func _post_move_turn_checks() -> void:
	if state != State.PATROL:
		return

	# 1) Ray de parede (vira antes de bater)
	if use_wall_ray and wall_ray.is_colliding():
		_turn_around()
		return

	# 2) Se encostou numa parede durante o move
	if is_on_wall():
		_turn_around()
		return

	# 3) Ray de "fim do chão" (para plataformas)
	if use_floor_ray and not floor_ray.is_colliding():
		_turn_around()
func _turn_around() -> void:
	dir *= -1
	_update_facing()

Configuração dos Rays:

  • WallRay: posicione à frente do inimigo e aponte no eixo X (comprimento pequeno). Marque Enabled.
  • FloorRay: posicione na frente e um pouco abaixo, apontando para baixo. Se não colidir, significa que não há chão adiante.

Detecção do jogador com Area2D (visão)

Detectar entrada/saída da área de visão

Com Vision (Area2D), você detecta quando o jogador entra no alcance. Para evitar acoplamento, verifique se o corpo está no grupo player.

func _on_vision_body_entered(body: Node) -> void:
	if not body.is_in_group("player"):
		return
	if body is Node2D:
		target = body
		_start_chase()
func _on_vision_body_exited(body: Node) -> void:
	if body == target:
		# Saiu da visão, mas ainda pode perseguir por alguns segundos
		chase_timer.start()

A lógica aqui é: ao ver o jogador, entra em perseguição. Ao perder de vista, inicia um timer; quando o timer acabar, volta a patrulhar.

func _start_chase() -> void:
	state = State.CHASE
	chase_timer.stop()
func _on_chase_timeout() -> void:
	state = State.PATROL
	target = null

Perseguir por alguns segundos

Na perseguição, o inimigo tenta se mover em direção ao jogador no eixo X (para side-scroller). Se for top-down, você pode usar direção em 2D (x e y).

func _chase(delta: float) -> void:
	if target == null or not is_instance_valid(target):
		state = State.PATROL
		return

	var dx := target.global_position.x - global_position.x
	dir = signi(dx) if dx != 0.0 else dir
	_update_facing()

	velocity.x = dir * chase_speed

Dica: se você quer que ele só persiga enquanto “vê” o jogador, não use timer; basta voltar para patrulha no body_exited. O timer cria um comportamento mais “humano”: ele continua procurando por um curto período.

Detecção com RayCast2D (linha de visão) como alternativa

Area2D detecta alcance, mas não considera obstáculos. Para uma visão mais realista, combine: Area2D para alcance + RayCast2D para checar se há parede entre inimigo e jogador.

Exemplo: ao detectar o jogador na Area, faça um ray do inimigo até o jogador e só inicie perseguição se o ray acertar o jogador.

@onready var los_ray: RayCast2D = $LineOfSightRay # crie um RayCast2D extra

func _can_see_target(t: Node2D) -> bool:
	los_ray.target_position = to_local(t.global_position)
	los_ray.force_raycast_update()
	if not los_ray.is_colliding():
		return false
	return los_ray.get_collider() == t
func _on_vision_body_entered(body: Node) -> void:
	if not body.is_in_group("player"):
		return
	if body is Node2D and _can_see_target(body):
		target = body
		_start_chase()

Assim, o inimigo só “enxerga” se não houver obstáculo no caminho.

Ataque ao encostar (hitbox de contato)

Para uma reação simples, use AttackHitbox (Area2D) para detectar contato e aplicar dano. O ideal é o jogador ter um método como take_damage(amount) ou receber um sinal. Aqui vai um exemplo direto chamando método, com verificação de existência:

func _on_attack_body_entered(body: Node) -> void:
	if not body.is_in_group("player"):
		return
	if body.has_method("take_damage"):
		body.call("take_damage", contact_damage)

Se você quiser que o inimigo pare para “atacar” por um instante, pode adicionar um pequeno cooldown com outro Timer e travar o movimento durante esse período.

Funções utilitárias: gravidade e orientação visual

func _apply_gravity(delta: float) -> void:
	if not is_on_floor():
		velocity.y += gravity * delta
	else:
		velocity.y = 0.0
func _update_facing() -> void:
	# Se estiver usando Sprite2D/AnimatedSprite2D, normalmente basta flipar no X
	if sprite is Sprite2D:
		(sprite as Sprite2D).flip_h = dir < 0
	elif sprite is AnimatedSprite2D:
		(sprite as AnimatedSprite2D).flip_h = dir < 0

	# Atualize Rays para apontarem para frente
	if use_wall_ray:
		wall_ray.target_position.x = abs(wall_ray.target_position.x) * dir
	if use_floor_ray:
		floor_ray.position.x = abs(floor_ray.position.x) * dir

Essa atualização garante que os Rays “acompanhem” a direção do inimigo.

Instanciando múltiplos inimigos sem duplicar lógica

1) Uma cena reutilizável + ajustes por instância

Com Enemy.tscn pronta, basta arrastar a cena para dentro da fase quantas vezes quiser. Para cada instância, ajuste no Inspector:

  • patrol_distance para controlar o trecho de patrulha
  • patrol_speed e chase_speed para variar dificuldade
  • chase_duration para “persistência” ao perder o jogador
  • use_floor_ray para inimigos de plataformas

2) Variantes com cenas herdadas (sem duplicar script)

Se você quer inimigos visualmente diferentes (sprites/colisores diferentes), mas com o mesmo comportamento:

  • Crie uma nova cena
  • Use New Inherited Scene a partir de Enemy.tscn
  • Troque apenas o Sprite, shapes e valores exportados

O script permanece o mesmo, e você ganha “tipos” de inimigo sem reescrever lógica.

3) Spawner simples (opcional) para popular a fase

Para instanciar inimigos via código (por exemplo, em um nó EnemySpawner), use um PackedScene exportado:

extends Node2D

@export var enemy_scene: PackedScene
@export var positions: Array[Vector2] = []

func _ready() -> void:
	for p in positions:
		var e := enemy_scene.instantiate()
		get_parent().add_child(e)
		e.global_position = p

Assim, você controla pontos de spawn e mantém o inimigo como uma cena reutilizável.

Checklist de depuração rápida

  • O jogador está no grupo player?
  • Vision e AttackHitbox têm shapes configurados e Monitoring ativo?
  • Os Rays estão com Enabled marcado e apontando para a direção correta?
  • O inimigo está em camadas/máscaras que permitem detectar o jogador nas Areas?
  • O ChaseTimer está como One Shot e com wait_time configurado?

Agora responda o exercício sobre o conteúdo:

Para reduzir acoplamento e permitir que vários inimigos identifiquem o jogador corretamente, qual prática é a mais adequada ao configurar a detecção?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

Usar grupos permite que o inimigo reconheça o jogador sem depender de nomes ou referências diretas. Assim, a detecção em Vision/AttackHitbox verifica is_in_group("player") e mantém o comportamento reutilizável.

Próximo capitúlo

Godot do Zero: Sistema de dano, vida e invencibilidade temporária

Arrow Right Icon
Capa do Ebook gratuito Godot do Zero: Criando seu Primeiro Jogo 2D com GDScript
53%

Godot do Zero: Criando seu Primeiro Jogo 2D com GDScript

Novo curso

17 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.