Enemigos y Señales: ¡Contacto!
Ya disparamos, pero nuestras balas viajan hacia el infinito sin golpear nada. Es hora de crear dianas voladoras. Pero antes, una decisión arquitectónica vital.
1. El Nodo Area2D
Para el jugador usamos CharacterBody2D porque queríamos física de movimiento precisa (chocar con paredes y deslizarse).
Para las balas y enemigos simples, usaremos Area2D.
¿Por qué usamos Area2D?
- Es más barato: Consume menos CPU que un cuerpo físico completo.
- Permite atravesar: En un Space Shooter, no quieres que los enemigos reboten, sino que detecten que te han tocado y exploten.
Area2Des perfecta para detectar solapamientos (“overlap”) sin empujar. - Superpoderes Físicos: Hereda de
CollisionObject2D, lo que lo hace visible para el motor de física (a diferencia de unNode2Dnormal que sería invisible aunque tuviera forma).
Preparando la Escena Enemiga
Puedes usar tu propio sprite o descargar este:

Clic derecho -> Guardar imagen.
Créditos: Set of military aircraft por brgfx en Freepik (Licencia gratuita con atribución).
- Crea nueva Escena. Nodo raíz:
Area2D. Nombre:EnemyPlane. - Añade
Sprite2D(tu nave enemiga). - Añade
CollisionShape2D(círculo o rectángulo que cubra la nave). - Adjunta un Script en Area2D: Guárdalo como
enemy_plane.gd.
¡No olvides la forma!
Un Area2D sin CollisionShape2D no sirve para nada. Godot te mostrará una alerta ⚠️ amarilla si se te olvida ponerlo.
2. Preparando el Script
Antes de escribir el código, analicemos qué necesita hacer nuestro enemigo. Vamos a mezclar tres conceptos:
A. Señales por Código
En el capítulo anterior usamos el editor para conectar señales. Hoy lo haremos por Código en _ready() usando connect().
Esto permite que cada enemigo sea autónomo y sepa reaccionar sin necesidad de configurarlo manualmente en el editor cada vez que instanciamos uno.
B. Físicas Simuladas
Como usamos Area2D y no un cuerpo físico (RigidBody), la gravedad no nos afecta.
Si queremos que el enemigo caiga dramáticamente al morir, tendremos que programar esa caída manualmente modificando su posición. Es un truco visual muy común y barato.
C. Estado
Necesitaremos saber si el enemigo está vivo (moviéndose recto) o muriendo (cayendo). Usaremos una variable (“bandera”) para controlar este estado.
3. Programando al Enemigo
Vamos a construir el script pieza a pieza.
Fase 1: Movimiento Básico
Primero, hagamos que se mueva a la izquierda. Nada nuevo aquí.
extends Area2D
# Exportamos la velocidad para que la podamos ajustar en el editor
@export var speed = 150
# Ignoramos por ahora _ready()
func _ready() -> void:
pass
# Añadimos el movimiento en _process
func _process(delta) -> void:
position.x -= speed * deltaextends Area2D
# Exportamos la velocidad para que la podamos ajustar en el editor
@export var speed = 150
# Ignoramos por ahora _ready()
func _ready() -> void:
pass
# Añadimos el movimiento en _process
func _process(delta) -> void:
position.x -= speed * deltaProfundizando en @export:
En Part 1 vimos que @export hace que la variable aparezca en el Inspector. Pero hay más:
- Balanceo sin recompilar: Puedes cambiar
speeden el Inspector mientras el juego está PAUSADO (F7) y ver el efecto inmediatamente. Esto es invaluable para ajustar la dificultad. - Valores por instancia: Si tienes 3 enemigos en la escena, cada uno puede tener un
speeddiferente SIN modificar el script. Simple: selecciona uno y cambia su valor en el Inspector. - Documentación implícita: El nombre de la variable aparece en el editor, haciendo que otros (o tú del futuro) entiendan qué se puede ajustar.
Fase 2: Conexión de Señales
Ahora añadimos la conexión. Recuerda: connect(quien_responde).
func _ready():
# area_entered es una señal de Area2D, con .connect haremos que llame a una
# función que definiremos ahora más abajo
area_entered.connect(_on_area_entered)
# Esta función se ejecutará cuando algo toque este el CollisionShape2D de este Area2D
# gracias al comportamiento de area_entered
func _on_area_entered(area):
print("¡Me han dado!") # <-- Esto es solo temporalfunc _ready():
# area_entered es una señal de Area2D, con .connect haremos que llame a una
# función que definiremos ahora más abajo
area_entered.connect(_on_area_entered)
# Esta función se ejecutará cuando algo toque este el CollisionShape2D de este Area2D
# gracias al comportamiento de area_entered
func _on_area_entered(area):
print("¡Me han dado!") # <-- Esto es solo temporal¿De dónde sale el parámetro area?
La señal area_entered no solo avisa de la colisión, también pasa información: el otro Area2D que ha colisionado contigo. En nuestro caso, ese “otro” es la bala disparada por el jugador.
Por eso, dentro de _on_area_entered(area), la variable area contiene una referencia directa al nodo bala. Esto nos permite hacer cosas como area.queue_free() para destruirla.
Fase 3: Vida y Muerte
Añadimos vida (hp) y la lógica de recibir daño.
@export var speed = 150
@export var hp = 3 # Añadimos la variable hp para controlar la vida y la exponemos
# en el editor por comodidad
# Código intermedio ---
func _on_area_entered(area):
# El parámetro 'area' es el OTRO Area2D que ha colisionado, la bala
hp -= 1
# area.queue_free() # Si lo descomentas, la bala se destruirá (no atravesará)
# Un condicional 'if' básico, si hp es menor o igual a 0, llama a die()
if hp <= 0:
die()
func die():
# Como ya explicamos, queue_free() eliminará al Area2D
# (lo mete en una cola para eliminarlo )
queue_free()@export var speed = 150
@export var hp = 3 # Añadimos la variable hp para controlar la vida y la exponemos
# en el editor por comodidad
# Código intermedio ---
func _on_area_entered(area):
# El parámetro 'area' es el OTRO Area2D que ha colisionado, la bala
hp -= 1
# area.queue_free() # Si lo descomentas, la bala se destruirá (no atravesará)
# Un condicional 'if' básico, si hp es menor o igual a 0, llama a die()
if hp <= 0:
die()
func die():
# Como ya explicamos, queue_free() eliminará al Area2D
# (lo mete en una cola para eliminarlo )
queue_free()Fase 4: Muerte Dramática (Físicas Fake)
Como usamos Area2D y no RigidBody2D, no tenemos física real. Vamos a “fingir” que el enemigo cae al morir.
Necesitamos una variable is_dying para saber si está en “modo caída”.
# Definimos variables un par de variables de estado del enemigo
var is_dying = false # booleano, si está muerto o no, por defecto no está muerto (false)
var fall_speed = 0.0 # número, controla la velocidad de caída, por defecto 0
func _process(delta):
if is_dying:
# Caída con gravedad simulada, si muere le añadimos velocidad
fall_speed += 500 * delta
# Ahora, teniendo la velocidad le decimos hacia dónde moverse
position.y += fall_speed * delta # Le añadimos velocidad vertical
position.x -= speed * 0.5 * delta # Le restamos velocidad horizontal (frenada)
else:
position.x -= speed * delta # Movimiento normal que ya teníamos# Definimos variables un par de variables de estado del enemigo
var is_dying = false # booleano, si está muerto o no, por defecto no está muerto (false)
var fall_speed = 0.0 # número, controla la velocidad de caída, por defecto 0
func _process(delta):
if is_dying:
# Caída con gravedad simulada, si muere le añadimos velocidad
fall_speed += 500 * delta
# Ahora, teniendo la velocidad le decimos hacia dónde moverse
position.y += fall_speed * delta # Le añadimos velocidad vertical
position.x -= speed * 0.5 * delta # Le restamos velocidad horizontal (frenada)
else:
position.x -= speed * delta # Movimiento normal que ya teníamos¿Qué es ese else?
Hasta ahora hemos usado if (“Si pasa esto…”). El else significa “Si NO pasa eso…”.
Es como un interruptor de dos vías:
- IF (
is_dyinges true): Ejecuta la física de caída. - ELSE (si no, en el otro caso): Ejecuta el movimiento normal hacia la izquierda.
Esto nos garantiza que el enemigo nunca hará las dos cosas a la vez. O está se está acercando al player, o está muriendo.
Fase 5: Eliminación de la Instancia
Cuando hp llega a 0, llamamos a die(). Esta función:
- Activa el estado
is_dyingpara que_processejecute la caída. - Desactiva las colisiones para ignorar impactos durante la animación.
- Usa un timer para destruir el nodo después de 1 segundo.
func die():
is_dying = true
# set_deferred evita errores si se llama durante una colisión
$CollisionShape2D.set_deferred("disabled", true)
# Espera 1 segundo y destruye
await get_tree().create_timer(1.0).timeout
queue_free()func die():
is_dying = true
# set_deferred evita errores si se llama durante una colisión
$CollisionShape2D.set_deferred("disabled", true)
# Espera 1 segundo y destruye
await get_tree().create_timer(1.0).timeout
queue_free()Fase 6: Evitando Bugs (La Cláusula de Guardia)
Hay un detalle sutil pero peligroso. ¿Qué pasa si le metemos 20 balazos al enemigo mientras está cayendo?
Que llamaríamos a die() 20 veces.
Para evitar esto mejoramos _on_area_entered con dos reglas de oro:
- Si ya estoy muriendo, ignora las balas: Usamos
if is_dying: returnpara salir de la función inmediatamente. - Si me tocas, desapareces: La bala (
area) debe destruirse siempre tras impactar.
func _on_area_entered(area):
# 1. Cláusula de Guardia: Si ya estoy muerto, no molestes
if is_dying:
return
hp -= 1
# 2. La bala impacta y desaparece (siempre)
area.queue_free()
if hp <= 0:
die()func _on_area_entered(area):
# 1. Cláusula de Guardia: Si ya estoy muerto, no molestes
if is_dying:
return
hp -= 1
# 2. La bala impacta y desaparece (siempre)
area.queue_free()
if hp <= 0:
die()5. Script Completo
Ahora junta todas las piezas. Así queda tu archivo enemy_plane.gd final:
extends Area2D
@export var speed = 150
@export var hp = 3
var is_dying = false
var fall_speed = 0.0
func _ready():
area_entered.connect(_on_area_entered)
func _process(delta):
if is_dying:
fall_speed += 500 * delta
position.y += fall_speed * delta
position.x -= speed * 0.5 * delta
else:
position.x -= speed * delta
func _on_area_entered(area):
if is_dying:
return
hp -= 1
area.queue_free()
if hp <= 0:
die()
func die():
is_dying = true
$CollisionShape2D.set_deferred("disabled", true)
await get_tree().create_timer(1.0).timeout
queue_free()extends Area2D
@export var speed = 150
@export var hp = 3
var is_dying = false
var fall_speed = 0.0
func _ready():
area_entered.connect(_on_area_entered)
func _process(delta):
if is_dying:
fall_speed += 500 * delta
position.y += fall_speed * delta
position.x -= speed * 0.5 * delta
else:
position.x -= speed * delta
func _on_area_entered(area):
if is_dying:
return
hp -= 1
area.queue_free()
if hp <= 0:
die()
func die():
is_dying = true
$CollisionShape2D.set_deferred("disabled", true)
await get_tree().create_timer(1.0).timeout
queue_free()¡Hecho! Has creado un enemigo autónomo que gestiona su propio movimiento, sus colisiones y su muerte, todo encapsulado en un script robusto.
Probando en tu Mundo
Antes de pasar al siguiente capítulo, verifica que todo funciona:
- Abre
level.tscn(tu escena principal). - Arrastra
enemy_plane.tscndesde el panel FileSystem al viewport. - Posiciona el enemigo a la derecha de la pantalla (para que vuele hacia el jugador).
- Ejecuta el juego (F5).
- Dispara al enemigo y verifica:
- Las balas desaparecen al impactar.
- El enemigo pierde HP y eventualmente cae girando.
- Tras 1 segundo de caída, desaparece.
Si todo funciona, ¡felicidades! Tu sistema de combate está operativo.
Repasemos lo aprendido
Area2D: Nodo para detectar solapamientos sin física de empuje.- Señales por código: Usar
signal.connect(función)en_ready()para encapsular la lógica. - Estado (
is_dying): Usar variables “bandera” para controlar el comportamiento. - Físicas simuladas: Modificar
positionmanualmente para crear efectos de caída. set_deferred(): Desactivar colisiones de forma segura durante callbacks de física.await+ Timer: Esperar un tiempo antes de ejecutar código (ej: destruir tras 1 segundo).
En el próximo capítulo, aprenderemos a dar feedback visual con Shaders (el famoso “Hit Flash”).