Enemigos, IA básica y dificultad progresiva en Construct

Capítulo 9

Tiempo estimado de lectura: 11 minutos

+ Ejercicio

Objetivo del capítulo

En este capítulo vas a construir enemigos con IA básica usando solo eventos: patrullaje, persecución por distancia, disparo por temporizador y un sistema de estados (idle/alert/attack). Además, implementarás spawn/despawn, oleadas y dificultad progresiva con variables, y definirás hitboxes y reglas de daño equilibradas. La práctica final incluye dos tipos de enemigo con comportamientos distintos y un sistema de oleadas que ajusta velocidad, vida y frecuencia de ataque.

IA sin programar: pensar en “reglas” y “estados”

En Construct, la IA básica se resuelve combinando: condiciones (distancia, línea de visión, colisiones, temporizadores) + acciones (mover, girar, disparar, cambiar variables) + variables (estado, vida, cooldown). El truco para que no se vuelva caótico es usar un estado por enemigo y permitir transiciones claras.

Estados recomendados (idle / alert / attack)

  • idle: patrulla o se queda quieto.
  • alert: detectó al jugador, se orienta y se acerca o busca.
  • attack: ejecuta el ataque (melee o disparo) con cooldown.

Representa el estado con una variable de instancia en el enemigo, por ejemplo state (texto) o state (número: 0/1/2). Para este capítulo usaremos texto por claridad: "idle", "alert", "attack".

Preparación: variables y objetos mínimos

Objetos sugeridos

  • Player (ya existe en tu proyecto).
  • Enemy_Chaser (enemigo que persigue y golpea).
  • Enemy_Shooter (enemigo que mantiene distancia y dispara).
  • Bullet_Enemy (proyectil enemigo).
  • Hitbox_Player (opcional, para separar colisión visual de daño).
  • Hitbox_Enemy (opcional).
  • Spawner (puede ser un Sprite invisible o un objeto punto).

Variables globales para oleadas y dificultad

Crea variables globales (Project):

  • Wave (número, inicia en 1)
  • EnemiesAlive (número, inicia en 0)
  • EnemiesToSpawn (número, inicia en 0)
  • SpawnedThisWave (número, inicia en 0)
  • Difficulty (número, inicia en 1)
  • SpawnInterval (número, por ejemplo 1.2)

Variables de instancia por enemigo

En Enemy_Chaser:

Continúa en nuestra aplicación.
  • Escuche el audio con la pantalla apagada.
  • Obtenga un certificado al finalizar.
  • ¡Más de 5000 cursos para que explores!
O continúa leyendo más abajo...
Download App

Descargar la aplicación

  • hp (número, ej. 30)
  • state (texto, inicia "idle")
  • speedBase (número, ej. 120)
  • damage (número, ej. 10)
  • attackCooldown (número, ej. 0.8)
  • canAttack (booleano, inicia true)
  • detectRange (número, ej. 320)
  • attackRange (número, ej. 48)
  • patrolMinX, patrolMaxX (número, para patrulla horizontal)
  • dir (número, inicia 1; 1 derecha, -1 izquierda)

En Enemy_Shooter:

  • hp (número, ej. 20)
  • state (texto, inicia "idle")
  • speedBase (número, ej. 90)
  • detectRange (número, ej. 420)
  • keepDistance (número, ej. 220)
  • fireCooldown (número, ej. 1.1)
  • canFire (booleano, inicia true)
  • bulletSpeed (número, ej. 420)
  • damage (número, ej. 8)

Patrullaje sin programar (Enemy_Chaser)

El patrullaje más estable sin comportamientos extra es mover al enemigo entre dos límites y cambiar dirección al llegar.

Paso a paso: definir límites de patrulla al crear el enemigo

  1. Cuando instancies un Enemy_Chaser, asigna sus límites: patrolMinX = X - 120 y patrolMaxX = X + 120 (ajusta el ancho según tu nivel).
  2. Inicializa dir = 1 y state = "idle".

Eventos de patrulla

Enemy_Chaser | state = "idle"  -> Set X to X + dir * speedBase * dt * Difficulty
Enemy_Chaser | state = "idle" AND X >= patrolMaxX  -> Set dir to -1
Enemy_Chaser | state = "idle" AND X <= patrolMinX  -> Set dir to 1

Nota didáctica: usar dt (delta time) hace que la velocidad sea consistente a distintos FPS. Si ya estás usando un comportamiento de movimiento en tu proyecto, mantén el enfoque de este capítulo en la lógica de IA (estado y condiciones) y aplica la velocidad con el método que ya uses.

Persecución por distancia (detección simple)

La detección por distancia es el “sensor” más común. Se basa en comparar la distancia entre enemigo y jugador con un rango.

Transición idle → alert

Enemy_Chaser | state = "idle" AND distance(Enemy_Chaser.X, Enemy_Chaser.Y, Player.X, Player.Y) <= detectRange  -> Set state to "alert"

Movimiento en alert (perseguir)

Enemy_Chaser | state = "alert"  -> Move toward position (Player.X, Player.Y) at (speedBase * Difficulty)

Si no quieres usar “Move toward position”, puedes simularlo con ángulo hacia el jugador y avance. Lo importante es que el enemigo cambie de comportamiento al entrar en alert.

Transición alert → idle (si el jugador se aleja)

Enemy_Chaser | state = "alert" AND distance(...) > detectRange * 1.2  -> Set state to "idle"

El multiplicador 1.2 crea histeresis: evita que el estado cambie en bucle cuando el jugador está justo en el borde del rango.

Ataque melee con cooldown (Enemy_Chaser)

Para un melee equilibrado necesitas: rango de ataque, ventana de daño (hitbox) y cooldown.

Transición alert → attack

Enemy_Chaser | state = "alert" AND distance(...) <= attackRange  -> Set state to "attack"

Ejecutar ataque con “canAttack”

Enemy_Chaser | state = "attack" AND canAttack = true  -> Set canAttack to false; (Aplicar daño si colisiona); Wait attackCooldown seconds; Set canAttack to true

Aplicar daño: lo más limpio es que el daño ocurra solo si el enemigo está en rango y hay solape con la hitbox del jugador.

Enemy_Chaser | state = "attack" AND canAttack = true AND Enemy_Chaser is overlapping Hitbox_Player  -> Subtract Enemy_Chaser.damage from Player.hp

Salir de attack

Enemy_Chaser | state = "attack" AND distance(...) > attackRange * 1.2  -> Set state to "alert"

Disparo por temporizador (Enemy_Shooter)

El disparo por temporizador se basa en un cooldown que habilita el disparo. El enemigo “shooter” suele ser más interesante si intenta mantener distancia.

Transición idle → alert

Enemy_Shooter | state = "idle" AND distance(...) <= detectRange  -> Set state to "alert"

Movimiento en alert: mantener distancia

Regla simple:

  • Si el jugador está muy cerca, el shooter se aleja.
  • Si está muy lejos pero dentro de detección, se acerca.
  • Si está cerca de la distancia ideal, se queda y dispara.
Enemy_Shooter | state = "alert" AND distance(...) < keepDistance * 0.8  -> Move away from Player at (speedBase * Difficulty)
Enemy_Shooter | state = "alert" AND distance(...) > keepDistance * 1.2  -> Move toward Player at (speedBase * Difficulty)
Enemy_Shooter | state = "alert" AND distance(...) between keepDistance*0.8 and keepDistance*1.2  -> Stop movement (o velocidad 0)

Transición alert → attack (cuando está en rango de disparo)

Enemy_Shooter | state = "alert" AND distance(...) <= detectRange  -> Set state to "attack"

En este caso, attack significa “modo disparo”, no necesariamente quedarse quieto.

Disparo con cooldown

Enemy_Shooter | state = "attack" AND canFire = true  -> Set canFire to false; Spawn Bullet_Enemy; Set Bullet_Enemy angle toward Player; Set Bullet_Enemy speed to bulletSpeed; Wait fireCooldown seconds; Set canFire to true

Consejo: si el jugador puede esquivar, añade una pequeña imprecisión para que no sea injusto:

Set Bullet_Enemy angle to angle(Enemy_Shooter.X, Enemy_Shooter.Y, Player.X, Player.Y) + random(-6, 6)

Spawn / Despawn: controlar la población de enemigos

El spawn crea presión y ritmo; el despawn evita que el nivel se llene de objetos fuera de cámara o irrelevantes.

Spawn con un objeto Spawner

Coloca varios Spawner en el layout (por ejemplo, en bordes o puertas). Cada spawner puede tener una variable de instancia type (texto: "chaser" o "shooter").

Reglas de despawn recomendadas

  • Si el enemigo está demasiado lejos del jugador durante cierto tiempo.
  • Si sale de los límites del layout.
  • Si la oleada terminó y quieres limpiar rezagados (opcional).
Enemy_* | distance(...) > 1200  -> Destroy

Si destruyes enemigos por despawn, recuerda ajustar EnemiesAlive para no romper la lógica de oleadas.

Oleadas: estructura simple y robusta

Una oleada necesita: cuántos enemigos se generan, con qué intervalo, y cómo detectas que terminó.

Definir el tamaño de oleada y el intervalo

Ejemplo de fórmula (ajústala a tu juego):

  • EnemiesToSpawn = 4 + Wave * 2
  • SpawnInterval = max(0.35, 1.2 - Wave * 0.08)
  • Difficulty = 1 + Wave * 0.12

Inicio de oleada

System | On start of layout  -> Set Wave=1; Call StartWave
Function StartWave  -> Set SpawnedThisWave=0; Set EnemiesAlive=0; Set EnemiesToSpawn = 4 + Wave*2; Set Difficulty = 1 + Wave*0.12; Set SpawnInterval = max(0.35, 1.2 - Wave*0.08)

Spawning por temporizador (sin bucles complejos)

Usa un temporizador repetitivo mientras falten enemigos por generar.

System | Every SpawnInterval seconds AND SpawnedThisWave < EnemiesToSpawn  -> Pick a random Spawner; Spawn enemy based on Spawner.type; Add 1 to SpawnedThisWave; Add 1 to EnemiesAlive

Para elegir tipo sin spawners tipados, puedes hacerlo por probabilidad:

System | (al spawnear)  -> If random(0,100) < 60 spawn Chaser else spawn Shooter

Fin de oleada

System | SpawnedThisWave = EnemiesToSpawn AND EnemiesAlive = 0  -> Add 1 to Wave; Call StartWave

Actualizar EnemiesAlive al morir

Enemy_* | hp <= 0  -> Destroy; Subtract 1 from EnemiesAlive

Importante: asegúrate de que EnemiesAlive solo se incrementa cuando el enemigo realmente aparece y solo se decrementa una vez (al morir o despawnear).

Dificultad progresiva: qué ajustar y cómo evitar picos injustos

La dificultad progresiva funciona mejor cuando ajustas pocas variables pero de forma consistente:

  • Velocidad: aumenta presión y reduce margen de error.
  • Vida: alarga combates, pero puede volverse tedioso.
  • Frecuencia de ataque: sube intensidad; cuidado con “spam”.

Aplicar escalado al instanciar enemigos

En el momento del spawn, ajusta stats con Difficulty para que cada oleada “nazca” con valores coherentes.

On spawn Enemy_Chaser  -> Set hp to round(30 * (1 + (Difficulty-1)*0.8)); Set speedBase to 120 * (1 + (Difficulty-1)*0.35); Set attackCooldown to max(0.35, 0.8 - (Difficulty-1)*0.08)
On spawn Enemy_Shooter  -> Set hp to round(20 * (1 + (Difficulty-1)*0.7)); Set bulletSpeed to 420 * (1 + (Difficulty-1)*0.25); Set fireCooldown to max(0.4, 1.1 - (Difficulty-1)*0.10)

Regla práctica: limita con max() los cooldowns para que nunca lleguen a valores imposibles de esquivar.

Hitboxes y reglas de daño equilibradas

Separar “cuerpo” de “daño”

Si tu sprite tiene formas irregulares, una hitbox dedicada hace el combate más justo. Dos enfoques comunes:

  • Hitbox como objeto hijo: un Sprite invisible anclado al jugador/enemigo (con Pin).
  • Polígono de colisión ajustado en el propio Sprite (más simple, menos flexible).

Invulnerabilidad breve (i-frames) para evitar daño múltiple

Si el jugador puede recibir daño por solape continuo, añade una ventana de invulnerabilidad. Variable sugerida en Player: invuln (boolean) y/o invulnTime.

Player | On damaged AND invuln = false  -> Set invuln=true; Wait 0.6 seconds; Set invuln=false

Luego, cualquier evento de daño debe comprobar invuln = false.

Tabla de balance inicial (ejemplo)

ElementoValor recomendadoMotivo
Chaser daño8–12Castiga cercanía, pero permite reaccionar
Chaser cooldown0.7–1.0 sEvita “triturar” al jugador
Shooter daño bala6–10Menor que melee por ser a distancia
Shooter cooldown0.9–1.3 sDa tiempo a esquivar
I-frames jugador0.4–0.8 sEvita daño múltiple por solape

Práctica guiada: 2 enemigos + oleadas + dificultad

Parte A: Enemy_Chaser completo (patrulla → persecución → melee)

  1. Crea Enemy_Chaser con variables: hp, state, speedBase, damage, attackCooldown, canAttack, detectRange, attackRange, patrolMinX, patrolMaxX, dir.
  2. Al spawnear: asigna límites de patrulla alrededor de su X inicial y escala stats con Difficulty.
  3. Eventos de idle: mover en X con dir y cambiar dirección en límites.
  4. Detección: si distancia ≤ detectRange, cambia a alert.
  5. Persecución: en alert, moverse hacia el jugador.
  6. Entrar en attack: si distancia ≤ attackRange, state="attack".
  7. Daño con cooldown: si solapa hitbox del jugador y canAttack=true y jugador no invulnerable, resta vida y activa cooldown.
  8. Salir de attack: si se aleja, vuelve a alert; si se aleja mucho, vuelve a idle.

Parte B: Enemy_Shooter completo (alert → mantener distancia → disparo)

  1. Crea Enemy_Shooter con variables: hp, state, speedBase, detectRange, keepDistance, fireCooldown, canFire, bulletSpeed, damage.
  2. Al spawnear: escala hp, fireCooldown y bulletSpeed con Difficulty.
  3. Detección: distancia ≤ detectRangealert.
  4. Movimiento: si está muy cerca, alejarse; si está muy lejos, acercarse; si está en rango ideal, frenar.
  5. Disparo: en attack (o en alert si prefieres simplificar), si canFire entonces crear Bullet_Enemy, apuntar al jugador con ligera desviación y aplicar velocidad.
  6. Daño de bala: al colisionar con hitbox del jugador, restar vida (si no invulnerable) y destruir bala.
  7. Despawn de balas: destruir si salen de pantalla o tras X segundos para evitar acumulación.

Parte C: Sistema de oleadas con spawners

  1. Coloca varios Spawner en el layout (mínimo 3). Opcional: variable type para forzar qué enemigo sale de cada punto.
  2. Crea función StartWave que calcule EnemiesToSpawn, SpawnInterval y Difficulty según Wave, y reinicie contadores.
  3. Evento de spawn periódico: cada SpawnInterval segundos, si SpawnedThisWave < EnemiesToSpawn, elige un spawner y genera un enemigo. Incrementa SpawnedThisWave y EnemiesAlive.
  4. Control de fin de oleada: cuando SpawnedThisWave = EnemiesToSpawn y EnemiesAlive = 0, incrementa Wave y llama StartWave.
  5. Despawn seguro: si destruyes enemigos por distancia/límites, decrementa EnemiesAlive igual que si murieran.

Parte D: Ajuste de dificultad que modifique velocidad, vida y frecuencia

Implementa el escalado en el evento de spawn (no en un “Every tick”), para que sea predecible y fácil de balancear. Recomendación:

  • Chaser: sube velocidad moderada + baja un poco cooldown + sube vida.
  • Shooter: sube velocidad poco + sube velocidad de bala + baja cooldown con límite + sube vida.

Verifica en juego que:

  • En oleadas tempranas, el jugador aprende patrones (tiempo para reaccionar).
  • En oleadas medias, el reto viene de la combinación (chaser presiona, shooter castiga).
  • En oleadas altas, el límite de cooldown evita que sea imposible.

Ahora responde el ejercicio sobre el contenido:

¿Cuál es la práctica recomendada para que la dificultad progresiva sea predecible y fácil de balancear en un sistema de oleadas?

¡Tienes razón! Felicitaciones, ahora pasa a la página siguiente.

¡Tú error! Inténtalo de nuevo.

El escalado al spawnear hace que cada oleada nazca con valores coherentes y fáciles de ajustar. Además, limitar los cooldown con un mínimo evita ataques imposibles de esquivar y picos injustos.

Siguiente capítulo

Optimización, depuración y publicación de proyectos en Construct

Arrow Right Icon
Portada de libro electrónico gratuitaConstruct desde Cero: Aprende a Crear Juegos Sin Programar
90%

Construct desde Cero: Aprende a Crear Juegos Sin Programar

Nuevo curso

10 páginas

Descarga la aplicación para obtener una certificación gratuita y escuchar cursos en segundo plano, incluso con la pantalla apagada.