Objetivo del capítulo
En este capítulo vas a construir una escena de enemigo reutilizable con parámetros exportables (por ejemplo, velocidad y rango), movimiento con colisión y un sistema de interacción con el jugador (daño, retroceso o reinicio). Además, usarás señales para comunicar eventos (por ejemplo, “el jugador fue golpeado”) sin acoplar el enemigo a la implementación del jugador.
Escena base: Enemigo patrullero (reutilizable)
Estructura recomendada de nodos
Crea una escena nueva llamada EnemyPatrol.tscn con un nodo raíz CharacterBody2D (nombre: Enemy). Añade:
Sprite2D(oAnimatedSprite2Dsi ya tienes animaciones)CollisionShape2D(para el cuerpo del enemigo)Area2D(nombre:Hitbox) con suCollisionShape2D(para detectar al jugador)RayCast2D(opcional, nombre:WallCheck) apuntando hacia delante para detectar paredesRayCast2D(opcional, nombre:FloorCheck) apuntando hacia abajo y ligeramente hacia delante para detectar borde/precipicio
La idea es separar “cuerpo que colisiona con el mundo” (CharacterBody2D) de “zona de daño” (Area2D). Esto facilita variar el comportamiento sin rearmar colisiones.
Parámetros exportables y señales
En el script del enemigo, exporta variables para ajustar el comportamiento desde el Inspector y define señales para notificar eventos. Esto permite reutilizar la escena en distintos niveles con valores diferentes.
extends CharacterBody2D
signal hit_player(player: Node, damage: int, knockback: Vector2)
signal died
@export var speed: float = 60.0
@export var patrol_range: float = 120.0
@export var damage: int = 1
@export var knockback_strength: float = 220.0
@export var gravity: float = 900.0
var _start_x: float
var _dir: int = 1
@onready var hitbox: Area2D = $Hitbox
@onready var wall_check: RayCast2D = get_node_or_null("WallCheck")
@onready var floor_check: RayCast2D = get_node_or_null("FloorCheck")
func _ready() -> void:
_start_x = global_position.x
hitbox.body_entered.connect(_on_hitbox_body_entered)
Movimiento de patrulla con colisión
El patrullero se mueve horizontalmente entre dos límites basados en patrol_range. Además, puede invertir dirección si detecta pared o si no hay suelo delante (si usas RayCast2D).
- Escuche el audio con la pantalla apagada.
- Obtenga un certificado al finalizar.
- ¡Más de 5000 cursos para que explores!
Descargar la aplicación
func _physics_process(delta: float) -> void:
# Gravedad
if not is_on_floor():
velocity.y += gravity * delta
else:
velocity.y = 0.0
# Lógica de giro por rango
var left_limit := _start_x - patrol_range * 0.5
var right_limit := _start_x + patrol_range * 0.5
if global_position.x <= left_limit:
_dir = 1
elif global_position.x >= right_limit:
_dir = -1
# Lógica de giro por sensores (opcional)
if wall_check and wall_check.is_colliding():
_dir *= -1
if floor_check and not floor_check.is_colliding():
_dir *= -1
velocity.x = _dir * speed
move_and_slide()
# (Opcional) Voltear sprite según dirección
if has_node("Sprite2D"):
$Sprite2D.flip_h = _dir < 0
Interacción con el jugador: daño, retroceso o reinicio
Para mantener el código modular, el enemigo no debería “asumir” cómo está implementado el jugador. En lugar de llamar métodos específicos, emite una señal con datos (daño y retroceso) y deja que el jugador o un “GameManager” decida qué hacer.
Conecta el Area2D (Hitbox) al evento body_entered. Cuando el jugador entra, emite la señal. El retroceso se calcula desde la posición relativa.
func _on_hitbox_body_entered(body: Node) -> void:
# Filtra por grupo para evitar acoplarte a una clase concreta
if not body.is_in_group("player"):
return
var dir_to_player := (body.global_position - global_position)
var knock_dir := dir_to_player.normalized()
# Empuja al jugador hacia afuera (desde el enemigo hacia el jugador)
var knockback := knock_dir * knockback_strength
emit_signal("hit_player", body, damage, knockback)
En tu escena del jugador (o en un nodo controlador), conecta la señal hit_player del enemigo. Ejemplo de manejo en el jugador (conceptual): aplicar retroceso, reducir vida o reiniciar.
# En el script del jugador (ejemplo de receptor)
func _on_enemy_hit_player(player: Node, damage: int, knockback: Vector2) -> void:
# Si este método está en el propio jugador, puedes ignorar 'player'
# 1) Daño
health -= damage
# 2) Retroceso
velocity += knockback
# 3) Alternativa: reinicio
# get_tree().reload_current_scene()
Conexión de señales en el editor (flujo recomendado)
- Selecciona el nodo
Enemyen la escena donde lo instancies. - En la pestaña “Node”, localiza la señal
hit_player. - Conéctala al nodo que deba reaccionar (jugador o manager).
- Así, el enemigo queda reutilizable y no depende de rutas como
../Player.
Guía práctica paso a paso (implementación rápida)
1) Prepara el enemigo base
- Crea
EnemyPatrol.tscnconCharacterBody2Dcomo raíz. - Añade
CollisionShape2Dal cuerpo. - Añade
Area2D(Hitbox) con suCollisionShape2Dligeramente más grande o igual al cuerpo (según tu diseño). - (Opcional) Añade
WallCheckyFloorCheckcomoRayCast2Dy activaenabled.
2) Añade grupos y capas
- Asigna al jugador el grupo
player(Project Settings → Groups). - Configura capas/máscaras: el cuerpo del enemigo debe colisionar con el mundo; la Hitbox debe detectar al jugador (y no necesariamente al mundo).
3) Pega el script y ajusta exportables
- Adjunta el script al nodo raíz
Enemy. - En el Inspector, prueba valores:
speed=60,patrol_range=160,knockback_strength=220.
4) Instancia y conecta señal
- Instancia
EnemyPatrol.tscnen tu nivel. - Conecta
hit_playeral jugador o a un nodo “GameManager”. - Implementa la reacción: daño, retroceso o reinicio.
Variaciones de comportamiento (mismo enfoque modular)
Las siguientes variaciones comparten la idea clave: parámetros exportables + señales + detección por grupos. Puedes implementarlas como escenas separadas (recomendado) que heredan de una base, o como un único script con “modo” configurable. Para mantener claridad, aquí se muestran como escenas/scripts distintos.
Variación A: Enemigo que cae desde arriba (emboscada)
Comportamiento: permanece “quieto” hasta que el jugador entra en una zona de activación; luego cae (o se suelta) y al tocar el suelo puede quedarse quieto, patrullar o destruirse.
Estructura sugerida:
- Raíz:
CharacterBody2D(EnemyDrop) Area2D(Trigger) para detectar al jugador debajoArea2D(Hitbox) para daño
extends CharacterBody2D
signal hit_player(player: Node, damage: int, knockback: Vector2)
@export var damage: int = 1
@export var gravity: float = 1200.0
@export var drop_speed_cap: float = 900.0
@export var activate_once: bool = true
var _active := false
@onready var trigger: Area2D = $Trigger
@onready var hitbox: Area2D = $Hitbox
func _ready() -> void:
trigger.body_entered.connect(_on_trigger_body_entered)
hitbox.body_entered.connect(_on_hitbox_body_entered)
func _physics_process(delta: float) -> void:
if not _active:
velocity = Vector2.ZERO
return
velocity.y = min(velocity.y + gravity * delta, drop_speed_cap)
move_and_slide()
func _on_trigger_body_entered(body: Node) -> void:
if body.is_in_group("player"):
_active = true
if activate_once:
trigger.monitoring = false
func _on_hitbox_body_entered(body: Node) -> void:
if not body.is_in_group("player"):
return
var knockback := (body.global_position - global_position).normalized() * 200.0
emit_signal("hit_player", body, damage, knockback)
Variación B: Enemigo que persigue a corta distancia (chaser)
Comportamiento: patrulla o se queda quieto, pero si el jugador entra en un radio, lo persigue. Para evitar persecuciones infinitas, usa un rango de detección y otro de “desenganche” (histeresis) o un temporizador.
Estructura sugerida:
- Raíz:
CharacterBody2D(EnemyChaser) Area2D(Aggro) conCollisionShape2DcircularArea2D(Hitbox)
extends CharacterBody2D
signal hit_player(player: Node, damage: int, knockback: Vector2)
@export var speed: float = 80.0
@export var gravity: float = 900.0
@export var damage: int = 1
@export var knockback_strength: float = 220.0
@export var disengage_distance: float = 220.0
var _target: Node2D = null
@onready var aggro: Area2D = $Aggro
@onready var hitbox: Area2D = $Hitbox
func _ready() -> void:
aggro.body_entered.connect(_on_aggro_body_entered)
aggro.body_exited.connect(_on_aggro_body_exited)
hitbox.body_entered.connect(_on_hitbox_body_entered)
func _physics_process(delta: float) -> void:
if not is_on_floor():
velocity.y += gravity * delta
else:
velocity.y = 0.0
if _target and is_instance_valid(_target):
var to_target := _target.global_position - global_position
# Desenganche por distancia (por si sale del área por teletransporte, etc.)
if to_target.length() > disengage_distance:
_target = null
else:
velocity.x = sign(to_target.x) * speed
else:
velocity.x = 0.0
move_and_slide()
func _on_aggro_body_entered(body: Node) -> void:
if body.is_in_group("player"):
_target = body as Node2D
func _on_aggro_body_exited(body: Node) -> void:
if body == _target:
_target = null
func _on_hitbox_body_entered(body: Node) -> void:
if not body.is_in_group("player"):
return
var knockback := (body.global_position - global_position).normalized() * knockback_strength
emit_signal("hit_player", body, damage, knockback)
Recomendaciones para mantener el código modular
1) Señales + grupos en lugar de dependencias directas
- El enemigo emite
hit_playercon datos; el receptor decide si resta vida, aplica invulnerabilidad, reproduce sonido o reinicia. - Usa
body.is_in_group("player")para identificar al jugador sin importar su clase concreta.
2) Separa “detección” de “respuesta”
Area2Ddetecta; el script decide qué señal emitir.- La vida del jugador, UI y reinicio de escena deberían vivir fuera del enemigo.
3) Base común por herencia o composición
Si tendrás varios enemigos, crea una base EnemyBase.gd con señales, parámetros comunes y utilidades (por ejemplo, cálculo de knockback). Luego hereda:
# EnemyBase.gd
extends CharacterBody2D
class_name EnemyBase
signal hit_player(player: Node, damage: int, knockback: Vector2)
@export var damage: int = 1
@export var knockback_strength: float = 220.0
func compute_knockback(target: Node2D) -> Vector2:
return (target.global_position - global_position).normalized() * knockback_strength
Y en cada enemigo:
extends EnemyBase
func _on_hitbox_body_entered(body: Node) -> void:
if body.is_in_group("player"):
emit_signal("hit_player", body, damage, compute_knockback(body))
4) Exporta “tuning knobs” y evita números mágicos
- Velocidad, rango, gravedad, daño y fuerza de retroceso deben ser
@export. - Esto permite iterar el diseño sin tocar código y reduce errores al duplicar enemigos.
5) Mantén estados simples y explícitos
Para comportamientos más complejos, usa un estado (enum) y transiciones claras (por ejemplo, IDLE, PATROL, CHASE, STUN). Evita mezclar muchas condiciones sueltas en _physics_process.
enum State { IDLE, PATROL, CHASE }
var state: State = State.PATROL