Parte: 1 — Motores 2D y tu primer juego jugable · Fuente: Documentación de Godot 4 ⏱️ Duración estimada: 95 min · Nivel: Intermedio
Construir un enemigo con inteligencia artificial básica en 2D. Será un CharacterBody2D que patrulla entre bordes, detecta al jugador mediante un Area2D de visión y lo persigue cuando entra en su rango, usando RayCast2D para no caer de las plataformas.
Al terminar tendrás una escena Enemigo reutilizable con su propia máquina de estados (PATROL, CHASE, ATTACK), aplicando el patrón FSM que aprendiste para el jugador. Este enemigo es la primera pieza del sistema de combate que completaremos en las siguientes clases.
Al finalizar, el alumno podrá:
CharacterBody2D con FSM propia.Area2D de visión y sus señales body_entered/body_exited.RayCast2D para consultar el entorno y evitar caídas.| # | Tema | Por qué importa |
|---|---|---|
| 1 | Enemigo como CharacterBody2D | Reutiliza la física de movimiento del jugador. |
| 2 | Patrulla ir y volver | Da vida al nivel sin scripting complejo. |
| 3 | Detección de borde con RayCast2D | Evita que el enemigo se despeñe. |
| 4 | Detección de pared con RayCast2D | Permite girar ante obstáculos. |
| 5 | Área de visión | Define el rango en que el enemigo "ve" al jugador. |
| 6 | Persecución | Convierte al enemigo en una amenaza activa. |
| 7 | FSM del enemigo | Organiza patrulla, persecución y ataque. |
| 8 | Capas y máscaras de colisión | Aseguran que solo el jugador dispare la visión. |
move_and_slide() mueve según velocity.is_colliding() se consulta tras _physics_process.body_entered con el cuerpo detectado.Trabaja en PlataformasCurso. Necesitas la escena Jugador en el grupo "jugador" o en una capa de colisión propia (por ejemplo la capa 2, "Player"). Configura en Project Settings > Layer Names > 2D Physics nombres claros: capa 1 World, capa 2 Player, capa 3 Enemy. Consulta la documentación de RayCast2D (https://docs.godotengine.org/en/stable/classes/class_raycast2d.html) y de Area2D (https://docs.godotengine.org/en/stable/classes/class_area2d.html).
Añade al Jugador al grupo jugador desde la pestaña Node > Groups para identificarlo con facilidad desde el enemigo.
Crearemos una escena Enemigo que patrulla, detecta y persigue.
Crea una nueva escena con raíz CharacterBody2D llamada Enemigo. Añade como hijos: AnimatedSprite2D, CollisionShape2D, dos RayCast2D (RayoSuelo y RayoPared) y un Area2D llamado Vision con su propio CollisionShape2D. Configura la capa del enemigo en 3 y en Vision la máscara solo en la capa 2 (Player).
Orienta los rayos: RayoPared con target_position = (16, 0) (hacia delante) y RayoSuelo con target_position = (16, 20) (hacia delante y abajo, para "sentir" el borde). Guarda la escena como escenas/enemigo.tscn.
Añade el script al nodo raíz con el enum, constantes y referencias.
extends CharacterBody2D
enum Estado { PATROL, CHASE, ATTACK }
const VELOCIDAD_PATRULLA := 45.0
const VELOCIDAD_PERSECUCION := 80.0
const GRAVEDAD := 900.0
@onready var sprite: AnimatedSprite2D = $AnimatedSprite2D
@onready var rayo_pared: RayCast2D = $RayoPared
@onready var rayo_suelo: RayCast2D = $RayoSuelo
@onready var vision: Area2D = $Vision
var estado: Estado = Estado.PATROL
var direccion: int = 1
var objetivo: Node2D = null
_ready y define sus manejadores. Al entrar el jugador pasamos a CHASE; al salir volvemos a PATROL.func _ready() -> void:
vision.body_entered.connect(_on_vision_body_entered)
vision.body_exited.connect(_on_vision_body_exited)
func _on_vision_body_entered(body: Node2D) -> void:
if body.is_in_group("jugador"):
objetivo = body
estado = Estado.CHASE
func _on_vision_body_exited(body: Node2D) -> void:
if body == objetivo:
objetivo = null
estado = Estado.PATROL
_physics_process: gravedad, despacho por estado, orientación de sprite y rayos según direccion, y movimiento.func _physics_process(delta: float) -> void:
if not is_on_floor():
velocity.y += GRAVEDAD * delta
match estado:
Estado.PATROL:
_patrullar()
Estado.CHASE:
_perseguir()
Estado.ATTACK:
velocity.x = 0.0
sprite.flip_h = direccion < 0
rayo_pared.target_position.x = 16 * direccion
rayo_suelo.target_position.x = 16 * direccion
move_and_slide()
func _patrullar() -> void:
velocity.x = direccion * VELOCIDAD_PATRULLA
sprite.play("walk")
var hay_borde := not rayo_suelo.is_colliding()
var hay_pared := rayo_pared.is_colliding()
if is_on_floor() and (hay_borde or hay_pared):
direccion *= -1
func _perseguir() -> void:
if objetivo == null:
estado = Estado.PATROL
return
sprite.play("walk")
direccion = 1 if objetivo.global_position.x > global_position.x else -1
velocity.x = direccion * VELOCIDAD_PERSECUCION
if abs(objetivo.global_position.x - global_position.x) < 12.0:
estado = Estado.ATTACK
Enemigo en tu nivel, colócalos sobre plataformas y ejecuta. Verifica que patrullan sin caerse, giran al llegar a una pared y te persiguen al entrar en su rango de visión.CollisionShape2D con forma de sector amplio para que la visión sea un cono frontal.alert de medio segundo al pasar de PATROL a CHASE.@export var velocidad_persecucion para ajustar la dificultad por instancia sin tocar el script.@export var direccion_inicial.Crea un segundo tipo de enemigo "volador" que ignore la gravedad y los RayCast2D, patrulle entre dos Marker2D colocados en la escena del nivel y, al detectar al jugador, lo persiga en las dos dimensiones (X e Y) hasta cierta distancia. Criterio de aceptación: el enemigo volador oscila entre ambos marcadores en bucle, al entrar el jugador en su visión cambia a persecución diagonal siguiéndolo, y al salir del rango retoma la patrulla entre los marcadores sin quedarse atascado.
| Síntoma / mensaje | Causa y cómo arreglar |
|---|---|
| La visión nunca dispara | La máscara de Vision no incluye la capa del jugador; corrígela en el inspector. |
| El enemigo se cae de la plataforma | RayoSuelo mal orientado o sin llegar al suelo; ajusta su target_position. |
body_entered detecta al propio enemigo |
El área también mira su capa; deja la máscara solo en Player. |
| Gira en el sitio sin avanzar | Invierte dirección cada frame porque el rayo topa consigo mismo; aléjalo del cuerpo. |
| Persigue aun fuera de pantalla | No limpias objetivo en body_exited; compara body == objetivo. |
❓ ¿Área de visión o distancia para detectar? El Area2D es más natural y barato: solo reacciona cuando el jugador entra. La distancia por código sirve si quieres un radio omnidireccional exacto.
❓ ¿Por qué dos RayCast2D y no uno? Uno mira la pared (frente) y otro el borde (frente-abajo). Cumplen roles distintos y combinarlos en uno daría lecturas ambiguas.
❓ ¿Debo activar el rayo con force_raycast_update? Solo si necesitas su lectura el mismo frame en que lo mueves. En patrulla normal basta con leerlo en el siguiente _physics_process.
❓ ¿El estado ATTACK ya hace daño? Todavía no. Aquí solo detiene al enemigo junto al jugador; el daño real lo implementaremos con hitbox/hurtbox en la clase 038.
Clase 036 - Máquina de estados del jugador: idle, run, jump, fall