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 à frenteFloorRay(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
CollisionShape2Dgrande (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.
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
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 = $SpritePor 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
RayCast2Dapontando 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 = nullPerseguir 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_speedDica: 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() == tfunc _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.0func _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) * dirEssa 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_distancepara controlar o trecho de patrulhapatrol_speedechase_speedpara variar dificuldadechase_durationpara “persistência” ao perder o jogadoruse_floor_raypara 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 = pAssim, 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? VisioneAttackHitboxtê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
ChaseTimerestá como One Shot e comwait_timeconfigurado?